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
155 lines
4.7 KiB
TypeScript
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()
|