mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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
This commit is contained in:
committed by
GitHub
parent
7490560c67
commit
d5a3740707
@@ -1683,10 +1683,7 @@ jobs:
|
||||
OPENCLAW_BUILD_PRIVATE_QA: "1"
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: |
|
||||
node scripts/build-all.mjs qaRuntime
|
||||
pnpm ui:build
|
||||
package_args=(
|
||||
--skip-build
|
||||
--output-dir .artifacts/qa-e2e/smoke-ci-package
|
||||
--output-name openclaw-current.tgz
|
||||
)
|
||||
@@ -1694,6 +1691,8 @@ jobs:
|
||||
package_args=(--allow-unreleased-changelog "${package_args[@]}")
|
||||
fi
|
||||
node scripts/package-openclaw-for-docker.mjs "${package_args[@]}"
|
||||
node scripts/build-all.mjs qaRuntime
|
||||
pnpm ui:build
|
||||
|
||||
- name: Run smoke profile part
|
||||
env:
|
||||
|
||||
@@ -76,6 +76,7 @@ COPY openclaw.mjs ./
|
||||
COPY ui/package.json ./ui/package.json
|
||||
COPY patches ./patches
|
||||
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs scripts/prepare-git-hooks.mjs ./scripts/
|
||||
COPY scripts/lib/guard-inventory-utils.mjs ./scripts/lib/guard-inventory-utils.mjs
|
||||
COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs
|
||||
|
||||
COPY --from=workspace-deps /out/packages/ ./packages/
|
||||
|
||||
@@ -347,6 +347,7 @@
|
||||
"scripts/lib/official-external-channel-catalog.json",
|
||||
"scripts/lib/official-external-plugin-catalog.json",
|
||||
"scripts/lib/official-external-provider-catalog.json",
|
||||
"scripts/lib/guard-inventory-utils.mjs",
|
||||
"scripts/lib/package-dist-imports.mjs",
|
||||
"scripts/lib/recommended-tool-installs.json",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
|
||||
@@ -20,6 +20,7 @@ COPY packages ./packages
|
||||
COPY extensions ./extensions
|
||||
COPY patches ./patches
|
||||
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/prepare-git-hooks.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
|
||||
COPY scripts/lib/guard-inventory-utils.mjs ./scripts/lib/guard-inventory-utils.mjs
|
||||
COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs
|
||||
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||
corepack enable \
|
||||
|
||||
@@ -6,8 +6,16 @@ export function resolveRepoSpecifier(
|
||||
specifier: unknown,
|
||||
importerFile: unknown,
|
||||
): string | null;
|
||||
/** Visit static and dynamic module specifiers in a parsed TypeScript source file. */
|
||||
export function visitModuleSpecifiers(ts: unknown, sourceFile: unknown, visit: unknown): void;
|
||||
/** Visit module specifiers, optionally including packaged-runtime dependencies. */
|
||||
export function visitModuleSpecifiers(
|
||||
ts: unknown,
|
||||
sourceFile: unknown,
|
||||
visit: unknown,
|
||||
options?: {
|
||||
includeCommonJs?: boolean;
|
||||
includeImportMetaUrl?: boolean;
|
||||
},
|
||||
): void;
|
||||
/** Diff expected and actual inventory entries using JSON identity. */
|
||||
export function diffInventoryEntries(
|
||||
expected: unknown,
|
||||
|
||||
@@ -22,7 +22,7 @@ export function resolveRepoSpecifier(repoRoot, specifier, importerFile) {
|
||||
}
|
||||
|
||||
/** Visit static and dynamic module specifiers in a parsed TypeScript source file. */
|
||||
export function visitModuleSpecifiers(ts, sourceFile, visit) {
|
||||
export function visitModuleSpecifiers(ts, sourceFile, visit, options = {}) {
|
||||
function walk(node) {
|
||||
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
||||
visit({
|
||||
@@ -54,6 +54,39 @@ export function visitModuleSpecifiers(ts, sourceFile, visit) {
|
||||
specifier: node.arguments[0].text,
|
||||
specifierNode: node.arguments[0],
|
||||
});
|
||||
} else if (
|
||||
options.includeCommonJs &&
|
||||
ts.isCallExpression(node) &&
|
||||
ts.isIdentifier(node.expression) &&
|
||||
node.expression.text === "require" &&
|
||||
node.arguments.length === 1 &&
|
||||
ts.isStringLiteralLike(node.arguments[0])
|
||||
) {
|
||||
visit({
|
||||
kind: "commonjs-require",
|
||||
node,
|
||||
specifier: node.arguments[0].text,
|
||||
specifierNode: node.arguments[0],
|
||||
});
|
||||
} else if (
|
||||
options.includeImportMetaUrl &&
|
||||
ts.isNewExpression(node) &&
|
||||
ts.isIdentifier(node.expression) &&
|
||||
node.expression.text === "URL" &&
|
||||
node.arguments?.length >= 2 &&
|
||||
ts.isStringLiteralLike(node.arguments[0]) &&
|
||||
ts.isPropertyAccessExpression(node.arguments[1]) &&
|
||||
node.arguments[1].name.text === "url" &&
|
||||
ts.isMetaProperty(node.arguments[1].expression) &&
|
||||
node.arguments[1].expression.keywordToken === ts.SyntaxKind.ImportKeyword &&
|
||||
node.arguments[1].expression.name.text === "meta"
|
||||
) {
|
||||
visit({
|
||||
kind: "import-meta-url",
|
||||
node,
|
||||
specifier: node.arguments[0].text,
|
||||
specifierNode: node.arguments[0],
|
||||
});
|
||||
}
|
||||
|
||||
ts.forEachChild(node, walk);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// 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;
|
||||
|
||||
@@ -26,108 +28,30 @@ function resolveDistImportPath(importerPath, specifier) {
|
||||
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 isRequireSpecifierContext(source, index) {
|
||||
const prefix = source.slice(Math.max(0, index - 32), index);
|
||||
return /\brequire\s*\(\s*$/u.test(prefix);
|
||||
}
|
||||
|
||||
function isImportMetaUrlContext(source, quoteStart, quoteEnd) {
|
||||
const prefix = source.slice(Math.max(0, quoteStart - 32), quoteStart);
|
||||
if (!/\bnew\s+URL\s*\(\s*$/u.test(prefix)) {
|
||||
return false;
|
||||
}
|
||||
const suffix = source.slice(quoteEnd + 1, quoteEnd + 96);
|
||||
return /^\s*,\s*import\.meta\.url\s*,?\s*\)/u.test(suffix);
|
||||
}
|
||||
|
||||
function collectImportSpecifiers(source) {
|
||||
function collectImportSpecifiers(source, importerPath) {
|
||||
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;
|
||||
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);
|
||||
}
|
||||
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(".")) {
|
||||
const isDistDependency =
|
||||
isImportSpecifierContext(source, index) ||
|
||||
isRequireSpecifierContext(source, index) ||
|
||||
(isImportMetaUrlContext(source, index, cursor) && hasJavaScriptFileExtension(value));
|
||||
if (isDistDependency) {
|
||||
specifiers.push(value);
|
||||
}
|
||||
}
|
||||
index = cursor;
|
||||
}
|
||||
},
|
||||
{ includeCommonJs: true, includeImportMetaUrl: true },
|
||||
);
|
||||
return specifiers;
|
||||
}
|
||||
|
||||
@@ -157,7 +81,7 @@ export function collectPackageDistImports(params) {
|
||||
continue;
|
||||
}
|
||||
const source = params.readText(importerPath);
|
||||
for (const specifier of collectImportSpecifiers(source)) {
|
||||
for (const specifier of collectImportSpecifiers(source, importerPath)) {
|
||||
const importedPath = resolveDistImportPath(importerPath, specifier);
|
||||
if (!importedPath) {
|
||||
continue;
|
||||
|
||||
@@ -30,6 +30,8 @@ const PACKAGE_BUILD_PLUGIN_SELECTION_ENV_NAMES = [
|
||||
"OPENCLAW_EXTENSIONS",
|
||||
"OPENCLAW_DOCKER_BUILD_EXTENSIONS",
|
||||
DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV,
|
||||
// Public package builds must not inherit a smoke lane's private QA entrypoints.
|
||||
"OPENCLAW_BUILD_PRIVATE_QA",
|
||||
];
|
||||
const SIGNAL_EXIT_CODES = {
|
||||
SIGHUP: 129,
|
||||
|
||||
@@ -108,6 +108,7 @@ const requiredPathGroups = [
|
||||
"scripts/lib/official-external-plugin-catalog.json",
|
||||
"scripts/lib/official-external-provider-catalog.json",
|
||||
"scripts/lib/recommended-tool-installs.json",
|
||||
"scripts/lib/guard-inventory-utils.mjs",
|
||||
"scripts/lib/package-dist-imports.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"dist/agents/compaction-planning.worker.js",
|
||||
|
||||
@@ -164,6 +164,9 @@ describe("Dockerfile", () => {
|
||||
const installIndex = dockerfile.indexOf("pnpm install --frozen-lockfile");
|
||||
const postinstallIndex = dockerfile.indexOf("COPY scripts/postinstall-bundled-plugins.mjs");
|
||||
const prepareIndex = dockerfile.indexOf("scripts/prepare-git-hooks.mjs");
|
||||
const importGrammarIndex = dockerfile.indexOf(
|
||||
"COPY scripts/lib/guard-inventory-utils.mjs ./scripts/lib/guard-inventory-utils.mjs",
|
||||
);
|
||||
const distImportHelperIndex = dockerfile.indexOf(
|
||||
"COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs",
|
||||
);
|
||||
@@ -176,6 +179,7 @@ describe("Dockerfile", () => {
|
||||
|
||||
expect(postinstallIndex).toBeGreaterThan(-1);
|
||||
expect(prepareIndex).toBeGreaterThan(-1);
|
||||
expect(importGrammarIndex).toBeGreaterThan(-1);
|
||||
expect(distImportHelperIndex).toBeGreaterThan(-1);
|
||||
expect(packageManifestIndex).toBeGreaterThan(-1);
|
||||
expect(extensionManifestIndex).toBeGreaterThan(-1);
|
||||
@@ -190,6 +194,7 @@ describe("Dockerfile", () => {
|
||||
);
|
||||
expect(postinstallIndex).toBeLessThan(installIndex);
|
||||
expect(prepareIndex).toBeLessThan(installIndex);
|
||||
expect(importGrammarIndex).toBeLessThan(installIndex);
|
||||
expect(distImportHelperIndex).toBeLessThan(installIndex);
|
||||
expect(packageManifestIndex).toBeLessThan(installIndex);
|
||||
expect(extensionManifestIndex).toBeLessThan(installIndex);
|
||||
|
||||
@@ -347,6 +347,7 @@ describe("package-openclaw-for-docker", () => {
|
||||
packageExtensions: string | undefined;
|
||||
dockerBuildExtensions: string | undefined;
|
||||
internalDockerBuildPluginIds: string | undefined;
|
||||
privateQa: string | undefined;
|
||||
skipDts: string | undefined;
|
||||
timeoutMs: number | undefined;
|
||||
}> = [];
|
||||
@@ -355,11 +356,13 @@ describe("package-openclaw-for-docker", () => {
|
||||
const previousPackageExtensions = process.env.OPENCLAW_EXTENSIONS;
|
||||
const previousDockerBuildExtensions = process.env.OPENCLAW_DOCKER_BUILD_EXTENSIONS;
|
||||
const previousInternalPluginIds = process.env[DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV];
|
||||
const previousPrivateQa = process.env.OPENCLAW_BUILD_PRIVATE_QA;
|
||||
process.env.OPENCLAW_DOCKER_PACKAGE_BUILD_TIMEOUT_MS = "1234";
|
||||
process.env.OPENCLAW_RUN_NODE_SKIP_DTS_BUILD = "1";
|
||||
process.env.OPENCLAW_EXTENSIONS = "clickclack";
|
||||
process.env.OPENCLAW_DOCKER_BUILD_EXTENSIONS = "slack";
|
||||
process.env[DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV] = "msteams";
|
||||
process.env.OPENCLAW_BUILD_PRIVATE_QA = "1";
|
||||
|
||||
try {
|
||||
await buildPackageArtifacts("/repo", {
|
||||
@@ -377,6 +380,7 @@ describe("package-openclaw-for-docker", () => {
|
||||
packageExtensions: options.env?.OPENCLAW_EXTENSIONS,
|
||||
dockerBuildExtensions: options.env?.OPENCLAW_DOCKER_BUILD_EXTENSIONS,
|
||||
internalDockerBuildPluginIds: options.env?.[DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV],
|
||||
privateQa: options.env?.OPENCLAW_BUILD_PRIVATE_QA,
|
||||
skipDts: options.env?.OPENCLAW_RUN_NODE_SKIP_DTS_BUILD,
|
||||
timeoutMs: options.timeoutMs,
|
||||
});
|
||||
@@ -397,6 +401,7 @@ describe("package-openclaw-for-docker", () => {
|
||||
["OPENCLAW_EXTENSIONS", previousPackageExtensions],
|
||||
["OPENCLAW_DOCKER_BUILD_EXTENSIONS", previousDockerBuildExtensions],
|
||||
[DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV, previousInternalPluginIds],
|
||||
["OPENCLAW_BUILD_PRIVATE_QA", previousPrivateQa],
|
||||
] as const) {
|
||||
if (previousValue === undefined) {
|
||||
delete process.env[envName];
|
||||
@@ -415,6 +420,7 @@ describe("package-openclaw-for-docker", () => {
|
||||
internalDockerBuildPluginIds: undefined,
|
||||
noPnpm: "1",
|
||||
packageExtensions: undefined,
|
||||
privateQa: undefined,
|
||||
skipDts: "0",
|
||||
timeoutMs: 1234,
|
||||
},
|
||||
|
||||
@@ -716,6 +716,7 @@ describe("collectMissingPackPaths", () => {
|
||||
"scripts/lib/official-external-plugin-catalog.json",
|
||||
"scripts/lib/official-external-provider-catalog.json",
|
||||
"scripts/lib/recommended-tool-installs.json",
|
||||
"scripts/lib/guard-inventory-utils.mjs",
|
||||
"scripts/lib/package-dist-imports.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"dist/agents/compaction-planning.worker.js",
|
||||
@@ -756,6 +757,7 @@ describe("collectMissingPackPaths", () => {
|
||||
"scripts/lib/official-external-plugin-catalog.json",
|
||||
"scripts/lib/official-external-provider-catalog.json",
|
||||
"scripts/lib/recommended-tool-installs.json",
|
||||
"scripts/lib/guard-inventory-utils.mjs",
|
||||
"scripts/lib/package-dist-imports.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"dist/agents/compaction-planning.worker.js",
|
||||
|
||||
@@ -357,6 +357,66 @@ describe("check-openclaw-package-tarball", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects leaked private QA Docker chunks that import an omitted QA runtime", () => {
|
||||
withTarball(
|
||||
["dist/docker-runtime-BVdgRgxA.js"],
|
||||
{
|
||||
"dist/docker-runtime-BVdgRgxA.js":
|
||||
'import { createQaDockerRuntime } from "./qa-runtime-Bi1S3plf.js";\n' +
|
||||
"export { createQaDockerRuntime };\n",
|
||||
},
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain(
|
||||
"dist/docker-runtime-BVdgRgxA.js imports missing dist/qa-runtime-Bi1S3plf.js",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "named imports",
|
||||
source: 'import { value } from "./missing.js";\n',
|
||||
},
|
||||
{
|
||||
name: "multiline named imports",
|
||||
source: 'import {\n value,\n} from "./missing.js";\n',
|
||||
},
|
||||
{
|
||||
name: "named re-exports",
|
||||
source: 'export { value } from "./missing.js";\n',
|
||||
},
|
||||
])("rejects missing packaged chunks in $name", ({ source }) => {
|
||||
withTarball(
|
||||
["dist/index.js"],
|
||||
{ "dist/index.js": source },
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain("dist/index.js imports missing dist/missing.js");
|
||||
},
|
||||
"2026.4.27",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not reject import-like text inside packaged template literals", () => {
|
||||
withTarball(
|
||||
["dist/index.js"],
|
||||
{ "dist/index.js": 'const example = `\nimport "./phantom.js"\n`;\n' },
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stdout).toContain("OpenClaw package tarball integrity passed.");
|
||||
},
|
||||
"2026.4.27",
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts dist files whose relative chunks are present", () => {
|
||||
withTarball(
|
||||
["dist/cli/run-main.js", "dist/memory-state-current.js"],
|
||||
@@ -393,6 +453,23 @@ describe("check-openclaw-package-tarball", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects named imported chunks omitted from the postinstall inventory", () => {
|
||||
withTarball(
|
||||
["dist/index.js"],
|
||||
{
|
||||
"dist/index.js": 'import { value } from "./chunk.js";\nexport { value };\n',
|
||||
"dist/chunk.js": "export const value = 42;\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/chunk.js");
|
||||
},
|
||||
"2026.4.27",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects CommonJS require chunks omitted from the postinstall inventory", () => {
|
||||
withTarball(
|
||||
["dist/index.cjs"],
|
||||
@@ -477,6 +554,27 @@ describe("check-openclaw-package-tarball", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"../../openclaw.mjs",
|
||||
"../../scripts/run-node.mjs",
|
||||
"../../dist/entry.js",
|
||||
"../../dist/entry.mjs",
|
||||
])("allows import.meta.url JavaScript probes outside packaged dist (%s)", (specifier) => {
|
||||
withTarball(
|
||||
["dist/index.js"],
|
||||
{
|
||||
"dist/index.js": `const candidate = new URL(${JSON.stringify(specifier)}, import.meta.url);\n`,
|
||||
},
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stdout).toContain("OpenClaw package tarball integrity passed.");
|
||||
},
|
||||
"2026.4.27",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows import.meta.url source helper probes", () => {
|
||||
withTarball(
|
||||
["dist/index.js"],
|
||||
|
||||
@@ -47,6 +47,69 @@ describe("check-package-dist-imports", () => {
|
||||
expect(result.stdout).toContain("OpenClaw package dist import closure passed.");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "named imports",
|
||||
source: 'import { value } from "./missing.js";\n',
|
||||
},
|
||||
{
|
||||
name: "multiline named imports",
|
||||
source: 'import {\n value,\n} from "./missing.js";\n',
|
||||
},
|
||||
{
|
||||
name: "named re-exports",
|
||||
source: 'export { value } from "./missing.js";\n',
|
||||
},
|
||||
{
|
||||
name: "multiline named re-exports",
|
||||
source: 'export {\n value,\n} from "./missing.js";\n',
|
||||
},
|
||||
])("rejects missing chunks in $name", ({ source }) => {
|
||||
const root = makeTempDir(tempDirs, "openclaw-package-dist-imports-");
|
||||
mkdirSync(join(root, "dist"), { recursive: true });
|
||||
writeFileSync(join(root, "dist", "index.js"), source, "utf8");
|
||||
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, root], { encoding: "utf8" });
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain("dist/index.js imports missing dist/missing.js");
|
||||
});
|
||||
|
||||
it("ignores import-like text inside multiline template literals", () => {
|
||||
const root = makeTempDir(tempDirs, "openclaw-package-dist-imports-");
|
||||
mkdirSync(join(root, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(root, "dist", "index.js"),
|
||||
'const example = `\nimport "./phantom.js"\n`;\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, root], { encoding: "utf8" });
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stdout).toContain("OpenClaw package dist import closure passed.");
|
||||
});
|
||||
|
||||
it.each([
|
||||
"../../openclaw.mjs",
|
||||
"../../scripts/run-node.mjs",
|
||||
"../../dist/entry.js",
|
||||
"../../dist/entry.mjs",
|
||||
])("ignores import.meta.url probes outside packaged dist (%s)", (specifier) => {
|
||||
const root = makeTempDir(tempDirs, "openclaw-package-dist-imports-");
|
||||
mkdirSync(join(root, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(root, "dist", "index.js"),
|
||||
`const candidate = new URL(${JSON.stringify(specifier)}, import.meta.url);\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, root], { encoding: "utf8" });
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stdout).toContain("OpenClaw package dist import closure passed.");
|
||||
});
|
||||
|
||||
it("rejects missing CommonJS require chunks", () => {
|
||||
const root = makeTempDir(tempDirs, "openclaw-package-dist-imports-");
|
||||
mkdirSync(join(root, "dist"), { recursive: true });
|
||||
|
||||
@@ -5066,10 +5066,13 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
expect(smokeBuildStep.run).toContain("node scripts/build-all.mjs qaRuntime");
|
||||
expect(smokeBuildStep.run).toContain("pnpm ui:build");
|
||||
expect(smokeBuildStep.env.OPENCLAW_BUILD_PRIVATE_QA).toBe("1");
|
||||
expect(smokeBuildStep.run).toContain("--skip-build");
|
||||
expect(smokeBuildStep.run).not.toContain("--skip-build");
|
||||
expect(smokeBuildStep.run).toContain("--allow-unreleased-changelog");
|
||||
expect(smokeBuildStep.run).toContain("grep -Fq");
|
||||
expect(smokeBuildStep.run).toContain('"${package_args[@]}"');
|
||||
expect(smokeBuildStep.run.indexOf("node scripts/package-openclaw-for-docker.mjs")).toBeLessThan(
|
||||
smokeBuildStep.run.indexOf("node scripts/build-all.mjs qaRuntime"),
|
||||
);
|
||||
expect(workflow.jobs["qa-smoke-ci-artifacts"]).toBeUndefined();
|
||||
expect(workflow.jobs["qa-smoke-ci"]).toBeUndefined();
|
||||
expect(smokeProfileJob.needs).toEqual(["preflight"]);
|
||||
|
||||
@@ -114,6 +114,16 @@ describe("bundled plugin postinstall", () => {
|
||||
fileURLToPath(new URL("../../scripts/lib/package-dist-imports.mjs", import.meta.url)),
|
||||
path.join(scriptRoot, "lib", "package-dist-imports.mjs"),
|
||||
);
|
||||
await fs.copyFile(
|
||||
fileURLToPath(new URL("../../scripts/lib/guard-inventory-utils.mjs", import.meta.url)),
|
||||
path.join(scriptRoot, "lib", "guard-inventory-utils.mjs"),
|
||||
);
|
||||
await fs.mkdir(path.join(packageRoot, "node_modules"), { recursive: true });
|
||||
await fs.symlink(
|
||||
fileURLToPath(new URL("../../node_modules/typescript", import.meta.url)),
|
||||
path.join(packageRoot, "node_modules", "typescript"),
|
||||
process.platform === "win32" ? "junction" : "dir",
|
||||
);
|
||||
for (const sentinel of sentinels) {
|
||||
await fs.mkdir(path.dirname(sentinel), { recursive: true });
|
||||
await fs.writeFile(sentinel, "owned by another Node application\n");
|
||||
@@ -691,6 +701,40 @@ describe("bundled plugin postinstall", () => {
|
||||
await expectPathMissing(staleFile);
|
||||
});
|
||||
|
||||
it("keeps named imported chunks without preserving template-literal pseudoimports", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-packaged-install-named-import-");
|
||||
const entryFile = path.join(packageRoot, "dist", "cli", "run-main.js");
|
||||
const importedChunk = path.join(packageRoot, "dist", "memory-state-current.js");
|
||||
const phantomChunk = path.join(packageRoot, "dist", "memory-state-phantom.js");
|
||||
await fs.mkdir(path.dirname(entryFile), { recursive: true });
|
||||
await fs.writeFile(
|
||||
entryFile,
|
||||
[
|
||||
"import {",
|
||||
" value,",
|
||||
'} from "../memory-state-current.js";',
|
||||
"const example = `",
|
||||
'import "../memory-state-phantom.js"',
|
||||
"`;",
|
||||
"export { value, example };",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
await writePackageDistInventory(packageRoot);
|
||||
await fs.writeFile(importedChunk, "export const value = 42;\n");
|
||||
await fs.writeFile(phantomChunk, "export const stale = true;\n");
|
||||
|
||||
expect(
|
||||
pruneInstalledPackageDist({
|
||||
packageRoot,
|
||||
log: { log: vi.fn(), warn: vi.fn() },
|
||||
}),
|
||||
).toEqual(["dist/memory-state-phantom.js"]);
|
||||
|
||||
await expectPathExists(importedChunk);
|
||||
await expectPathMissing(phantomChunk);
|
||||
});
|
||||
|
||||
it("does not abort dist pruning when a listed chunk disappears before import expansion", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-packaged-install-missing-chunk-");
|
||||
const entryFile = path.join(packageRoot, "dist", "control-ui", "assets", "instances.js");
|
||||
|
||||
Reference in New Issue
Block a user