Files
simstudioai--sim/scripts/check-utils-enforcement.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

155 lines
4.7 KiB
TypeScript

#!/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()