refactor(platform): complete decoupled vertical runtime
This commit is contained in:
@@ -7,12 +7,6 @@ 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']);
|
||||
const SDK_INDEX_MIGRATION_DEPENDENCIES = new Set([
|
||||
'core/events.ts',
|
||||
'core/reactive.ts',
|
||||
'platform/readonly.ts',
|
||||
'platform/stores/types.ts',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Recursively scans TypeScript imports for framework/game layering violations.
|
||||
@@ -208,7 +202,7 @@ function* typescriptFiles(directory) {
|
||||
|
||||
function extractImports(source, file) {
|
||||
const imports = [];
|
||||
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
||||
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
||||
|
||||
const visit = (node) => {
|
||||
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node))
|
||||
@@ -218,7 +212,7 @@ function extractImports(source, file) {
|
||||
specifier: node.moduleSpecifier.text,
|
||||
bindings: ts.isImportDeclaration(node) && node.importClause
|
||||
? node.importClause.getText(sourceFile)
|
||||
: '',
|
||||
: ts.isExportDeclaration(node) ? node.exportClause?.getText(sourceFile) ?? '' : '',
|
||||
});
|
||||
} else if (ts.isImportEqualsDeclaration(node)) {
|
||||
const specifier = externalImportEqualsSpecifier(node);
|
||||
@@ -228,10 +222,10 @@ function extractImports(source, file) {
|
||||
} else if (ts.isImportTypeNode(node)
|
||||
&& ts.isLiteralTypeNode(node.argument)
|
||||
&& ts.isStringLiteralLike(node.argument.literal)) {
|
||||
imports.push({ specifier: node.argument.literal.text, bindings: '' });
|
||||
imports.push({ specifier: node.argument.literal.text, bindings: node.qualifier?.getText(sourceFile) ?? '' });
|
||||
} else if (isModuleLoaderCall(node)) {
|
||||
const specifier = literalCallSpecifier(node);
|
||||
if (specifier !== null) imports.push({ specifier, bindings: '' });
|
||||
if (specifier !== null) imports.push({ specifier, bindings: moduleLoaderBindings(node, sourceFile) });
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
@@ -239,6 +233,22 @@ function extractImports(source, file) {
|
||||
return imports;
|
||||
}
|
||||
|
||||
function moduleLoaderBindings(node, sourceFile) {
|
||||
let expression = node;
|
||||
while (expression.parent && (
|
||||
ts.isAwaitExpression(expression.parent)
|
||||
|| ts.isParenthesizedExpression(expression.parent)
|
||||
|| ts.isAsExpression(expression.parent)
|
||||
)) expression = expression.parent;
|
||||
const parent = expression.parent;
|
||||
if (parent && ts.isVariableDeclaration(parent) && ts.isObjectBindingPattern(parent.name)) {
|
||||
return parent.name.getText(sourceFile);
|
||||
}
|
||||
if (parent && ts.isPropertyAccessExpression(parent)) return parent.name.getText(sourceFile);
|
||||
if (parent && ts.isElementAccessExpression(parent)) return parent.argumentExpression.getText(sourceFile);
|
||||
return '';
|
||||
}
|
||||
|
||||
function externalImportEqualsSpecifier(node) {
|
||||
if (!ts.isImportEqualsDeclaration(node)
|
||||
|| !ts.isExternalModuleReference(node.moduleReference)
|
||||
@@ -286,8 +296,7 @@ function findViolation(context) {
|
||||
if (isSdk
|
||||
&& resolved
|
||||
&& isInside(resolved, frameworkDir)
|
||||
&& !SDK_ALLOWED.test(resolvedPath)
|
||||
&& !isSdkIndexMigrationDependency(filePath, resolved, frameworkDir)) {
|
||||
&& !SDK_ALLOWED.test(resolvedPath)) {
|
||||
return violation(filePath, specifier, `sdk cannot import framework implementation ${resolvedPath}`);
|
||||
}
|
||||
|
||||
@@ -295,7 +304,7 @@ function findViolation(context) {
|
||||
return violation(filePath, specifier, `game code may import only framework/sdk; game import resolved to ${resolvedPath}`);
|
||||
}
|
||||
|
||||
if (importsMigrationOnlyName(bindings) && !isMigrationOnlyImporter(filePath, frameworkDir)) {
|
||||
if (importsMigrationOnlyName(bindings)) {
|
||||
const importer = isGame ? 'game code' : 'production code';
|
||||
return violation(filePath, specifier, `${importer} cannot import ${migrationOnlyName(bindings)}; it is migration-only`);
|
||||
}
|
||||
@@ -304,7 +313,7 @@ function findViolation(context) {
|
||||
return violation(filePath, specifier, `framework code cannot import games; framework import resolved to ${resolvedPath}`);
|
||||
}
|
||||
|
||||
if (resolved && LEGACY_RUNTIME.test(resolvedPath) && !isLegacyRuntimeImporter(filePath, frameworkDir)) {
|
||||
if (resolved && LEGACY_RUNTIME.test(resolvedPath) && !LEGACY_RUNTIME.test(filePath)) {
|
||||
return violation(filePath, specifier, `new production code cannot import quarantined legacy runtime ${resolvedPath}`);
|
||||
}
|
||||
|
||||
@@ -318,28 +327,10 @@ function isFrameworkReference(path, frameworkDir) {
|
||||
|| /(?:^|[\\/])framework(?:[\\/]|$)/.test(path);
|
||||
}
|
||||
|
||||
function isLegacyRuntimeImporter(filePath, frameworkDir) {
|
||||
const normalizedFrameworkDir = displayPath(frameworkDir);
|
||||
return filePath === `${normalizedFrameworkDir}/sdk/index.ts` || LEGACY_RUNTIME.test(filePath);
|
||||
}
|
||||
|
||||
function isSdkIndexMigrationDependency(filePath, resolved, frameworkDir) {
|
||||
if (filePath !== `${displayPath(frameworkDir)}/sdk/index.ts`) return false;
|
||||
return SDK_INDEX_MIGRATION_DEPENDENCIES.has(displayPath(relative(frameworkDir, resolved)));
|
||||
}
|
||||
|
||||
function isCocosSpecifier(specifier) {
|
||||
return specifier === 'cc' || specifier.startsWith('cc/');
|
||||
}
|
||||
|
||||
/**
|
||||
* ActiveGame is the retained compatibility owner of IGameModule until Task 11
|
||||
* removes both it and the migration-only declarations from sdk/index.ts.
|
||||
*/
|
||||
function isMigrationOnlyImporter(filePath, frameworkDir) {
|
||||
return filePath === `${displayPath(frameworkDir)}/protocol/active-game.ts`;
|
||||
}
|
||||
|
||||
function importsMigrationOnlyName(bindings) {
|
||||
return [...MIGRATION_ONLY_NAMES].some((name) => new RegExp(`\\b${name}\\b`).test(bindings));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user