plugins: trim staged runtime cargo

This commit is contained in:
Gustavo Madeira Santana
2026-04-13 18:02:56 -04:00
parent e04a63d08a
commit b5dcc11273
3 changed files with 194 additions and 19 deletions

View File

@@ -0,0 +1,11 @@
.DS_Store
dist/.boundary-tsc.tsbuildinfo
**/*.map
**/*.test.ts
**/*.test.tsx
**/*.spec.ts
**/*.spec.tsx
**/*.test-helpers.ts
**/*.test-harness.ts
src/test-*.ts
src/test-support/

View File

@@ -62,12 +62,48 @@ function dependencyVersionSatisfied(spec, installedVersion) {
return semverSatisfies(installedVersion, spec, { includePrerelease: false });
}
const stagedRuntimeDepPruneRules = new Map([
const defaultStagedRuntimeDepGlobalPruneSuffixes = [".map"];
const defaultStagedRuntimeDepPruneRules = new Map([
// Type declarations only; runtime resolves through lib/es entrypoints.
["@larksuiteoapi/node-sdk", ["types"]],
["@larksuiteoapi/node-sdk", { paths: ["types"] }],
[
"@matrix-org/matrix-sdk-crypto-nodejs",
{
paths: ["index.d.ts", "README.md", "CHANGELOG.md", "RELEASING.md", ".node-version"],
},
],
[
"@matrix-org/matrix-sdk-crypto-wasm",
{
paths: [
"index.d.ts",
"pkg/matrix_sdk_crypto_wasm.d.ts",
"pkg/matrix_sdk_crypto_wasm_bg.wasm.d.ts",
"README.md",
],
},
],
[
"matrix-js-sdk",
{
paths: ["src", "CHANGELOG.md", "CONTRIBUTING.rst", "README.md", "release.sh"],
suffixes: [".d.ts"],
},
],
["matrix-widget-api", { paths: ["src"], suffixes: [".d.ts"] }],
["oidc-client-ts", { paths: ["README.md"], suffixes: [".d.ts"] }],
["music-metadata", { paths: ["README.md"], suffixes: [".d.ts"] }],
]);
const runtimeDepsStagingVersion = 2;
function resolveRuntimeDepPruneConfig(params = {}) {
return {
globalPruneSuffixes:
params.stagedRuntimeDepGlobalPruneSuffixes ?? defaultStagedRuntimeDepGlobalPruneSuffixes,
pruneRules: params.stagedRuntimeDepPruneRules ?? defaultStagedRuntimeDepPruneRules,
};
}
function collectInstalledRuntimeClosure(rootNodeModulesDir, dependencySpecs) {
const packageCache = new Map();
const closure = new Set();
@@ -102,20 +138,73 @@ function collectInstalledRuntimeClosure(rootNodeModulesDir, dependencySpecs) {
return [...closure];
}
function pruneStagedInstalledDependencyCargo(nodeModulesDir, depName) {
const prunePaths = stagedRuntimeDepPruneRules.get(depName);
if (!prunePaths) {
function walkFiles(rootDir, visitFile) {
if (!fs.existsSync(rootDir)) {
return;
}
const depRoot = dependencyNodeModulesPath(nodeModulesDir, depName);
for (const relativePath of prunePaths) {
removePathIfExists(path.join(depRoot, relativePath));
const queue = [rootDir];
while (queue.length > 0) {
const currentDir = queue.shift();
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
queue.push(fullPath);
continue;
}
if (entry.isFile()) {
visitFile(fullPath);
}
}
}
}
function pruneStagedRuntimeDependencyCargo(nodeModulesDir) {
for (const depName of stagedRuntimeDepPruneRules.keys()) {
pruneStagedInstalledDependencyCargo(nodeModulesDir, depName);
function pruneDependencyFilesBySuffixes(depRoot, suffixes) {
if (!suffixes || suffixes.length === 0 || !fs.existsSync(depRoot)) {
return;
}
walkFiles(depRoot, (fullPath) => {
if (suffixes.some((suffix) => fullPath.endsWith(suffix))) {
removePathIfExists(fullPath);
}
});
}
function pruneStagedInstalledDependencyCargo(nodeModulesDir, depName, pruneConfig) {
const depRoot = dependencyNodeModulesPath(nodeModulesDir, depName);
const pruneRule = pruneConfig.pruneRules.get(depName);
for (const relativePath of pruneRule?.paths ?? []) {
removePathIfExists(path.join(depRoot, relativePath));
}
pruneDependencyFilesBySuffixes(depRoot, pruneConfig.globalPruneSuffixes);
pruneDependencyFilesBySuffixes(depRoot, pruneRule?.suffixes ?? []);
}
function listInstalledDependencyNames(nodeModulesDir) {
if (!fs.existsSync(nodeModulesDir)) {
return [];
}
const names = [];
for (const entry of fs.readdirSync(nodeModulesDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
if (entry.name.startsWith("@")) {
const scopeDir = path.join(nodeModulesDir, entry.name);
for (const scopedEntry of fs.readdirSync(scopeDir, { withFileTypes: true })) {
if (scopedEntry.isDirectory()) {
names.push(`${entry.name}/${scopedEntry.name}`);
}
}
continue;
}
names.push(entry.name);
}
return names;
}
function pruneStagedRuntimeDependencyCargo(nodeModulesDir, pruneConfig) {
for (const depName of listInstalledDependencyNames(nodeModulesDir)) {
pruneStagedInstalledDependencyCargo(nodeModulesDir, depName, pruneConfig);
}
}
@@ -174,12 +263,13 @@ function resolveRuntimeDepsStampPath(pluginDir) {
return path.join(pluginDir, ".openclaw-runtime-deps-stamp.json");
}
function createRuntimeDepsFingerprint(packageJson) {
function createRuntimeDepsFingerprint(packageJson, pruneConfig) {
return createHash("sha256")
.update(
JSON.stringify({
globalPruneSuffixes: pruneConfig.globalPruneSuffixes,
packageJson,
pruneRules: [...stagedRuntimeDepPruneRules.entries()],
pruneRules: [...pruneConfig.pruneRules.entries()],
version: runtimeDepsStagingVersion,
}),
)
@@ -198,7 +288,7 @@ function readRuntimeDepsStamp(stampPath) {
}
function stageInstalledRootRuntimeDeps(params) {
const { fingerprint, packageJson, pluginDir, repoRoot } = params;
const { fingerprint, packageJson, pluginDir, pruneConfig, repoRoot } = params;
const dependencySpecs = {
...packageJson.dependencies,
...packageJson.optionalDependencies,
@@ -230,7 +320,7 @@ function stageInstalledRootRuntimeDeps(params) {
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.cpSync(sourcePath, targetPath, { recursive: true, force: true, dereference: true });
}
pruneStagedRuntimeDependencyCargo(stagedNodeModulesDir);
pruneStagedRuntimeDependencyCargo(stagedNodeModulesDir, pruneConfig);
replaceDir(nodeModulesDir, stagedNodeModulesDir);
writeJson(stampPath, {
@@ -244,10 +334,10 @@ function stageInstalledRootRuntimeDeps(params) {
}
function installPluginRuntimeDeps(params) {
const { fingerprint, packageJson, pluginDir, pluginId, repoRoot } = params;
const { fingerprint, packageJson, pluginDir, pluginId, pruneConfig, repoRoot } = params;
if (
repoRoot &&
stageInstalledRootRuntimeDeps({ fingerprint, packageJson, pluginDir, repoRoot })
stageInstalledRootRuntimeDeps({ fingerprint, packageJson, pluginDir, pruneConfig, repoRoot })
) {
return;
}
@@ -291,7 +381,7 @@ function installPluginRuntimeDeps(params) {
);
}
pruneStagedRuntimeDependencyCargo(stagedNodeModulesDir);
pruneStagedRuntimeDependencyCargo(stagedNodeModulesDir, pruneConfig);
replaceDir(nodeModulesDir, stagedNodeModulesDir);
writeJson(stampPath, {
@@ -325,6 +415,7 @@ export function stageBundledPluginRuntimeDeps(params = {}) {
const installPluginRuntimeDepsImpl =
params.installPluginRuntimeDepsImpl ?? installPluginRuntimeDeps;
const installAttempts = params.installAttempts ?? 3;
const pruneConfig = resolveRuntimeDepPruneConfig(params);
for (const pluginDir of listBundledPluginRuntimeDirs(repoRoot)) {
const pluginId = path.basename(pluginDir);
const packageJson = sanitizeBundledManifestForRuntimeInstall(pluginDir);
@@ -335,7 +426,7 @@ export function stageBundledPluginRuntimeDeps(params = {}) {
removePathIfExists(stampPath);
continue;
}
const fingerprint = createRuntimeDepsFingerprint(packageJson);
const fingerprint = createRuntimeDepsFingerprint(packageJson, pruneConfig);
const stamp = readRuntimeDepsStamp(stampPath);
if (fs.existsSync(nodeModulesDir) && stamp?.fingerprint === fingerprint) {
continue;
@@ -348,6 +439,7 @@ export function stageBundledPluginRuntimeDeps(params = {}) {
packageJson,
pluginDir,
pluginId,
pruneConfig,
repoRoot,
},
});

View File

@@ -189,6 +189,78 @@ describe("stageBundledPluginRuntimeDeps", () => {
).toBe("module.exports = 'transitive';\n");
});
it("removes source maps from staged runtime dependencies", () => {
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 = 1;\n", "utf8");
fs.writeFileSync(path.join(directDir, "index.js.map"), '{ "version": 3 }\n', "utf8");
stageBundledPluginRuntimeDeps({ cwd: repoRoot });
expect(fs.existsSync(path.join(pluginDir, "node_modules", "direct", "index.js"))).toBe(true);
expect(fs.existsSync(path.join(pluginDir, "node_modules", "direct", "index.js.map"))).toBe(
false,
);
});
it("applies package-specific cargo prune rules after staging", () => {
const { pluginDir, repoRoot } = createBundledPluginFixture({
packageJson: {
name: "@openclaw/fixture-plugin",
version: "1.0.0",
dependencies: { "rule-target": "1.0.0" },
openclaw: { bundle: { stageRuntimeDependencies: true } },
},
});
const depDir = path.join(repoRoot, "node_modules", "rule-target");
fs.mkdirSync(path.join(depDir, "docs"), { recursive: true });
fs.mkdirSync(path.join(depDir, "lib"), { recursive: true });
fs.writeFileSync(
path.join(depDir, "package.json"),
'{ "name": "rule-target", "version": "1.0.0" }\n',
"utf8",
);
fs.writeFileSync(path.join(depDir, "lib", "index.js"), "export {};\n", "utf8");
fs.writeFileSync(path.join(depDir, "lib", "index.d.ts"), "export {};\n", "utf8");
fs.writeFileSync(path.join(depDir, "docs", "guide.md"), "docs\n", "utf8");
fs.writeFileSync(path.join(depDir, "README.md"), "readme\n", "utf8");
fs.writeFileSync(path.join(depDir, "LICENSE"), "license\n", "utf8");
stageBundledPluginRuntimeDeps({
cwd: repoRoot,
stagedRuntimeDepPruneRules: new Map([
["rule-target", { paths: ["docs", "README.md"], suffixes: [".d.ts"] }],
]),
});
expect(
fs.existsSync(path.join(pluginDir, "node_modules", "rule-target", "lib", "index.js")),
).toBe(true);
expect(
fs.existsSync(path.join(pluginDir, "node_modules", "rule-target", "lib", "index.d.ts")),
).toBe(false);
expect(fs.existsSync(path.join(pluginDir, "node_modules", "rule-target", "docs"))).toBe(false);
expect(fs.existsSync(path.join(pluginDir, "node_modules", "rule-target", "README.md"))).toBe(
false,
);
expect(fs.existsSync(path.join(pluginDir, "node_modules", "rule-target", "LICENSE"))).toBe(
true,
);
});
it("falls back to staging installs when the root dependency version is incompatible", () => {
const { pluginDir, repoRoot } = createBundledPluginFixture({
packageJson: {