Skip to content

Commit 6e5042c

Browse files
committed
fix: avoid rescanning repaired plugin peers
1 parent 18ca285 commit 6e5042c

3 files changed

Lines changed: 120 additions & 2 deletions

File tree

src/infra/npm-managed-root.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,13 @@ export async function syncManagedNpmRootPeerDependencies(params: {
427427
return changed;
428428
}
429429

430+
export async function readManagedNpmRootPeerDependencyNames(params: {
431+
npmRoot: string;
432+
}): Promise<Set<string>> {
433+
const manifest = await readManagedNpmRootManifest(path.join(params.npmRoot, "package.json"));
434+
return new Set(readManagedPeerDependencyKeys(manifest.openclaw));
435+
}
436+
430437
export async function repairManagedNpmRootOpenClawPeer(params: {
431438
npmRoot: string;
432439
timeoutMs?: number;

src/plugins/install.npm-spec.e2e.test.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ async function packPlugin(params: {
5252
pluginId: string;
5353
version: string;
5454
rootDir: string;
55+
indexJs?: string;
5556
}): Promise<PackedVersion> {
5657
const packageDir = path.join(params.rootDir, `package-${params.packageName}-${params.version}`);
5758
const peerDependenciesMeta = params.peerDependencies
@@ -94,7 +95,11 @@ async function packPlugin(params: {
9495
)}\n`,
9596
"utf8",
9697
);
97-
await fs.writeFile(path.join(packageDir, "dist", "index.js"), "export {};\n", "utf8");
98+
await fs.writeFile(
99+
path.join(packageDir, "dist", "index.js"),
100+
params.indexJs ?? "export {};\n",
101+
"utf8",
102+
);
98103

99104
const packOutput = execFileSync(
100105
"npm",
@@ -439,6 +444,102 @@ describe("installPluginFromNpmSpec e2e", () => {
439444
).resolves.toBeTruthy();
440445
});
441446

447+
it("does not attribute repaired pre-existing peer dependencies to later installs", async () => {
448+
const rootDir = await makeTempDir("npm-plugin-repaired-peer-scan-e2e");
449+
const npmRoot = path.join(rootDir, "managed-npm");
450+
const pluginWithRuntimePeer = `existing-peer-plugin-${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
451+
const laterPlugin = `later-plugin-${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
452+
const runtimePeer = `runtime-peer-${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
453+
const registry = await startStaticRegistry([
454+
{
455+
packageName: pluginWithRuntimePeer,
456+
latest: "1.0.0",
457+
versions: [
458+
await packPlugin({
459+
packageName: pluginWithRuntimePeer,
460+
peerDependencies: { [runtimePeer]: "^1.0.0" },
461+
peerDependenciesMeta: {},
462+
pluginId: pluginWithRuntimePeer,
463+
version: "1.0.0",
464+
rootDir,
465+
}),
466+
],
467+
},
468+
{
469+
packageName: laterPlugin,
470+
latest: "1.0.0",
471+
versions: [
472+
await packPlugin({
473+
packageName: laterPlugin,
474+
pluginId: laterPlugin,
475+
version: "1.0.0",
476+
rootDir,
477+
}),
478+
],
479+
},
480+
{
481+
packageName: runtimePeer,
482+
latest: "1.0.0",
483+
versions: [
484+
await packPlugin({
485+
indexJs: "eval('1');\n",
486+
packageName: runtimePeer,
487+
pluginId: runtimePeer,
488+
version: "1.0.0",
489+
rootDir,
490+
}),
491+
],
492+
},
493+
]);
494+
process.env.NPM_CONFIG_REGISTRY = registry;
495+
process.env.npm_config_registry = registry;
496+
497+
await fs.mkdir(npmRoot, { recursive: true });
498+
await fs.writeFile(
499+
path.join(npmRoot, "package.json"),
500+
`${JSON.stringify(
501+
{
502+
private: true,
503+
dependencies: { [pluginWithRuntimePeer]: "1.0.0" },
504+
},
505+
null,
506+
2,
507+
)}\n`,
508+
"utf8",
509+
);
510+
await execFileAsync(
511+
"npm",
512+
[
513+
"install",
514+
"--omit=dev",
515+
"--omit=peer",
516+
"--legacy-peer-deps",
517+
"--loglevel=error",
518+
"--ignore-scripts",
519+
"--no-audit",
520+
"--no-fund",
521+
],
522+
{ cwd: npmRoot },
523+
);
524+
await expect(
525+
fs.lstat(path.join(npmRoot, "node_modules", runtimePeer, "package.json")),
526+
).rejects.toHaveProperty("code", "ENOENT");
527+
528+
const later = await installPluginFromNpmSpec({
529+
spec: `${laterPlugin}@1.0.0`,
530+
npmDir: npmRoot,
531+
logger: { info: () => {}, warn: () => {} },
532+
timeoutMs: 120_000,
533+
});
534+
if (!later.ok) {
535+
throw new Error(later.error);
536+
}
537+
538+
await expect(
539+
fs.lstat(path.join(npmRoot, "node_modules", runtimePeer, "package.json")),
540+
).resolves.toBeTruthy();
541+
});
542+
442543
it("scrubs host peers when installing beside an existing host-peer plugin", async () => {
443544
const rootDir = await makeTempDir("npm-plugin-sibling-peer-e2e");
444545
const npmRoot = path.join(rootDir, "managed-npm");

src/plugins/install.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { resolveNpmIntegrityDriftWithDefaultMessage } from "../infra/npm-integrity.js";
1313
import {
1414
readManagedNpmRootInstalledDependency,
15+
readManagedNpmRootPeerDependencyNames,
1516
readOpenClawManagedNpmRootOverrides,
1617
repairManagedNpmRootOpenClawPeer,
1718
removeManagedNpmRootDependency,
@@ -465,11 +466,16 @@ function resolveManagedNpmRootPackageDir(npmRoot: string, packageName: string):
465466

466467
async function listNewManagedNpmRootPackageDirs(params: {
467468
beforeInstallPackageNames: Set<string>;
469+
excludePackageNames?: Set<string>;
468470
npmRoot: string;
469471
}): Promise<string[]> {
470472
const afterInstallPackageNames = await listManagedNpmRootPackageNames(params.npmRoot);
471473
return [...afterInstallPackageNames]
472-
.filter((packageName) => !params.beforeInstallPackageNames.has(packageName))
474+
.filter(
475+
(packageName) =>
476+
!params.beforeInstallPackageNames.has(packageName) &&
477+
!params.excludePackageNames?.has(packageName),
478+
)
473479
.map((packageName) => resolveManagedNpmRootPackageDir(params.npmRoot, packageName))
474480
.toSorted((left, right) => left.localeCompare(right));
475481
}
@@ -572,6 +578,9 @@ async function installPluginFromManagedNpmRoot(
572578
managedOverrides,
573579
});
574580
await syncManagedNpmRootPeerDependencies({ npmRoot, managedOverrides });
581+
const preExistingManagedPeerDependencyNames = await readManagedNpmRootPeerDependencyNames({
582+
npmRoot,
583+
});
575584
const npmInstallArgs = [
576585
"npm",
577586
...createSafeNpmInstallArgs({
@@ -751,6 +760,7 @@ async function installPluginFromManagedNpmRoot(
751760

752761
const newRootPackageDirs = await listNewManagedNpmRootPackageDirs({
753762
beforeInstallPackageNames: preInstallRootPackageNames,
763+
excludePackageNames: preExistingManagedPeerDependencyNames,
754764
npmRoot,
755765
});
756766
const result = await installPluginFromInstalledPackageDir({

0 commit comments

Comments
 (0)