// Guards the one architectural rule the type system cannot express: the game // logic must not depend on the engine. See docs/TECHNICAL.md. // // core/ never imports `cc`, at all. // modules/ is engine-aware, but only through the adapters listed below — // everything else in it stays testable headlessly. // // A new file that needs `cc` under modules/ is a deliberate decision, so it has // to be added here on purpose rather than slipping in unnoticed. import { readdirSync, readFileSync, statSync } from 'node:fs'; import { join, relative, sep } from 'node:path'; const PURE_ROOT = 'assets/scripts/core'; const MODULES_ROOT = 'assets/scripts/modules'; const ENGINE_ADAPTERS = new Set([ 'save/CocosStorage.ts', ]); const ENGINE_IMPORT = /\bfrom\s+['"](cc|cc\/env)['"]/; const offenders = []; function walk(dir, onFile) { for (const entry of readdirSync(dir)) { const path = join(dir, entry); if (statSync(path).isDirectory()) walk(path, onFile); else if (path.endsWith('.ts')) onFile(path); } } function engineImports(path) { return readFileSync(path, 'utf8') .split('\n') .map((line, i) => ({ line: line.trim(), number: i + 1 })) .filter((entry) => ENGINE_IMPORT.test(entry.line)); } walk(PURE_ROOT, (path) => { for (const hit of engineImports(path)) { offenders.push(`${path}:${hit.number} ${hit.line} (core/ must never import the engine)`); } }); walk(MODULES_ROOT, (path) => { const key = relative(MODULES_ROOT, path).split(sep).join('/'); if (ENGINE_ADAPTERS.has(key)) return; for (const hit of engineImports(path)) { offenders.push( `${path}:${hit.number} ${hit.line} (add '${key}' to ENGINE_ADAPTERS if this is intended)`, ); } }); if (offenders.length > 0) { console.error(`Engine dependency in code that must stay pure:\n${offenders.join('\n')}`); process.exit(1); } console.log('core purity OK');