fix(platform): serialize runtime session transitions
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user