fix(platform): serialize runtime session transitions

This commit is contained in:
2026-09-05 06:17:04 +08:00
parent 1c124cf216
commit 7e5a950cc7
13 changed files with 1297 additions and 157 deletions
@@ -1,4 +1,4 @@
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
import { dirname, isAbsolute, relative, resolve } from 'node:path';
import ts from 'typescript';
@@ -38,6 +38,137 @@ export function scanImportBoundaries(options) {
return violations;
}
/**
* Finds the first dependency path from one modern entry into a forbidden
* compatibility file. Reachable non-static dynamic imports fail closed.
*/
export function findCompatibilityDependency(options) {
const entry = canonicalPath(options.entry);
const traversalRoot = canonicalPath(options.traversalRoot);
const forbiddenFiles = new Set(options.forbiddenFiles.map((file) => pathKey(canonicalPath(file))));
const compilerOptions = readCompilerOptions(options.tsconfigPath);
const moduleCache = ts.createModuleResolutionCache(
options.tsconfigPath ? dirname(resolve(options.tsconfigPath)) : traversalRoot,
(file) => pathKey(canonicalPath(file)),
compilerOptions,
);
const visited = new Set();
const visit = (file, path) => {
const canonicalFile = canonicalPath(file);
const key = pathKey(canonicalFile);
const nextPath = [...path, canonicalFile];
if (forbiddenFiles.has(key)) {
return { kind: 'forbidden', path: nextPath, file: canonicalFile };
}
if (visited.has(key) || !isInsideOrEqual(canonicalFile, traversalRoot)) return null;
visited.add(key);
const source = readFileSync(canonicalFile, 'utf8');
for (const edge of extractDependencyEdges(source, canonicalFile)) {
if (edge.kind === 'dynamic') {
return { kind: 'dynamic', path: nextPath, file: canonicalFile };
}
const imported = resolveDependency(edge.specifier, canonicalFile, compilerOptions, moduleCache);
if (imported === null) continue;
const found = visit(imported, nextPath);
if (found !== null) return found;
}
return null;
};
return visit(entry, []);
}
function readCompilerOptions(tsconfigPath) {
if (!tsconfigPath) {
return {
allowImportingTsExtensions: true,
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.Bundler,
target: ts.ScriptTarget.ES2020,
};
}
const configFile = resolve(tsconfigPath);
const loaded = ts.readConfigFile(configFile, ts.sys.readFile);
if (loaded.error) throw new Error(formatTsDiagnostics([loaded.error]));
const parsed = ts.parseJsonConfigFileContent(
loaded.config,
ts.sys,
dirname(configFile),
undefined,
configFile,
);
if (parsed.errors.length > 0) throw new Error(formatTsDiagnostics(parsed.errors));
return parsed.options;
}
function formatTsDiagnostics(diagnostics) {
return ts.formatDiagnostics(diagnostics, {
getCanonicalFileName: (file) => file,
getCurrentDirectory: () => process.cwd(),
getNewLine: () => '\n',
});
}
function extractDependencyEdges(source, file) {
const edges = [];
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.isStringLiteralLike(node.moduleSpecifier)) {
edges.push({ kind: 'static', specifier: node.moduleSpecifier.text });
} else if (ts.isImportTypeNode(node)
&& ts.isLiteralTypeNode(node.argument)
&& ts.isStringLiteralLike(node.argument.literal)) {
edges.push({ kind: 'static', specifier: node.argument.literal.text });
} else if (ts.isCallExpression(node)
&& node.expression.kind === ts.SyntaxKind.ImportKeyword) {
const argument = node.arguments[0];
if (node.arguments.length === 1 && argument && ts.isStringLiteralLike(argument)) {
edges.push({ kind: 'static', specifier: argument.text });
} else {
edges.push({ kind: 'dynamic' });
}
}
ts.forEachChild(node, visit);
};
ts.forEachChild(sourceFile, visit);
return edges;
}
function resolveDependency(specifier, containingFile, compilerOptions, moduleCache) {
const result = ts.resolveModuleName(
specifier,
containingFile,
compilerOptions,
ts.sys,
moduleCache,
);
return result.resolvedModule ? canonicalPath(result.resolvedModule.resolvedFileName) : null;
}
function canonicalPath(file) {
const absolute = resolve(file);
try {
return realpathSync.native(absolute);
} catch (error) {
if (error && error.code === 'ENOENT') return absolute;
throw error;
}
}
function pathKey(file) {
return process.platform === 'win32' ? file.toLowerCase() : file;
}
function isInsideOrEqual(file, directory) {
if (pathKey(file) === pathKey(directory)) return true;
return isInside(file, directory);
}
function* typescriptFiles(directory) {
let entries;
try {