build: keep runtime dep stamps out of dist

This commit is contained in:
Peter Steinberger
2026-04-23 06:50:00 +01:00
parent bb55e23c67
commit acb8fe986d
7 changed files with 136 additions and 73 deletions

View File

@@ -29,6 +29,8 @@
"dist/",
"!dist/**/*.map",
"!dist/plugin-sdk/.tsbuildinfo",
"!dist/extensions/*/.openclaw-runtime-deps-*/**",
"!dist/extensions/*/.openclaw-runtime-deps-stamp.json",
"!dist/extensions/node_modules/**",
"!dist/extensions/*/node_modules/**",
"!dist/extensions/qa-channel/**",

View File

@@ -457,6 +457,8 @@ export function collectForbiddenPackPaths(paths: Iterable<string>): string[] {
.filter(
(path) =>
forbiddenPrefixes.some((prefix) => path.startsWith(prefix)) ||
/(^|\/)\.openclaw-runtime-deps-[^/]+(\/|$)/u.test(path) ||
path.endsWith("/.openclaw-runtime-deps-stamp.json") ||
path.includes("node_modules/"),
)
.toSorted((left, right) => left.localeCompare(right));

View File

@@ -852,10 +852,19 @@ function runNpmInstall(params) {
throw new Error(output || "npm install failed");
}
function resolveRuntimeDepsStampPath(pluginDir) {
function resolveLegacyRuntimeDepsStampPath(pluginDir) {
return path.join(pluginDir, ".openclaw-runtime-deps-stamp.json");
}
function resolveRuntimeDepsStampPath(repoRoot, pluginId) {
return path.join(
repoRoot,
".artifacts",
"bundled-runtime-deps-stamps",
`${sanitizeTempPrefixSegment(pluginId)}.json`,
);
}
function createRuntimeDepsFingerprint(packageJson, pruneConfig, params = {}) {
const repoRoot = params.repoRoot;
const lockfilePath =
@@ -892,6 +901,17 @@ function readRuntimeDepsStamp(stampPath) {
}
}
function removeStaleRuntimeDepsTempDirs(pluginDir) {
if (!fs.existsSync(pluginDir)) {
return;
}
for (const entry of fs.readdirSync(pluginDir, { withFileTypes: true })) {
if (entry.name.startsWith(".openclaw-runtime-deps-")) {
removePathIfExists(path.join(pluginDir, entry.name));
}
}
}
function stageInstalledRootRuntimeDeps(params) {
const {
directDependencyPackageRoot = null,
@@ -900,6 +920,7 @@ function stageInstalledRootRuntimeDeps(params) {
pluginDir,
pruneConfig,
repoRoot,
stampPath,
} = params;
const dependencySpecs = {
...packageJson.dependencies,
@@ -931,7 +952,6 @@ function stageInstalledRootRuntimeDeps(params) {
}
const rootsToCopy = selectRuntimeDependencyRootsToCopy(resolution);
const nodeModulesDir = path.join(pluginDir, "node_modules");
const stampPath = resolveRuntimeDepsStampPath(pluginDir);
if (rootsToCopy.length === 0) {
assertPathIsNotSymlink(nodeModulesDir, "remove runtime deps");
removePathIfExists(nodeModulesDir);
@@ -1030,9 +1050,9 @@ function installPluginRuntimeDeps(params) {
pluginId,
pruneConfig,
repoRoot,
stampPath,
} = params;
const nodeModulesDir = path.join(pluginDir, "node_modules");
const stampPath = resolveRuntimeDepsStampPath(pluginDir);
const tempInstallDir = makePluginOwnedTempDir(pluginDir, "install");
const pinnedGroups = resolvePinnedRuntimeDependencyGroups(packageJson, {
directDependencyPackageRoot,
@@ -1088,7 +1108,10 @@ export function stageBundledPluginRuntimeDeps(params = {}) {
: null;
const packageJson = sanitizeBundledManifestForRuntimeInstall(pluginDir);
const nodeModulesDir = path.join(pluginDir, "node_modules");
const stampPath = resolveRuntimeDepsStampPath(pluginDir);
const stampPath = resolveRuntimeDepsStampPath(repoRoot, pluginId);
const legacyStampPath = resolveLegacyRuntimeDepsStampPath(pluginDir);
removePathIfExists(legacyStampPath);
removeStaleRuntimeDepsTempDirs(pluginDir);
if (!hasRuntimeDeps(packageJson) || !shouldStageRuntimeDeps(packageJson)) {
removePathIfExists(nodeModulesDir);
removePathIfExists(stampPath);
@@ -1115,6 +1138,7 @@ export function stageBundledPluginRuntimeDeps(params = {}) {
pluginDir,
pruneConfig,
repoRoot,
stampPath,
})
) {
continue;
@@ -1131,6 +1155,7 @@ export function stageBundledPluginRuntimeDeps(params = {}) {
pluginId,
pruneConfig,
repoRoot,
stampPath,
},
});
} catch (error) {

View File

@@ -70,6 +70,22 @@ describe("package dist inventory", () => {
"cli.d.ts",
);
const omittedQaRuntimeChunk = path.join(packageRoot, "dist", "qa-runtime-B9LDtssJ.js");
const omittedRuntimeDepsStamp = path.join(
packageRoot,
"dist",
"extensions",
"discord",
".openclaw-runtime-deps-stamp.json",
);
const omittedRuntimeDepsTempFile = path.join(
packageRoot,
"dist",
"extensions",
"discord",
".openclaw-runtime-deps-backup-node_modules-old",
"left-pad",
"index.js",
);
const omittedExtensionNodeModuleSymlink = path.join(
packageRoot,
"dist",
@@ -92,6 +108,8 @@ describe("package dist inventory", () => {
await fs.mkdir(path.dirname(packagedQaLabRuntime), { recursive: true });
await fs.mkdir(path.dirname(omittedQaMatrixChunk), { recursive: true });
await fs.mkdir(path.dirname(omittedQaLabTypes), { recursive: true });
await fs.mkdir(path.dirname(omittedRuntimeDepsStamp), { recursive: true });
await fs.mkdir(path.dirname(omittedRuntimeDepsTempFile), { recursive: true });
await fs.mkdir(path.dirname(omittedExtensionNodeModuleSymlink), { recursive: true });
await fs.mkdir(path.dirname(omittedExtensionRootAliasSymlink), { recursive: true });
await fs.mkdir(path.join(packageRoot, "dist", "plugin-sdk"), { recursive: true });
@@ -104,6 +122,8 @@ describe("package dist inventory", () => {
await fs.writeFile(omittedQaLabPluginSdk, "export {};\n", "utf8");
await fs.writeFile(omittedQaLabTypes, "export {};\n", "utf8");
await fs.writeFile(omittedQaRuntimeChunk, "export {};\n", "utf8");
await fs.writeFile(omittedRuntimeDepsStamp, "{}\n", "utf8");
await fs.writeFile(omittedRuntimeDepsTempFile, "module.exports = 1;\n", "utf8");
await fs.symlink(
path.join(packageRoot, "color-support.js"),
omittedExtensionNodeModuleSymlink,

View File

@@ -21,6 +21,7 @@ const OMITTED_PRIVATE_QA_DIST_PREFIXES = ["dist/qa-runtime-"];
const OMITTED_DIST_SUBTREE_PATTERNS = [
/^dist\/extensions\/node_modules(?:\/|$)/u,
/^dist\/extensions\/[^/]+\/node_modules(?:\/|$)/u,
/^dist\/extensions\/[^/]+\/\.openclaw-runtime-deps-[^/]+(?:\/|$)/u,
/^dist\/extensions\/qa-matrix(?:\/|$)/u,
new RegExp(`^dist/plugin-sdk/extensions/${LEGACY_QA_LAB_DIR}(?:/|$)`, "u"),
] as const;
@@ -36,6 +37,9 @@ function isPackagedDistPath(relativePath: string): boolean {
if (relativePath === PACKAGE_DIST_INVENTORY_RELATIVE_PATH) {
return false;
}
if (relativePath.endsWith("/.openclaw-runtime-deps-stamp.json")) {
return false;
}
if (relativePath.endsWith(".map")) {
return false;
}

View File

@@ -322,6 +322,19 @@ describe("collectForbiddenPackPaths", () => {
]);
});
it("blocks legacy runtime dependency stamps from npm pack output", () => {
expect(
collectForbiddenPackPaths([
"dist/index.js",
"dist/extensions/codex/.openclaw-runtime-deps-backup-node_modules-old/zod/index.js",
"dist/extensions/discord/.openclaw-runtime-deps-stamp.json",
]),
).toEqual([
"dist/extensions/codex/.openclaw-runtime-deps-backup-node_modules-old/zod/index.js",
"dist/extensions/discord/.openclaw-runtime-deps-stamp.json",
]);
});
it("blocks private qa channel, qa lab, and suite paths from npm pack output", () => {
expect(
collectForbiddenPackPaths([

View File

@@ -10,6 +10,11 @@ import { createScriptTestHarness } from "./test-helpers.js";
const { createTempDir } = createScriptTestHarness();
type RuntimeDepsStampParams = {
fingerprint: string;
stampPath: string;
};
describe("stageBundledPluginRuntimeDeps", () => {
function createBundledPluginFixture(params: {
packageJson: Record<string, unknown>;
@@ -27,6 +32,15 @@ describe("stageBundledPluginRuntimeDeps", () => {
return { pluginDir, repoRoot };
}
function writeRuntimeDepsStamp(stampPath: string, fingerprint: string) {
fs.mkdirSync(path.dirname(stampPath), { recursive: true });
fs.writeFileSync(stampPath, `${JSON.stringify({ fingerprint }, null, 2)}\n`, "utf8");
}
function runtimeDepsStampPath(repoRoot: string, pluginId = "fixture-plugin") {
return path.join(repoRoot, ".artifacts", "bundled-runtime-deps-stamps", `${pluginId}.json`);
}
it("pins fallback install specs to exact installed versions", () => {
const { repoRoot } = createBundledPluginFixture({
packageJson: {
@@ -142,13 +156,9 @@ describe("stageBundledPluginRuntimeDeps", () => {
let installCount = 0;
stageBundledPluginRuntimeDeps({
cwd: repoRoot,
installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => {
installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => {
installCount += 1;
fs.writeFileSync(
path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"),
`${JSON.stringify({ fingerprint }, null, 2)}\n`,
"utf8",
);
writeRuntimeDepsStamp(stampPath, fingerprint);
},
});
stageBundledPluginRuntimeDeps({
@@ -182,16 +192,12 @@ describe("stageBundledPluginRuntimeDeps", () => {
const stageOnce = () =>
stageBundledPluginRuntimeDeps({
cwd: repoRoot,
installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => {
installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => {
installCount += 1;
const nodeModulesDir = path.join(pluginDir, "node_modules");
fs.mkdirSync(nodeModulesDir, { recursive: true });
fs.writeFileSync(path.join(nodeModulesDir, "marker.txt"), `${installCount}\n`, "utf8");
fs.writeFileSync(
path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"),
`${JSON.stringify({ fingerprint }, null, 2)}\n`,
"utf8",
);
writeRuntimeDepsStamp(stampPath, fingerprint);
},
});
@@ -226,16 +232,12 @@ describe("stageBundledPluginRuntimeDeps", () => {
const stageOnce = () =>
stageBundledPluginRuntimeDeps({
cwd: repoRoot,
installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => {
installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => {
installCount += 1;
const nodeModulesDir = path.join(pluginDir, "node_modules");
fs.mkdirSync(nodeModulesDir, { recursive: true });
fs.writeFileSync(path.join(nodeModulesDir, "marker.txt"), `${installCount}\n`, "utf8");
fs.writeFileSync(
path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"),
`${JSON.stringify({ fingerprint }, null, 2)}\n`,
"utf8",
);
writeRuntimeDepsStamp(stampPath, fingerprint);
},
});
@@ -310,7 +312,7 @@ describe("stageBundledPluginRuntimeDeps", () => {
});
it("refuses to write a runtime deps stamp through a symlink", () => {
const { pluginDir, repoRoot } = createBundledPluginFixture({
const { repoRoot } = createBundledPluginFixture({
packageJson: {
name: "@openclaw/fixture-plugin",
version: "1.0.0",
@@ -320,8 +322,9 @@ describe("stageBundledPluginRuntimeDeps", () => {
});
const directDir = path.join(repoRoot, "node_modules", "direct");
const outsideStamp = path.join(repoRoot, "outside-stamp.json");
const stampPath = path.join(pluginDir, ".openclaw-runtime-deps-stamp.json");
const stampPath = runtimeDepsStampPath(repoRoot);
fs.mkdirSync(directDir, { recursive: true });
fs.mkdirSync(path.dirname(stampPath), { recursive: true });
fs.writeFileSync(
path.join(directDir, "package.json"),
'{ "name": "direct", "version": "1.0.0" }\n',
@@ -359,7 +362,33 @@ describe("stageBundledPluginRuntimeDeps", () => {
expect(
fs.readFileSync(path.join(pluginDir, "node_modules", "left-pad", "index.js"), "utf8"),
).toBe("module.exports = 1;\n");
expect(fs.existsSync(path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"))).toBe(true);
expect(fs.existsSync(path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"))).toBe(false);
expect(fs.existsSync(runtimeDepsStampPath(repoRoot))).toBe(true);
});
it("removes legacy runtime dependency stamps from dist", () => {
const { pluginDir, repoRoot } = createBundledPluginFixture({
packageJson: {
name: "@openclaw/fixture-plugin",
version: "1.0.0",
dependencies: { "left-pad": "1.3.0" },
openclaw: { bundle: { stageRuntimeDependencies: true } },
},
});
const rootDepDir = path.join(repoRoot, "node_modules", "left-pad");
const legacyStampPath = path.join(pluginDir, ".openclaw-runtime-deps-stamp.json");
fs.mkdirSync(rootDepDir, { recursive: true });
fs.writeFileSync(
path.join(rootDepDir, "package.json"),
'{ "name": "left-pad", "version": "1.3.0" }\n',
"utf8",
);
fs.writeFileSync(legacyStampPath, '{"legacy":true}\n', "utf8");
stageBundledPluginRuntimeDeps({ cwd: repoRoot });
expect(fs.existsSync(legacyStampPath)).toBe(false);
expect(fs.existsSync(runtimeDepsStampPath(repoRoot))).toBe(true);
});
it("skips missing optional runtime deps when copying the installed closure", () => {
@@ -617,16 +646,12 @@ describe("stageBundledPluginRuntimeDeps", () => {
let installCount = 0;
stageBundledPluginRuntimeDeps({
cwd: repoRoot,
installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => {
installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => {
installCount += 1;
const nodeModulesDir = path.join(pluginDir, "node_modules");
fs.mkdirSync(nodeModulesDir, { recursive: true });
fs.writeFileSync(path.join(nodeModulesDir, "marker.txt"), "installed\n", "utf8");
fs.writeFileSync(
path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"),
`${JSON.stringify({ fingerprint }, null, 2)}\n`,
"utf8",
);
writeRuntimeDepsStamp(stampPath, fingerprint);
},
});
@@ -698,16 +723,12 @@ describe("stageBundledPluginRuntimeDeps", () => {
let installCount = 0;
stageBundledPluginRuntimeDeps({
cwd: repoRoot,
installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => {
installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => {
installCount += 1;
const nodeModulesDir = path.join(pluginDir, "node_modules");
fs.mkdirSync(nodeModulesDir, { recursive: true });
fs.writeFileSync(path.join(nodeModulesDir, "marker.txt"), "installed\n", "utf8");
fs.writeFileSync(
path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"),
`${JSON.stringify({ fingerprint }, null, 2)}\n`,
"utf8",
);
writeRuntimeDepsStamp(stampPath, fingerprint);
},
});
@@ -743,16 +764,12 @@ describe("stageBundledPluginRuntimeDeps", () => {
let installCount = 0;
stageBundledPluginRuntimeDeps({
cwd: repoRoot,
installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => {
installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => {
installCount += 1;
const nodeModulesDir = path.join(pluginDir, "node_modules");
fs.mkdirSync(nodeModulesDir, { recursive: true });
fs.writeFileSync(path.join(nodeModulesDir, "marker.txt"), "installed\n", "utf8");
fs.writeFileSync(
path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"),
`${JSON.stringify({ fingerprint }, null, 2)}\n`,
"utf8",
);
writeRuntimeDepsStamp(stampPath, fingerprint);
},
});
@@ -788,7 +805,7 @@ describe("stageBundledPluginRuntimeDeps", () => {
let installCount = 0;
stageBundledPluginRuntimeDeps({
cwd: repoRoot,
installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => {
installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => {
installCount += 1;
const nodeModulesDir = path.join(pluginDir, "node_modules", "direct");
fs.mkdirSync(nodeModulesDir, { recursive: true });
@@ -802,11 +819,7 @@ describe("stageBundledPluginRuntimeDeps", () => {
"module.exports = 'installed';\n",
"utf8",
);
fs.writeFileSync(
path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"),
`${JSON.stringify({ fingerprint }, null, 2)}\n`,
"utf8",
);
writeRuntimeDepsStamp(stampPath, fingerprint);
},
});
@@ -976,7 +989,7 @@ describe("stageBundledPluginRuntimeDeps", () => {
let installCount = 0;
stageBundledPluginRuntimeDeps({
cwd: repoRoot,
installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => {
installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => {
installCount += 1;
const nodeModulesDir = path.join(pluginDir, "node_modules", "left-pad");
fs.mkdirSync(nodeModulesDir, { recursive: true });
@@ -990,11 +1003,7 @@ describe("stageBundledPluginRuntimeDeps", () => {
"module.exports = 'nested';\n",
"utf8",
);
fs.writeFileSync(
path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"),
`${JSON.stringify({ fingerprint }, null, 2)}\n`,
"utf8",
);
writeRuntimeDepsStamp(stampPath, fingerprint);
},
});
@@ -1024,7 +1033,7 @@ describe("stageBundledPluginRuntimeDeps", () => {
let installCount = 0;
stageBundledPluginRuntimeDeps({
cwd: repoRoot,
installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => {
installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => {
installCount += 1;
const nodeModulesDir = path.join(pluginDir, "node_modules", "tiny");
fs.mkdirSync(nodeModulesDir, { recursive: true });
@@ -1033,11 +1042,7 @@ describe("stageBundledPluginRuntimeDeps", () => {
'{ "name": "tiny", "version": "0.0.3" }\n',
"utf8",
);
fs.writeFileSync(
path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"),
`${JSON.stringify({ fingerprint }, null, 2)}\n`,
"utf8",
);
writeRuntimeDepsStamp(stampPath, fingerprint);
},
});
@@ -1064,7 +1069,7 @@ describe("stageBundledPluginRuntimeDeps", () => {
let installCount = 0;
stageBundledPluginRuntimeDeps({
cwd: repoRoot,
installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => {
installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => {
installCount += 1;
const nodeModulesDir = path.join(pluginDir, "node_modules", "direct");
fs.mkdirSync(nodeModulesDir, { recursive: true });
@@ -1073,11 +1078,7 @@ describe("stageBundledPluginRuntimeDeps", () => {
'{ "name": "direct", "version": "1.2.3" }\n',
"utf8",
);
fs.writeFileSync(
path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"),
`${JSON.stringify({ fingerprint }, null, 2)}\n`,
"utf8",
);
writeRuntimeDepsStamp(stampPath, fingerprint);
},
});
@@ -1097,7 +1098,7 @@ describe("stageBundledPluginRuntimeDeps", () => {
let installCount = 0;
stageBundledPluginRuntimeDeps({
cwd: repoRoot,
installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => {
installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => {
installCount += 1;
if (installCount < 3) {
throw new Error(`attempt ${installCount} failed`);
@@ -1105,11 +1106,7 @@ describe("stageBundledPluginRuntimeDeps", () => {
const nodeModulesDir = path.join(pluginDir, "node_modules");
fs.mkdirSync(nodeModulesDir, { recursive: true });
fs.writeFileSync(path.join(nodeModulesDir, "marker.txt"), "ok\n", "utf8");
fs.writeFileSync(
path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"),
`${JSON.stringify({ fingerprint }, null, 2)}\n`,
"utf8",
);
writeRuntimeDepsStamp(stampPath, fingerprint);
},
});