import { readdirSync, readFileSync, statSync } from 'node:fs'; import { dirname, isAbsolute, relative, resolve } from 'node:path'; const SDK_ALLOWED = /framework[\\/]sdk(?:[\\/]|$)/; const FRAMEWORK_INTERNAL = /framework[\\/](?:net|protocol|platform|application|domain|presentation|ui|core|compat)(?:[\\/]|$)/; const LEGACY_RUNTIME = /framework[\\/](?:net[\\/]net-client|platform[\\/](?:session|startup|room-rpc-bus|readonly|stores[\\/](?:app-store|player-store|room-store|types))|protocol[\\/]room-handlers)\.ts$/; const CONTRACTS_PATH = /framework[\\/]sdk[\\/]contracts(?:[\\/]|$)/; const MIGRATION_ONLY_NAMES = new Set(['GameContext', 'IGameModule']); /** * Recursively scans TypeScript imports for framework/game layering violations. * Paths in violations are normalized for useful, cross-platform diagnostics. */ export function scanImportBoundaries(options) { const frameworkDir = resolve(options.frameworkDir); const gamesDir = resolve(options.gamesDir); const violations = []; for (const file of [...typescriptFiles(frameworkDir), ...typescriptFiles(gamesDir)]) { const source = readFileSync(file, 'utf8'); for (const imported of extractImports(source)) { const resolved = imported.specifier.startsWith('.') ? resolve(dirname(file), imported.specifier) : null; const violation = findViolation({ file, specifier: imported.specifier, bindings: imported.bindings, resolved, frameworkDir, gamesDir, }); if (violation) violations.push(violation); } } return violations; } function* typescriptFiles(directory) { let entries; try { entries = readdirSync(directory); } catch (error) { if (error && error.code === 'ENOENT') return; throw error; } for (const entry of entries) { const file = resolve(directory, entry); if (statSync(file).isDirectory()) yield* typescriptFiles(file); else if (file.endsWith('.ts')) yield file; } } function extractImports(source) { const imports = []; const staticImport = /\bimport\s+(?!\()(?:(?:type\s+)?([^;\n]+?)\s+from\s+)?(['"])([^'"\n]+)\2/g; for (const match of source.matchAll(staticImport)) { imports.push({ specifier: match[3], bindings: match[1] ?? '' }); } const dynamicImport = /\bimport\s*\(\s*(['"])([^'"\n]+)\1\s*\)/g; for (const match of source.matchAll(dynamicImport)) { imports.push({ specifier: match[2], bindings: '' }); } return imports; } function findViolation(context) { const { file, specifier, bindings, resolved, frameworkDir, gamesDir } = context; const filePath = displayPath(file); const resolvedPath = resolved ? displayPath(resolved) : specifier; const isContract = CONTRACTS_PATH.test(filePath); const isGame = isInside(file, gamesDir); const isFramework = isInside(file, frameworkDir); if (isContract && specifier === 'cc') { return violation(filePath, specifier, `sdk/contracts cannot import cc (${filePath})`); } if (isContract && resolved && dirname(resolved) !== dirname(file)) { return violation(filePath, specifier, `sdk/contracts may import only sibling contracts; sdk path resolved to ${resolvedPath}`); } if (isGame && resolved && isFrameworkReference(resolved, frameworkDir) && !SDK_ALLOWED.test(resolvedPath)) { return violation(filePath, specifier, `game code may import only framework/sdk; game import resolved to ${resolvedPath}`); } if (isGame && resolved && SDK_ALLOWED.test(resolvedPath) && importsMigrationOnlyName(bindings)) { return violation(filePath, specifier, `game code cannot import ${migrationOnlyName(bindings)}; it is migration-only`); } if (isFramework && resolved && isInside(resolved, gamesDir)) { return violation(filePath, specifier, `framework code cannot import games; framework import resolved to ${resolvedPath}`); } if (resolved && LEGACY_RUNTIME.test(resolvedPath) && !isLegacyRuntimeImporter(filePath, frameworkDir)) { return violation(filePath, specifier, `new production code cannot import quarantined legacy runtime ${resolvedPath}`); } return null; } function isFrameworkReference(path, frameworkDir) { return isInside(path, frameworkDir) || SDK_ALLOWED.test(path) || FRAMEWORK_INTERNAL.test(path) || /(?:^|[\\/])framework(?:[\\/]|$)/.test(path); } function isLegacyRuntimeImporter(filePath, frameworkDir) { const normalizedFrameworkDir = displayPath(frameworkDir); return filePath === `${normalizedFrameworkDir}/sdk/index.ts` || LEGACY_RUNTIME.test(filePath); } function importsMigrationOnlyName(bindings) { return [...MIGRATION_ONLY_NAMES].some((name) => new RegExp(`\\b${name}\\b`).test(bindings)); } function migrationOnlyName(bindings) { return [...MIGRATION_ONLY_NAMES].find((name) => new RegExp(`\\b${name}\\b`).test(bindings)); } function isInside(file, directory) { const pathFromDirectory = relative(directory, file); return pathFromDirectory !== '' && !pathFromDirectory.startsWith('..') && !isAbsolute(pathFromDirectory); } function displayPath(file) { return file.replaceAll('\\', '/'); } function violation(file, specifier, message) { return { file, specifier, message }; } export { FRAMEWORK_INTERNAL, LEGACY_RUNTIME, SDK_ALLOWED };