fix(sdk): harden import boundary scanner

This commit is contained in:
2026-09-05 00:49:26 +08:00
parent e1556d50f4
commit c0c6a7c800
2 changed files with 108 additions and 15 deletions
@@ -1,5 +1,6 @@
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, isAbsolute, relative, resolve } from 'node:path';
import ts from 'typescript';
const SDK_ALLOWED = /framework[\\/]sdk(?:[\\/]|$)/;
const FRAMEWORK_INTERNAL = /framework[\\/](?:net|protocol|platform|application|domain|presentation|ui|core|compat)(?:[\\/]|$)/;
@@ -18,7 +19,7 @@ export function scanImportBoundaries(options) {
for (const file of [...typescriptFiles(frameworkDir), ...typescriptFiles(gamesDir)]) {
const source = readFileSync(file, 'utf8');
for (const imported of extractImports(source)) {
for (const imported of extractImports(source, file)) {
const resolved = imported.specifier.startsWith('.')
? resolve(dirname(file), imported.specifier)
: null;
@@ -52,16 +53,35 @@ function* typescriptFiles(directory) {
}
}
function extractImports(source) {
function extractImports(source, file) {
const imports = [];
const staticImport = /\bimport\s+(?!\()(?:(?:type\s+)?([^;\n]+?)\s+from\s+)?(['"])([^'"\n]+)\2/g;
for (const match of source.matchAll(staticImport)) {
imports.push({ specifier: match[3], bindings: match[1] ?? '' });
}
const dynamicImport = /\bimport\s*\(\s*(['"])([^'"\n]+)\1\s*\)/g;
for (const match of source.matchAll(dynamicImport)) {
imports.push({ specifier: match[2], bindings: '' });
}
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
const visit = (node) => {
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node))
&& node.moduleSpecifier
&& ts.isStringLiteral(node.moduleSpecifier)) {
imports.push({
specifier: node.moduleSpecifier.text,
bindings: ts.isImportDeclaration(node) && node.importClause
? node.importClause.getText(sourceFile)
: '',
});
}
if (ts.isImportTypeNode(node)
&& ts.isLiteralTypeNode(node.argument)
&& ts.isStringLiteral(node.argument.literal)) {
imports.push({ specifier: node.argument.literal.text, bindings: '' });
}
if (ts.isCallExpression(node)
&& node.expression.kind === ts.SyntaxKind.ImportKeyword
&& node.arguments.length === 1
&& ts.isStringLiteral(node.arguments[0])) {
imports.push({ specifier: node.arguments[0].text, bindings: '' });
}
ts.forEachChild(node, visit);
};
ts.forEachChild(sourceFile, visit);
return imports;
}
@@ -73,11 +93,11 @@ function findViolation(context) {
const isGame = isInside(file, gamesDir);
const isFramework = isInside(file, frameworkDir);
if (isContract && specifier === 'cc') {
return violation(filePath, specifier, `sdk/contracts cannot import cc (${filePath})`);
if (isContract && !resolved) {
return violation(filePath, specifier, `sdk/contracts may import only sibling contracts; non-relative specifier ${specifier} is forbidden`);
}
if (isContract && resolved && dirname(resolved) !== dirname(file)) {
if (isContract && dirname(resolved) !== dirname(file)) {
return violation(filePath, specifier, `sdk/contracts may import only sibling contracts; sdk path resolved to ${resolvedPath}`);
}
@@ -85,8 +105,9 @@ function findViolation(context) {
return violation(filePath, specifier, `game code may import only framework/sdk; game import resolved to ${resolvedPath}`);
}
if (isGame && resolved && SDK_ALLOWED.test(resolvedPath) && importsMigrationOnlyName(bindings)) {
return violation(filePath, specifier, `game code cannot import ${migrationOnlyName(bindings)}; it is migration-only`);
if (importsMigrationOnlyName(bindings) && !isMigrationOnlyImporter(filePath, frameworkDir)) {
const importer = isGame ? 'game code' : 'production code';
return violation(filePath, specifier, `${importer} cannot import ${migrationOnlyName(bindings)}; it is migration-only`);
}
if (isFramework && resolved && isInside(resolved, gamesDir)) {
@@ -112,6 +133,14 @@ function isLegacyRuntimeImporter(filePath, frameworkDir) {
return filePath === `${normalizedFrameworkDir}/sdk/index.ts` || LEGACY_RUNTIME.test(filePath);
}
/**
* 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));
}