Files
openclaw/scripts/lib/package-dist-imports.mjs
Peter Steinberger d5a3740707 fix(package): prevent installs from deleting required runtime modules (#113821)
* fix(package): preserve imported runtime chunks after install

* fix(package): isolate public build from private QA

* test(package): prove installed postinstall dependency graph
2026-07-25 14:05:41 -07:00

121 lines
3.9 KiB
JavaScript

// Scans packaged dist JavaScript for relative imports and missing closure entries.
import path from "node:path";
import ts from "typescript";
import { visitModuleSpecifiers } from "./guard-inventory-utils.mjs";
const JS_DIST_FILE_RE = /^dist\/.*\.(?:cjs|js|mjs)$/u;
function normalizePackagePath(value) {
return value.replace(/\\/gu, "/").replace(/^package\//u, "");
}
function stripSpecifierSuffix(value) {
return value.replace(/[?#].*$/u, "");
}
function hasJavaScriptFileExtension(value) {
return /\.(?:cjs|js|mjs)$/u.test(path.posix.basename(stripSpecifierSuffix(value)));
}
function resolveDistImportPath(importerPath, specifier) {
if (!specifier.startsWith(".")) {
return null;
}
const stripped = stripSpecifierSuffix(specifier);
if (!stripped) {
return null;
}
return path.posix.normalize(path.posix.join(path.posix.dirname(importerPath), stripped));
}
function collectImportSpecifiers(source, importerPath) {
const specifiers = [];
const sourceFile = ts.createSourceFile(
importerPath,
source,
ts.ScriptTarget.Latest,
false,
ts.ScriptKind.JS,
);
visitModuleSpecifiers(
ts,
sourceFile,
({ kind, specifier }) => {
if (
specifier.startsWith(".") &&
(kind !== "import-meta-url" ||
(hasJavaScriptFileExtension(specifier) &&
resolveDistImportPath(importerPath, specifier)?.startsWith("dist/")))
) {
specifiers.push(specifier);
}
},
{ includeCommonJs: true, includeImportMetaUrl: true },
);
return specifiers;
}
/** Collect missing-file errors for relative imports inside package dist files. */
export function collectPackageDistImportErrors(params) {
const files = [...new Set(params.files.map(normalizePackagePath))];
const fileSet = new Set(files);
const errors = [];
const imports = params.imports ?? collectPackageDistImports({ files, readText: params.readText });
for (const { importerPath, importedPath } of imports) {
if (!fileSet.has(importedPath)) {
errors.push(`${importerPath} imports missing ${importedPath}`);
}
}
return errors;
}
/** Collect relative dist import edges from package JavaScript files. */
export function collectPackageDistImports(params) {
const files = [...new Set(params.files.map(normalizePackagePath))];
const imports = [];
for (const importerPath of files.toSorted((left, right) => left.localeCompare(right))) {
if (!JS_DIST_FILE_RE.test(importerPath) || importerPath.includes("/node_modules/")) {
continue;
}
const source = params.readText(importerPath);
for (const specifier of collectImportSpecifiers(source, importerPath)) {
const importedPath = resolveDistImportPath(importerPath, specifier);
if (!importedPath) {
continue;
}
imports.push({ importerPath, importedPath });
}
}
return imports;
}
/** Expand seed dist files to include all reachable relative dist imports. */
export function expandPackageDistImportClosure(params) {
const files = [...new Set(params.files.map(normalizePackagePath))];
const fileSet = new Set(files);
const expectedSet = new Set(params.seedFiles.map(normalizePackagePath));
const imports = params.imports ?? collectPackageDistImports({ files, readText: params.readText });
const importsByImporter = new Map();
for (const { importerPath, importedPath } of imports) {
const importerImports = importsByImporter.get(importerPath) ?? [];
importerImports.push(importedPath);
importsByImporter.set(importerPath, importerImports);
}
const queue = [...expectedSet].filter((file) => fileSet.has(file));
for (const importerPath of queue) {
for (const importedPath of importsByImporter.get(importerPath) ?? []) {
if (fileSet.has(importedPath) && !expectedSet.has(importedPath)) {
expectedSet.add(importedPath);
queue.push(importedPath);
}
}
}
return [...expectedSet].toSorted((left, right) => left.localeCompare(right));
}