По ARCHITECTURE.md: реестр независимых секций, JSON с ключом на секцию. Ключи — явные строковые константы, а не имена классов: релизная сборка минифицирует имена и молча потеряла бы все сохранения. - ISaveSection, SaveManager, IStorage + MemoryStorage - CocosStorage: единственный файл модуля, знающий про движок; при отказе хранилища уходит в память, а не роняет игру - StatsSection: сыграно/побед/поражений/ничьих, текущая и лучшая серия - терпимая загрузка: битый JSON, чужая версия, сломанная секция или отвалившееся хранилище не ломают остальное - проверка чистоты расширена на modules/ со списком адаптеров Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
60 lines
1.9 KiB
JavaScript
60 lines
1.9 KiB
JavaScript
// 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');
|