M3: модуль сохранения
По ARCHITECTURE.md: реестр независимых секций, JSON с ключом на секцию. Ключи — явные строковые константы, а не имена классов: релизная сборка минифицирует имена и молча потеряла бы все сохранения. - ISaveSection, SaveManager, IStorage + MemoryStorage - CocosStorage: единственный файл модуля, знающий про движок; при отказе хранилища уходит в память, а не роняет игру - StatsSection: сыграно/побед/поражений/ничьих, текущая и лучшая серия - терпимая загрузка: битый JSON, чужая версия, сломанная секция или отвалившееся хранилище не ломают остальное - проверка чистоты расширена на modules/ со списком адаптеров Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,28 +1,59 @@
|
||||
// Guards the one architectural rule that the type system cannot express:
|
||||
// assets/scripts/core must never import the Cocos engine. See docs/TECHNICAL.md.
|
||||
// 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 } from 'node:path';
|
||||
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 ROOT = 'assets/scripts/core';
|
||||
const FORBIDDEN = /from\s+['"](cc|cc\/env|db:\/\/)/;
|
||||
const offenders = [];
|
||||
|
||||
function walk(dir) {
|
||||
function walk(dir, onFile) {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const p = join(dir, entry);
|
||||
if (statSync(p).isDirectory()) walk(p);
|
||||
else if (p.endsWith('.ts')) {
|
||||
const src = readFileSync(p, 'utf8');
|
||||
src.split('\n').forEach((line, i) => {
|
||||
if (FORBIDDEN.test(line)) offenders.push(`${p}:${i + 1} ${line.trim()}`);
|
||||
});
|
||||
}
|
||||
const path = join(dir, entry);
|
||||
if (statSync(path).isDirectory()) walk(path, onFile);
|
||||
else if (path.endsWith('.ts')) onFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
walk(ROOT);
|
||||
if (offenders.length) {
|
||||
console.error('core/ must not import the engine:\n' + offenders.join('\n'));
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user