build: preserve staged plugin runtime deps

This commit is contained in:
Peter Steinberger
2026-04-24 16:17:59 +01:00
parent 2b45a112cb
commit f3bcea8732
3 changed files with 31 additions and 9 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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", () => {