feat: integrate room framework and isolated subgame bundles
Complete room UI and protocol integration, move game definitions and resources behind bundle entries, publish authoritative version XML, and document single-game builds. Include all current resource changes and experiment artifacts.
This commit is contained in:
@@ -7,6 +7,8 @@ const FRAMEWORK_INTERNAL = /framework[\\/](?:net|protocol|platform|application|d
|
||||
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']);
|
||||
// Pre-existing, quarantined prototype entry points; modern code must not import them.
|
||||
const LEGACY_SCENE_SCRIPT = /scripts[\\/](?:LoginFlow|RoomEventProbe|RoomSceneStart)\.ts$/;
|
||||
|
||||
/**
|
||||
* Recursively scans TypeScript imports for framework/game layering violations.
|
||||
@@ -15,6 +17,8 @@ const MIGRATION_ONLY_NAMES = new Set(['GameContext', 'IGameModule']);
|
||||
export function scanImportBoundaries(options) {
|
||||
const frameworkDir = resolve(options.frameworkDir);
|
||||
const gamesDir = resolve(options.gamesDir);
|
||||
const publicDirs = (options.publicDirs ?? []).map(path=>resolve(path));
|
||||
const appDir = options.appDir ? resolve(options.appDir) : null;
|
||||
const compilerOptions = readCompilerOptions(options.tsconfigPath);
|
||||
const resolutionBase = options.tsconfigPath
|
||||
? dirname(resolve(options.tsconfigPath))
|
||||
@@ -26,7 +30,7 @@ export function scanImportBoundaries(options) {
|
||||
);
|
||||
const violations = [];
|
||||
|
||||
for (const file of [...typescriptFiles(frameworkDir), ...typescriptFiles(gamesDir)]) {
|
||||
for (const file of [...typescriptFiles(frameworkDir), ...typescriptFiles(gamesDir), ...publicDirs.flatMap(dir=>[...typescriptFiles(dir)]), ...(appDir?[...typescriptFiles(appDir)]:[])]) {
|
||||
const source = readFileSync(file, 'utf8');
|
||||
for (const imported of extractImports(source, file)) {
|
||||
const resolved = resolveDependency(
|
||||
@@ -44,6 +48,8 @@ export function scanImportBoundaries(options) {
|
||||
resolved,
|
||||
frameworkDir,
|
||||
gamesDir,
|
||||
publicDirs,
|
||||
appDir,
|
||||
});
|
||||
if (violation) violations.push(violation);
|
||||
}
|
||||
@@ -273,13 +279,19 @@ function literalCallSpecifier(node) {
|
||||
}
|
||||
|
||||
function findViolation(context) {
|
||||
const { file, specifier, bindings, resolved, frameworkDir, gamesDir } = context;
|
||||
const { file, specifier, bindings, resolved, frameworkDir, gamesDir, publicDirs, appDir } = context;
|
||||
if(appDir && isInside(file,appDir) && resolved && isInside(resolved,gamesDir))return violation(file.replaceAll('\\','/'),specifier,'app composition cannot import concrete games');
|
||||
const filePath = displayPath(file);
|
||||
const resolvedPath = resolved ? displayPath(resolved) : specifier;
|
||||
const isContract = CONTRACTS_PATH.test(filePath);
|
||||
const isSdk = SDK_ALLOWED.test(filePath);
|
||||
const isGame = isInside(file, gamesDir);
|
||||
const isFramework = isInside(file, frameworkDir);
|
||||
const isPublic = publicDirs.some(dir=>isInside(file,dir));
|
||||
|
||||
if ((isFramework || isPublic) && resolved && (isInside(resolved,gamesDir) || (appDir && isInside(resolved,appDir)))) {
|
||||
return violation(filePath,specifier,'public framework/platform code cannot import games or app composition');
|
||||
}
|
||||
|
||||
if (isContract && !resolved) {
|
||||
return violation(filePath, specifier, `sdk/contracts may import only sibling contracts; non-relative specifier ${specifier} is forbidden`);
|
||||
@@ -304,6 +316,11 @@ function findViolation(context) {
|
||||
return violation(filePath, specifier, `game code may import only framework/sdk; game import resolved to ${resolvedPath}`);
|
||||
}
|
||||
|
||||
if (isGame && resolved && isInside(resolved,gamesDir)
|
||||
&& relative(gamesDir,file).split(/[\\/]/)[0] !== relative(gamesDir,resolved).split(/[\\/]/)[0]) {
|
||||
return violation(filePath,specifier,'game code cannot import another game');
|
||||
}
|
||||
|
||||
if (importsMigrationOnlyName(bindings)) {
|
||||
const importer = isGame ? 'game code' : 'production code';
|
||||
return violation(filePath, specifier, `${importer} cannot import ${migrationOnlyName(bindings)}; it is migration-only`);
|
||||
@@ -313,7 +330,7 @@ function findViolation(context) {
|
||||
return violation(filePath, specifier, `framework code cannot import games; framework import resolved to ${resolvedPath}`);
|
||||
}
|
||||
|
||||
if (resolved && LEGACY_RUNTIME.test(resolvedPath) && !LEGACY_RUNTIME.test(filePath)) {
|
||||
if (resolved && (LEGACY_RUNTIME.test(resolvedPath)||LEGACY_SCENE_SCRIPT.test(resolvedPath)) && !LEGACY_RUNTIME.test(filePath)&&!LEGACY_SCENE_SCRIPT.test(filePath)) {
|
||||
return violation(filePath, specifier, `new production code cannot import quarantined legacy runtime ${resolvedPath}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user