fix(release): verify package entrypoint imports

This commit is contained in:
Peter Steinberger
2026-04-28 14:06:46 +01:00
parent 727bff8133
commit 2e8f91c36e
5 changed files with 203 additions and 4 deletions

View File

@@ -262,6 +262,75 @@ export async function readPackageDistInventoryIfPresent(
}
}
const DIST_JS_IMPORT_SPECIFIER_PATTERN =
/\b(?:import|export)\s+(?:(?:[^'"()]*?\s+from\s+)|)["'](?<staticSpecifier>[^"']+)["']|\bimport\s*\(\s*["'](?<dynamicSpecifier>[^"']+)["']\s*\)/gu;
const PACKAGE_DIST_IMPORT_REFERENCE_ENTRYPOINTS = [
"dist/entry.js",
"dist/cli/run-main.js",
"dist/index.js",
"dist/index.mjs",
] as const;
function isRelativeModuleSpecifier(value: string): boolean {
return value.startsWith("./") || value.startsWith("../");
}
function normalizeModuleSpecifierTarget(value: string): string {
return value.split(/[?#]/u, 1)[0] ?? value;
}
function resolveDistImportTarget(importer: string, specifier: string, actualFiles: Set<string>) {
const normalizedSpecifier = normalizeModuleSpecifierTarget(specifier);
const base = normalizeRelativePath(
path.posix.normalize(path.posix.join(path.posix.dirname(importer), normalizedSpecifier)),
);
const candidates = [
base,
`${base}.js`,
`${base}.mjs`,
`${base}.cjs`,
`${base}/index.js`,
`${base}/index.mjs`,
`${base}/index.cjs`,
];
return candidates.find((candidate) => actualFiles.has(candidate)) ?? null;
}
export async function collectPackageDistImportReferenceErrors(
packageRoot: string,
): Promise<string[]> {
const actualFiles = new Set(await collectPackageDistInventory(packageRoot));
const jsFiles = PACKAGE_DIST_IMPORT_REFERENCE_ENTRYPOINTS.filter((relativePath) =>
actualFiles.has(relativePath),
);
const errors: string[] = [];
await Promise.all(
jsFiles.map(async (relativePath) => {
let source: string;
try {
source = await fs.readFile(path.join(packageRoot, relativePath), "utf8");
} catch {
errors.push(`unable to read packaged dist file ${relativePath}`);
return;
}
for (const match of source.matchAll(DIST_JS_IMPORT_SPECIFIER_PATTERN)) {
const specifier = match.groups?.staticSpecifier ?? match.groups?.dynamicSpecifier ?? "";
if (!isRelativeModuleSpecifier(specifier)) {
continue;
}
if (resolveDistImportTarget(relativePath, specifier, actualFiles)) {
continue;
}
errors.push(`missing packaged dist import target ${specifier} from ${relativePath}`);
}
}),
);
return errors.toSorted((left, right) => left.localeCompare(right));
}
export async function collectPackageDistInventoryErrors(packageRoot: string): Promise<string[]> {
const expectedFiles = await readPackageDistInventoryIfPresent(packageRoot);
if (expectedFiles === null) {

View File

@@ -457,6 +457,26 @@ describe("update global helpers", () => {
});
});
it("checks installed dist import references during global verify", async () => {
await withTempDir({ prefix: "openclaw-update-global-imports-" }, async (packageRoot) => {
await writeGlobalPackageJson(packageRoot, "2026.4.27");
const runMain = path.join(packageRoot, "dist", "cli", "run-main.js");
await fs.mkdir(path.dirname(runMain), { recursive: true });
await fs.writeFile(runMain, 'await import("../memory-state-CcqRgDZU.js");\n', "utf8");
await writePackageDistInventory(packageRoot);
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toContain(
"missing packaged dist import target ../memory-state-CcqRgDZU.js from dist/cli/run-main.js",
);
const chunk = path.join(packageRoot, "dist", "memory-state-CcqRgDZU.js");
await fs.writeFile(chunk, "export {};\n", "utf8");
await writePackageDistInventory(packageRoot);
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toEqual([]);
});
});
it("ignores bundled plugin install stages during installed dist verification", async () => {
await withTempDir({ prefix: "openclaw-update-global-plugin-stage-" }, async (packageRoot) => {
await writeGlobalPackageJson(packageRoot);

View File

@@ -7,6 +7,7 @@ import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { pathExists } from "../utils.js";
import {
collectPackageDistInventory,
collectPackageDistImportReferenceErrors,
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
readPackageDistInventoryIfPresent,
} from "./package-dist-inventory.js";
@@ -154,15 +155,17 @@ async function collectInstalledPackageDistErrors(params: {
missingMessage: (relativePath) => `missing packaged dist file ${relativePath}`,
unexpectedMessage: (relativePath) => `unexpected packaged dist file ${relativePath}`,
});
const importErrors = await collectPackageDistImportReferenceErrors(params.packageRoot);
const inventorySet = new Set(inventoryFiles);
const supplementalCriticalPaths = criticalPaths.filter(
(relativePath) => !inventorySet.has(relativePath),
);
if (supplementalCriticalPaths.length === 0) {
return inventoryErrors;
return [...inventoryErrors, ...importErrors];
}
return [
...inventoryErrors,
...importErrors,
...(await collectInstalledPathErrors({
packageRoot: params.packageRoot,
expectedFiles: supplementalCriticalPaths,