mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:40:49 +00:00
fix: stage patched WhatsApp Baileys runtime deps
This commit is contained in:
@@ -47,6 +47,9 @@
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.4.12"
|
||||
},
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.4.12"
|
||||
},
|
||||
|
||||
@@ -15,6 +15,13 @@ function writeJson(filePath, value) {
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function readOptionalUtf8(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
}
|
||||
|
||||
function removePathIfExists(targetPath) {
|
||||
fs.rmSync(targetPath, { recursive: true, force: true });
|
||||
}
|
||||
@@ -147,6 +154,81 @@ function collectInstalledRuntimeClosure(rootNodeModulesDir, dependencySpecs) {
|
||||
return [...closure];
|
||||
}
|
||||
|
||||
function resolveInstalledDirectDependencyNames(rootNodeModulesDir, dependencySpecs) {
|
||||
const directDependencyNames = [];
|
||||
for (const [depName, spec] of Object.entries(dependencySpecs)) {
|
||||
const installedVersion = readInstalledDependencyVersion(rootNodeModulesDir, depName);
|
||||
if (installedVersion === null || !dependencyVersionSatisfied(spec, installedVersion)) {
|
||||
return null;
|
||||
}
|
||||
directDependencyNames.push(depName);
|
||||
}
|
||||
return directDependencyNames;
|
||||
}
|
||||
|
||||
function appendDirectoryFingerprint(hash, rootDir, currentDir = rootDir) {
|
||||
const entries = fs
|
||||
.readdirSync(currentDir, { withFileTypes: true })
|
||||
.toSorted((left, right) => left.name.localeCompare(right.name));
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
const stat = fs.statSync(fullPath);
|
||||
const relativePath = path.relative(rootDir, fullPath).replace(/\\/g, "/");
|
||||
if (stat.isDirectory()) {
|
||||
hash.update(`dir:${relativePath}\n`);
|
||||
appendDirectoryFingerprint(hash, rootDir, fullPath);
|
||||
continue;
|
||||
}
|
||||
if (!stat.isFile()) {
|
||||
continue;
|
||||
}
|
||||
hash.update(`file:${relativePath}:${stat.size}\n`);
|
||||
hash.update(fs.readFileSync(fullPath));
|
||||
}
|
||||
}
|
||||
|
||||
function createInstalledRuntimeClosureFingerprint(rootNodeModulesDir, dependencyNames) {
|
||||
const hash = createHash("sha256");
|
||||
for (const depName of [...dependencyNames].toSorted((left, right) => left.localeCompare(right))) {
|
||||
const depRoot = dependencyNodeModulesPath(rootNodeModulesDir, depName);
|
||||
if (!fs.existsSync(depRoot)) {
|
||||
return null;
|
||||
}
|
||||
hash.update(`package:${depName}\n`);
|
||||
appendDirectoryFingerprint(hash, depRoot);
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
function resolveInstalledRuntimeClosureFingerprint(params) {
|
||||
const dependencySpecs = {
|
||||
...params.packageJson.dependencies,
|
||||
...params.packageJson.optionalDependencies,
|
||||
};
|
||||
if (Object.keys(dependencySpecs).length === 0 || !fs.existsSync(params.rootNodeModulesDir)) {
|
||||
return null;
|
||||
}
|
||||
const directDependencyNames = resolveInstalledDirectDependencyNames(
|
||||
params.rootNodeModulesDir,
|
||||
dependencySpecs,
|
||||
);
|
||||
if (directDependencyNames === null) {
|
||||
return null;
|
||||
}
|
||||
const dependencyNames = new Set(directDependencyNames);
|
||||
const transitiveClosure = collectInstalledRuntimeClosure(
|
||||
params.rootNodeModulesDir,
|
||||
dependencySpecs,
|
||||
);
|
||||
if (transitiveClosure !== null) {
|
||||
for (const depName of transitiveClosure) {
|
||||
dependencyNames.add(depName);
|
||||
}
|
||||
}
|
||||
return createInstalledRuntimeClosureFingerprint(params.rootNodeModulesDir, dependencyNames);
|
||||
}
|
||||
|
||||
function walkFiles(rootDir, visitFile) {
|
||||
if (!fs.existsSync(rootDir)) {
|
||||
return;
|
||||
@@ -272,13 +354,22 @@ function resolveRuntimeDepsStampPath(pluginDir) {
|
||||
return path.join(pluginDir, ".openclaw-runtime-deps-stamp.json");
|
||||
}
|
||||
|
||||
function createRuntimeDepsFingerprint(packageJson, pruneConfig) {
|
||||
function createRuntimeDepsFingerprint(packageJson, pruneConfig, params = {}) {
|
||||
const repoRoot = params.repoRoot;
|
||||
const lockfilePath =
|
||||
typeof repoRoot === "string" && repoRoot.length > 0
|
||||
? path.join(repoRoot, "pnpm-lock.yaml")
|
||||
: null;
|
||||
const rootLockfile = lockfilePath ? readOptionalUtf8(lockfilePath) : null;
|
||||
return createHash("sha256")
|
||||
.update(
|
||||
JSON.stringify({
|
||||
globalPruneSuffixes: pruneConfig.globalPruneSuffixes,
|
||||
packageJson,
|
||||
pruneRules: [...pruneConfig.pruneRules.entries()],
|
||||
rootInstalledRuntimeFingerprint: params.rootInstalledRuntimeFingerprint ?? null,
|
||||
rootLockfile,
|
||||
pruneRules: [...pruneConfig.pruneRules.entries()],
|
||||
version: runtimeDepsStagingVersion,
|
||||
}),
|
||||
)
|
||||
@@ -307,10 +398,20 @@ function stageInstalledRootRuntimeDeps(params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dependencyNames = collectInstalledRuntimeClosure(rootNodeModulesDir, dependencySpecs);
|
||||
if (dependencyNames === null) {
|
||||
const directDependencyNames = resolveInstalledDirectDependencyNames(
|
||||
rootNodeModulesDir,
|
||||
dependencySpecs,
|
||||
);
|
||||
if (directDependencyNames === null) {
|
||||
return false;
|
||||
}
|
||||
const dependencyNames = new Set(directDependencyNames);
|
||||
const transitiveClosure = collectInstalledRuntimeClosure(rootNodeModulesDir, dependencySpecs);
|
||||
if (transitiveClosure !== null) {
|
||||
for (const depName of transitiveClosure) {
|
||||
dependencyNames.add(depName);
|
||||
}
|
||||
}
|
||||
|
||||
const nodeModulesDir = path.join(pluginDir, "node_modules");
|
||||
const stampPath = resolveRuntimeDepsStampPath(pluginDir);
|
||||
@@ -323,7 +424,9 @@ function stageInstalledRootRuntimeDeps(params) {
|
||||
);
|
||||
|
||||
try {
|
||||
for (const depName of dependencyNames) {
|
||||
for (const depName of [...dependencyNames].toSorted((left, right) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
const sourcePath = dependencyNodeModulesPath(rootNodeModulesDir, depName);
|
||||
const targetPath = dependencyNodeModulesPath(stagedNodeModulesDir, depName);
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
@@ -435,7 +538,14 @@ export function stageBundledPluginRuntimeDeps(params = {}) {
|
||||
removePathIfExists(stampPath);
|
||||
continue;
|
||||
}
|
||||
const fingerprint = createRuntimeDepsFingerprint(packageJson, pruneConfig);
|
||||
const rootInstalledRuntimeFingerprint = resolveInstalledRuntimeClosureFingerprint({
|
||||
packageJson,
|
||||
rootNodeModulesDir: path.join(repoRoot, "node_modules"),
|
||||
});
|
||||
const fingerprint = createRuntimeDepsFingerprint(packageJson, pruneConfig, {
|
||||
repoRoot,
|
||||
rootInstalledRuntimeFingerprint,
|
||||
});
|
||||
const stamp = readRuntimeDepsStamp(stampPath);
|
||||
if (fs.existsSync(nodeModulesDir) && stamp?.fingerprint === fingerprint) {
|
||||
continue;
|
||||
|
||||
@@ -62,4 +62,20 @@ describe("collectBuiltBundledPluginStagedRuntimeDependencyErrors", () => {
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps the WhatsApp bundled plugin opted into staged runtime dependencies", () => {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(path.join(process.cwd(), "extensions/whatsapp/package.json"), "utf8"),
|
||||
) as {
|
||||
dependencies?: Record<string, string>;
|
||||
openclaw?: {
|
||||
bundle?: {
|
||||
stageRuntimeDependencies?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
expect(packageJson.dependencies?.["@whiskeysockets/baileys"]).toBe("7.0.0-rc.9");
|
||||
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,6 +123,77 @@ describe("stageBundledPluginRuntimeDeps", () => {
|
||||
expect(fs.readFileSync(path.join(pluginDir, "node_modules", "marker.txt"), "utf8")).toBe("2\n");
|
||||
});
|
||||
|
||||
it("restages when the root pnpm lockfile changes", () => {
|
||||
const { pluginDir, repoRoot } = createBundledPluginFixture({
|
||||
packageJson: {
|
||||
name: "@openclaw/fixture-plugin",
|
||||
version: "1.0.0",
|
||||
dependencies: { "left-pad": "1.3.0" },
|
||||
openclaw: { bundle: { stageRuntimeDependencies: true } },
|
||||
},
|
||||
});
|
||||
fs.writeFileSync(path.join(repoRoot, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n", "utf8");
|
||||
|
||||
let installCount = 0;
|
||||
const stageOnce = () =>
|
||||
stageBundledPluginRuntimeDeps({
|
||||
cwd: repoRoot,
|
||||
installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => {
|
||||
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",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
stageOnce();
|
||||
fs.writeFileSync(
|
||||
path.join(repoRoot, "pnpm-lock.yaml"),
|
||||
"lockfileVersion: '9.0'\npatchedDependencies:\n left-pad@1.3.0: patches/left-pad.patch\n",
|
||||
"utf8",
|
||||
);
|
||||
stageOnce();
|
||||
|
||||
expect(installCount).toBe(2);
|
||||
expect(fs.readFileSync(path.join(pluginDir, "node_modules", "marker.txt"), "utf8")).toBe("2\n");
|
||||
});
|
||||
|
||||
it("restages when installed root runtime dependency contents change", () => {
|
||||
const { pluginDir, repoRoot } = createBundledPluginFixture({
|
||||
packageJson: {
|
||||
name: "@openclaw/fixture-plugin",
|
||||
version: "1.0.0",
|
||||
dependencies: { direct: "1.0.0" },
|
||||
openclaw: { bundle: { stageRuntimeDependencies: true } },
|
||||
},
|
||||
});
|
||||
const directDir = path.join(repoRoot, "node_modules", "direct");
|
||||
fs.mkdirSync(directDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(directDir, "package.json"),
|
||||
'{ "name": "direct", "version": "1.0.0" }\n',
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(path.join(directDir, "index.js"), "module.exports = 'first';\n", "utf8");
|
||||
|
||||
stageBundledPluginRuntimeDeps({ cwd: repoRoot });
|
||||
expect(
|
||||
fs.readFileSync(path.join(pluginDir, "node_modules", "direct", "index.js"), "utf8"),
|
||||
).toBe("module.exports = 'first';\n");
|
||||
|
||||
fs.writeFileSync(path.join(directDir, "index.js"), "module.exports = 'second';\n", "utf8");
|
||||
stageBundledPluginRuntimeDeps({ cwd: repoRoot });
|
||||
|
||||
expect(
|
||||
fs.readFileSync(path.join(pluginDir, "node_modules", "direct", "index.js"), "utf8"),
|
||||
).toBe("module.exports = 'second';\n");
|
||||
});
|
||||
|
||||
it("stages runtime deps from the root node_modules when already installed", () => {
|
||||
const { pluginDir, repoRoot } = createBundledPluginFixture({
|
||||
packageJson: {
|
||||
@@ -189,6 +260,44 @@ describe("stageBundledPluginRuntimeDeps", () => {
|
||||
).toBe("module.exports = 'transitive';\n");
|
||||
});
|
||||
|
||||
it("stages nested dependency trees from installed direct package roots", () => {
|
||||
const { pluginDir, repoRoot } = createBundledPluginFixture({
|
||||
packageJson: {
|
||||
name: "@openclaw/fixture-plugin",
|
||||
version: "1.0.0",
|
||||
dependencies: { direct: "1.0.0" },
|
||||
openclaw: { bundle: { stageRuntimeDependencies: true } },
|
||||
},
|
||||
});
|
||||
const directDir = path.join(repoRoot, "node_modules", "direct");
|
||||
const nestedDir = path.join(directDir, "node_modules", "nested");
|
||||
fs.mkdirSync(nestedDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(directDir, "package.json"),
|
||||
'{ "name": "direct", "version": "1.0.0", "dependencies": { "nested": "^1.0.0" } }\n',
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(path.join(directDir, "index.js"), "module.exports = 'direct';\n", "utf8");
|
||||
fs.writeFileSync(
|
||||
path.join(nestedDir, "package.json"),
|
||||
'{ "name": "nested", "version": "1.0.0" }\n',
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(path.join(nestedDir, "index.js"), "module.exports = 'nested';\n", "utf8");
|
||||
|
||||
stageBundledPluginRuntimeDeps({ cwd: repoRoot });
|
||||
|
||||
expect(
|
||||
fs.readFileSync(path.join(pluginDir, "node_modules", "direct", "index.js"), "utf8"),
|
||||
).toBe("module.exports = 'direct';\n");
|
||||
expect(
|
||||
fs.readFileSync(
|
||||
path.join(pluginDir, "node_modules", "direct", "node_modules", "nested", "index.js"),
|
||||
"utf8",
|
||||
),
|
||||
).toBe("module.exports = 'nested';\n");
|
||||
});
|
||||
|
||||
it("removes global non-runtime suffixes from staged runtime dependencies", () => {
|
||||
const { pluginDir, repoRoot } = createBundledPluginFixture({
|
||||
packageJson: {
|
||||
|
||||
Reference in New Issue
Block a user