-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Expand file tree
/
Copy pathcheck-utils-enforcement.ts
More file actions
154 lines (139 loc) · 4.74 KB
/
check-utils-enforcement.ts
File metadata and controls
154 lines (139 loc) · 4.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#!/usr/bin/env bun
/**
* Enforces use of shared @sim/utils helpers over inline implementations.
*
* Biome's noRestrictedImports covers import-based bans (nanoid, uuid, crypto named imports).
* This script catches patterns that static import analysis misses — global property access,
* inline idioms, and reimplemented helpers that should live in @sim/utils.
*/
import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
const ROOT = path.resolve(import.meta.dir, '..')
const SCAN_DIRS = [path.join(ROOT, 'apps'), path.join(ROOT, 'packages')]
const SKIP_DIRS = new Set(['node_modules', 'dist', '.next', '.turbo', 'coverage', 'bundles'])
/** Files that implement the utilities themselves — allowed to use the underlying primitives. */
const ALLOWLISTED_FILES = new Set([
'packages/utils/src/errors.ts',
'packages/utils/src/helpers.ts',
'packages/utils/src/random.ts',
'packages/utils/src/id.ts',
'packages/utils/src/object.ts',
'packages/utils/src/retry.ts',
'packages/utils/src/errors.test.ts',
'packages/utils/src/helpers.test.ts',
'packages/utils/src/random.test.ts',
'packages/utils/src/id.test.ts',
'packages/utils/src/object.test.ts',
'packages/utils/src/retry.test.ts',
'packages/cli/src/index.ts',
'packages/ts-sdk/src/index.ts',
// CJS bundle — cannot use ES module imports
'apps/sim/lib/execution/isolated-vm-worker.cjs',
// Uses crypto.getRandomValues() directly (not crypto.randomUUID) — TSDoc comment triggers false positive
'packages/testing/src/factories/id.ts',
])
const BANNED_PATTERNS: Array<{
pattern: RegExp
description: string
suggestion: string
}> = [
// Randomness / ID generation — global property access that import bans miss
{
pattern: /\bMath\.random\s*\(/g,
description: 'Math.random()',
suggestion: 'randomInt / randomFloat / randomItem from @sim/utils/random',
},
{
pattern: /\bcrypto\.randomUUID\s*\(/g,
description: 'crypto.randomUUID()',
suggestion: 'generateId() or generateShortId() from @sim/utils/id',
},
{
pattern: /\bcrypto\.randomBytes\s*\(/g,
description: 'crypto.randomBytes()',
suggestion: 'generateRandomBytes() or generateRandomHex() from @sim/utils/random',
},
// Deep clone idiom
{
pattern: /JSON\.parse\s*\(\s*JSON\.stringify\s*\(/g,
description: 'JSON.parse(JSON.stringify(...))',
suggestion: 'structuredClone() — built-in, no import needed',
},
// Inline error message extraction (excludes null/undefined/false fallbacks — those have different semantics)
{
pattern: /instanceof Error\s*\?\s*\w+\.message\s*:\s*(?!\s*null\b|\s*undefined\b|\s*false\b)./g,
description: 'e instanceof Error ? e.message : fallback',
suggestion: 'getErrorMessage(e, fallback?) from @sim/utils/errors',
},
// Inline sleep
{
pattern: /new Promise\s*[(<]\s*(?:resolve|\(resolve\))\s*=>\s*setTimeout\s*\(\s*resolve/g,
description: 'new Promise(resolve => setTimeout(resolve, ms))',
suggestion: 'sleep(ms) from @sim/utils/helpers',
},
]
async function walk(dir: string, results: string[] = []): Promise<string[]> {
let entries
try {
entries = await readdir(dir, { withFileTypes: true })
} catch {
return results
}
for (const entry of entries) {
if (SKIP_DIRS.has(entry.name)) continue
const full = path.join(dir, entry.name)
if (entry.isDirectory()) {
await walk(full, results)
} else if (/\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(entry.name)) {
results.push(full)
}
}
return results
}
interface Violation {
file: string
line: number
description: string
suggestion: string
snippet: string
}
async function main() {
const allFiles: string[] = []
for (const dir of SCAN_DIRS) {
await walk(dir, allFiles)
}
const violations: Violation[] = []
for (const file of allFiles) {
const rel = path.relative(ROOT, file)
if (ALLOWLISTED_FILES.has(rel)) continue
const content = await readFile(file, 'utf8')
const lines = content.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
for (const { pattern, description, suggestion } of BANNED_PATTERNS) {
pattern.lastIndex = 0
if (pattern.test(line)) {
violations.push({
file: rel,
line: i + 1,
description,
suggestion,
snippet: line.trim(),
})
}
}
}
}
if (violations.length === 0) {
console.log('✓ No banned patterns found.')
process.exit(0)
}
console.error(`\nFound ${violations.length} banned pattern(s):\n`)
for (const v of violations) {
console.error(` ${v.file}:${v.line}`)
console.error(` ✗ ${v.description} → use ${v.suggestion}`)
console.error(` ${v.snippet}\n`)
}
process.exit(1)
}
main()