From f3bcea8732728f83d23b7aeb6adc63b4231b3252 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 24 Apr 2026 16:17:59 +0100 Subject: [PATCH] build: preserve staged plugin runtime deps --- scripts/copy-bundled-plugin-metadata.mjs | 10 +++++--- scripts/stage-bundled-plugin-runtime-deps.mjs | 24 +++++++++++++++++-- .../copy-bundled-plugin-metadata.test.ts | 6 ++--- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/scripts/copy-bundled-plugin-metadata.mjs b/scripts/copy-bundled-plugin-metadata.mjs index 0c141d5602f..1f83bf9d84b 100644 --- a/scripts/copy-bundled-plugin-metadata.mjs +++ b/scripts/copy-bundled-plugin-metadata.mjs @@ -191,6 +191,11 @@ function copyDeclaredPluginSkillPaths(params) { const shouldExcludeNestedNodeModules = /^node_modules(?:\/|$)/u.test( normalizeManifestRelativePath(raw), ); + if (shouldExcludeNestedNodeModules) { + removePathIfExists( + ensurePathInsideRoot(params.distPluginDir, normalizeManifestRelativePath(raw)), + ); + } copySkillPathWithRetry({ sourcePath, targetPath, @@ -270,10 +275,9 @@ export function copyBundledPluginMetadata(params = {}) { if (fs.existsSync(manifestPath)) { const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); - // Generated skill assets live under a dedicated dist-owned directory. Also - // remove the older bad node_modules tree so release packs cannot pick it up. + // Generated skill assets live under a dedicated dist-owned directory. Runtime + // dependency staging owns dist plugin node_modules; do not remove it here. removePathIfExists(path.join(distPluginDir, GENERATED_BUNDLED_SKILLS_DIR)); - removePathIfExists(path.join(distPluginDir, "node_modules")); const copiedSkills = copyDeclaredPluginSkillPaths({ manifest, pluginDir, diff --git a/scripts/stage-bundled-plugin-runtime-deps.mjs b/scripts/stage-bundled-plugin-runtime-deps.mjs index 63ebe614c39..8b4c0db2bfc 100644 --- a/scripts/stage-bundled-plugin-runtime-deps.mjs +++ b/scripts/stage-bundled-plugin-runtime-deps.mjs @@ -885,6 +885,17 @@ function resolveRuntimeDepsStampPath(repoRoot, pluginId) { } function createRuntimeDepsFingerprint(packageJson, pruneConfig, params = {}) { + return createHash("sha256") + .update( + JSON.stringify({ + cheapFingerprint: createRuntimeDepsCheapFingerprint(packageJson, pruneConfig, params), + rootInstalledRuntimeFingerprint: params.rootInstalledRuntimeFingerprint ?? null, + }), + ) + .digest("hex"); +} + +function createRuntimeDepsCheapFingerprint(packageJson, pruneConfig, params = {}) { const repoRoot = params.repoRoot; const lockfilePath = typeof repoRoot === "string" && repoRoot.length > 0 @@ -901,7 +912,6 @@ function createRuntimeDepsFingerprint(packageJson, pruneConfig, params = {}) { globalPruneSuffixes: pruneConfig.globalPruneSuffixes, packageJson, pruneRules: [...pruneConfig.pruneRules.entries()], - rootInstalledRuntimeFingerprint: params.rootInstalledRuntimeFingerprint ?? null, rootLockfile, version: runtimeDepsStagingVersion, }), @@ -949,6 +959,7 @@ function removeStaleRuntimeDepsTempDirs(pluginDir) { function stageInstalledRootRuntimeDeps(params) { const { directDependencyPackageRoot = null, + cheapFingerprint, fingerprint, packageJson, pluginDir, @@ -990,6 +1001,7 @@ function stageInstalledRootRuntimeDeps(params) { assertPathIsNotSymlink(nodeModulesDir, "remove runtime deps"); removePathIfExists(nodeModulesDir); writeJsonAtomically(stampPath, { + cheapFingerprint, fingerprint, generatedAt: new Date().toISOString(), }); @@ -1029,6 +1041,7 @@ function stageInstalledRootRuntimeDeps(params) { replaceDirAtomically(nodeModulesDir, stagedNodeModulesDir); writeJsonAtomically(stampPath, { + cheapFingerprint, fingerprint, generatedAt: new Date().toISOString(), }); @@ -1078,6 +1091,7 @@ function createRootRuntimeStagingError(params) { function installPluginRuntimeDeps(params) { const { directDependencyPackageRoot = null, + cheapFingerprint, fingerprint, packageJson, pluginDir, @@ -1120,6 +1134,7 @@ function installPluginRuntimeDeps(params) { removePathIfExists(nodeModulesDir); } writeJsonAtomically(stampPath, { + cheapFingerprint, fingerprint, generatedAt: new Date().toISOString(), }); @@ -1151,6 +1166,10 @@ export function stageBundledPluginRuntimeDeps(params = {}) { removePathIfExists(stampPath); continue; } + const cheapFingerprint = createRuntimeDepsCheapFingerprint(packageJson, pruneConfig, { + repoRoot, + }); + const stamp = readRuntimeDepsStamp(stampPath); const rootInstalledRuntimeFingerprint = resolveInstalledRuntimeClosureFingerprint({ directDependencyPackageRoot, packageJson, @@ -1160,7 +1179,6 @@ export function stageBundledPluginRuntimeDeps(params = {}) { repoRoot, rootInstalledRuntimeFingerprint, }); - const stamp = readRuntimeDepsStamp(stampPath); if (fs.existsSync(nodeModulesDir) && stamp?.fingerprint === fingerprint) { continue; } @@ -1168,6 +1186,7 @@ export function stageBundledPluginRuntimeDeps(params = {}) { stageInstalledRootRuntimeDeps({ directDependencyPackageRoot, fingerprint, + cheapFingerprint, packageJson, pluginDir, pruneConfig, @@ -1184,6 +1203,7 @@ export function stageBundledPluginRuntimeDeps(params = {}) { installParams: { directDependencyPackageRoot, fingerprint, + cheapFingerprint, packageJson, pluginDir, pluginId, diff --git a/src/plugins/copy-bundled-plugin-metadata.test.ts b/src/plugins/copy-bundled-plugin-metadata.test.ts index 277741428ba..16638a84db4 100644 --- a/src/plugins/copy-bundled-plugin-metadata.test.ts +++ b/src/plugins/copy-bundled-plugin-metadata.test.ts @@ -172,9 +172,7 @@ describe("copyBundledPluginMetadata", () => { expect(fs.existsSync(path.join(copiedSkillDir, "SKILL.md"))).toBe(true); expect(fs.lstatSync(copiedSkillDir).isSymbolicLink()).toBe(false); expect(fs.existsSync(path.join(copiedSkillDir, "node_modules"))).toBe(false); - expect(fs.existsSync(path.join(bundledPluginDir(repoRoot, "tlon"), "node_modules"))).toBe( - false, - ); + expect(fs.existsSync(staleNodeModulesSkillDir)).toBe(false); expectBundledSkills(repoRoot, "tlon", ["./bundled-skills/@tloncorp/tlon-skill"]); }); @@ -217,7 +215,7 @@ describe("copyBundledPluginMetadata", () => { expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "tlon", "bundled-skills"))).toBe( false, ); - expect(fs.existsSync(staleNodeModulesDir)).toBe(false); + expect(fs.existsSync(staleNodeModulesDir)).toBe(true); }); it("retries transient skill copy races from concurrent runtime postbuilds", () => {