mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 16:24:05 +00:00
fix(ci): preserve imported dist chunks after install
This commit is contained in:
@@ -7,6 +7,7 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { LOCAL_BUILD_METADATA_DIST_PATHS } from "./lib/local-build-metadata-paths.mjs";
|
||||
import { expandPackageDistImportClosure } from "./lib/package-dist-imports.mjs";
|
||||
|
||||
function usage() {
|
||||
return "Usage: node scripts/check-openclaw-package-tarball.mjs <openclaw.tgz>";
|
||||
@@ -243,6 +244,8 @@ if (entrySet.has("dist/postinstall-inventory.json")) {
|
||||
if (!Array.isArray(inventory) || inventory.some((entry) => typeof entry !== "string")) {
|
||||
errors.push("invalid dist/postinstall-inventory.json");
|
||||
} else {
|
||||
const normalizedInventory = inventory.map((entry) => entry.replace(/\\/gu, "/"));
|
||||
const normalizedInventorySet = new Set(normalizedInventory);
|
||||
for (const inventoryEntry of inventory) {
|
||||
const normalizedEntry = inventoryEntry.replace(/\\/gu, "/");
|
||||
if (!entrySet.has(normalizedEntry)) {
|
||||
@@ -258,6 +261,16 @@ if (entrySet.has("dist/postinstall-inventory.json")) {
|
||||
errors.push(`inventory references missing tar entry ${normalizedEntry}`);
|
||||
}
|
||||
}
|
||||
const expandedInventory = expandPackageDistImportClosure({
|
||||
files: normalized,
|
||||
seedFiles: normalizedInventory,
|
||||
readText: readTarEntry,
|
||||
});
|
||||
for (const importedEntry of expandedInventory) {
|
||||
if (!normalizedInventorySet.has(importedEntry)) {
|
||||
errors.push(`inventory omits imported dist file ${importedEntry}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
|
||||
170
scripts/lib/package-dist-imports.mjs
Normal file
170
scripts/lib/package-dist-imports.mjs
Normal file
@@ -0,0 +1,170 @@
|
||||
import path from "node:path";
|
||||
|
||||
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 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 findStatementStart(source, index) {
|
||||
return (
|
||||
Math.max(
|
||||
source.lastIndexOf(";", index),
|
||||
source.lastIndexOf("{", index),
|
||||
source.lastIndexOf("}", index),
|
||||
source.lastIndexOf("\n", index),
|
||||
source.lastIndexOf("\r", index),
|
||||
) + 1
|
||||
);
|
||||
}
|
||||
|
||||
function isImportSpecifierContext(source, index) {
|
||||
const dynamicPrefix = source.slice(Math.max(0, index - 32), index);
|
||||
if (/\bimport\s*\(\s*$/u.test(dynamicPrefix)) {
|
||||
return true;
|
||||
}
|
||||
const statementPrefix = source.slice(findStatementStart(source, index), index).trimStart();
|
||||
return (
|
||||
/^(?:import|export)\b[\s\S]*\bfrom\s*$/u.test(statementPrefix) ||
|
||||
/^import\s*$/u.test(statementPrefix)
|
||||
);
|
||||
}
|
||||
|
||||
function collectImportSpecifiers(source) {
|
||||
const specifiers = [];
|
||||
let inBlockComment = false;
|
||||
let inLineComment = false;
|
||||
for (let index = 0; index < source.length; index += 1) {
|
||||
if (inBlockComment) {
|
||||
if (source[index] === "*" && source[index + 1] === "/") {
|
||||
inBlockComment = false;
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inLineComment) {
|
||||
if (source[index] === "\n" || source[index] === "\r") {
|
||||
inLineComment = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (source[index] === "/" && source[index + 1] === "*") {
|
||||
inBlockComment = true;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (source[index] === "/" && source[index + 1] === "/") {
|
||||
inLineComment = true;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const quote = source[index];
|
||||
if (quote !== '"' && quote !== "'") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cursor = index + 1;
|
||||
let value = "";
|
||||
while (cursor < source.length) {
|
||||
const char = source[cursor];
|
||||
if (char === "\\") {
|
||||
value += source.slice(cursor, cursor + 2);
|
||||
cursor += 2;
|
||||
continue;
|
||||
}
|
||||
if (char === quote) {
|
||||
break;
|
||||
}
|
||||
value += char;
|
||||
cursor += 1;
|
||||
}
|
||||
if (cursor >= source.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (value.startsWith(".")) {
|
||||
if (isImportSpecifierContext(source, index)) {
|
||||
specifiers.push(value);
|
||||
}
|
||||
}
|
||||
index = cursor;
|
||||
}
|
||||
return specifiers;
|
||||
}
|
||||
|
||||
export function collectPackageDistImportErrors(params) {
|
||||
const files = [...new Set(params.files.map(normalizePackagePath))];
|
||||
const fileSet = new Set(files);
|
||||
const errors = [];
|
||||
|
||||
for (const { importerPath, importedPath } of collectPackageDistImports({
|
||||
files,
|
||||
readText: params.readText,
|
||||
})) {
|
||||
if (!fileSet.has(importedPath)) {
|
||||
errors.push(`${importerPath} imports missing ${importedPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
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)) {
|
||||
const importedPath = resolveDistImportPath(importerPath, specifier);
|
||||
if (!importedPath) {
|
||||
continue;
|
||||
}
|
||||
imports.push({ importerPath, importedPath });
|
||||
}
|
||||
}
|
||||
|
||||
return 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));
|
||||
let changed = true;
|
||||
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const { importedPath } of collectPackageDistImports({
|
||||
files: [...expectedSet].filter((file) => fileSet.has(file)),
|
||||
readText: params.readText,
|
||||
})) {
|
||||
if (!fileSet.has(importedPath) || expectedSet.has(importedPath)) {
|
||||
continue;
|
||||
}
|
||||
expectedSet.add(importedPath);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return [...expectedSet].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "node:fs";
|
||||
import { basename, dirname, isAbsolute, join, relative } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { expandPackageDistImportClosure } from "./lib/package-dist-imports.mjs";
|
||||
import { resolveNpmRunner } from "./npm-runner.mjs";
|
||||
|
||||
export const BUNDLED_PLUGIN_INSTALL_TARGETS = [];
|
||||
@@ -292,6 +293,16 @@ export function pruneInstalledPackageDist(params = {}) {
|
||||
}
|
||||
}
|
||||
const installedFiles = listInstalledDistFiles(params);
|
||||
const readFile = params.readFileSync ?? readFileSync;
|
||||
expectedFiles = new Set(
|
||||
expandPackageDistImportClosure({
|
||||
files: installedFiles,
|
||||
seedFiles: [...expectedFiles],
|
||||
readText(relativePath) {
|
||||
return readFile(join(packageRoot, relativePath), "utf8");
|
||||
},
|
||||
}),
|
||||
);
|
||||
const removed = [];
|
||||
|
||||
for (const relativePath of installedFiles) {
|
||||
|
||||
@@ -122,6 +122,26 @@ describe("check-openclaw-package-tarball", () => {
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
},
|
||||
"2026.4.27",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects imported dist chunks omitted from the postinstall inventory", () => {
|
||||
withTarball(
|
||||
["dist/cli/run-main.js"],
|
||||
{
|
||||
"dist/cli/run-main.js": 'await import("../memory-state-current.js");\n',
|
||||
"dist/memory-state-current.js": "export {};\n",
|
||||
},
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain(
|
||||
"inventory omits imported dist file dist/memory-state-current.js",
|
||||
);
|
||||
},
|
||||
"2026.4.27",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -397,6 +397,28 @@ describe("bundled plugin postinstall", () => {
|
||||
await expect(fs.stat(staleFile)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("keeps imported dist chunks even when inventory is stale", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-packaged-install-import-");
|
||||
const entryFile = path.join(packageRoot, "dist", "cli", "run-main.js");
|
||||
const importedChunk = path.join(packageRoot, "dist", "memory-state-CcqRgDZU.js");
|
||||
const staleFile = path.join(packageRoot, "dist", "memory-state-old.js");
|
||||
await fs.mkdir(path.dirname(entryFile), { recursive: true });
|
||||
await fs.writeFile(entryFile, 'await import("../memory-state-CcqRgDZU.js");\n');
|
||||
await writePackageDistInventory(packageRoot);
|
||||
await fs.writeFile(importedChunk, "export {};\n");
|
||||
await fs.writeFile(staleFile, "export {};\n");
|
||||
|
||||
expect(
|
||||
pruneInstalledPackageDist({
|
||||
packageRoot,
|
||||
log: { log: vi.fn(), warn: vi.fn() },
|
||||
}),
|
||||
).toEqual(["dist/memory-state-old.js"]);
|
||||
|
||||
await expect(fs.stat(importedChunk)).resolves.toBeTruthy();
|
||||
await expect(fs.stat(staleFile)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("prunes stale private QA files without restoring compat sidecars", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-packaged-install-qa-compat-");
|
||||
const currentFile = path.join(packageRoot, "dist", "entry.js");
|
||||
|
||||
Reference in New Issue
Block a user