diff --git a/package.json b/package.json index 358212d42ed7..435eb46d66b3 100644 --- a/package.json +++ b/package.json @@ -2248,7 +2248,8 @@ }, "bundleDependencies": [ "@openclaw/proxyline", - "chrome-devtools-mcp" + "chrome-devtools-mcp", + "undici" ], "dependencies": { "@agentclientprotocol/sdk": "1.4.0", diff --git a/patches/README.md b/patches/README.md index 2d4070de23ec..844edaea393a 100644 --- a/patches/README.md +++ b/patches/README.md @@ -8,7 +8,9 @@ agent to retain per-request `NO_PROXY` routing even when every configured proxy route is invalid. The patch changes only the package entrypoint and regenerated entrypoint declarations; proxy behavior and the package version are unchanged. The root package bundles the dependency so npm installations retain the export. -Remove this patch, its registration, and the bundle entry when an upstream release +Its pinned Undici peer is bundled too: npm infers that bundled peers are already +present and otherwise skips installing Undici, even though the root requires it. +Remove this patch, its registration, and both bundle entries when an upstream release exports the standalone agent and passes `extensions/whatsapp/src/session.media-upload.test.ts` and `src/infra/net/node-proxy-agent.test.ts` without the patch. diff --git a/scripts/lib/package-bundled-dependencies.mts b/scripts/lib/package-bundled-dependencies.mts index 9ee9a8a86e23..0a7c62f517b3 100644 --- a/scripts/lib/package-bundled-dependencies.mts +++ b/scripts/lib/package-bundled-dependencies.mts @@ -236,6 +236,18 @@ export function collectBundledDependencyErrors({ errors.push(`bundled ${name} package.json must name ${name}`); continue; } + // npm infers peers of bundled packages are already in the bundle, while + // packlist excludes them unless explicitly bundled. Missing peers are not + // installed, even when the root declares them as required dependencies. + const peers = isRecord(manifest.peerDependencies) ? manifest.peerDependencies : {}; + for (const peer of Object.keys(peers)) { + if ( + typeof dependencies[peer] === "string" && + !runtime.entries.has(`node_modules/${peer}/package.json`) + ) { + errors.push(`bundled ${name} is missing its root dependency peer ${peer}`); + } + } const bundled = { ...runtime, name }; if (name === PATCHED_MCP_NAME) { errors.push(...collectPatchedMcpErrors(bundled, manifest, dependencies[PATCHED_MCP_NAME])); diff --git a/test/scripts/check-openclaw-package-tarball.test.ts b/test/scripts/check-openclaw-package-tarball.test.ts index 5239907b5e6a..22eac6583461 100644 --- a/test/scripts/check-openclaw-package-tarball.test.ts +++ b/test/scripts/check-openclaw-package-tarball.test.ts @@ -1085,6 +1085,52 @@ syncBuiltinESMExports(); ); const bundledRuntimeCases: NamedTarballCheck[] = [ + ...[ + { bundledPeer: false, optional: false }, + { bundledPeer: false, optional: true }, + { bundledPeer: true, optional: false }, + ].map(({ bundledPeer, optional }): NamedTarballCheck => ({ + name: `${bundledPeer ? "accepts" : "rejects"} a bundled dependency with ${bundledPeer ? "a bundled" : "a missing"} root-required ${optional ? "optional" : "required"} peer`, + files: { + "dist/index.js": "export {};\n", + "node_modules/example/package.json": JSON.stringify({ + name: "example", + version: "1.0.0", + peerDependencies: { host: "^1.0.0" }, + ...(optional ? { peerDependenciesMeta: { host: { optional: true } } } : {}), + }), + ...(bundledPeer + ? { "node_modules/host/package.json": '{"name":"host","version":"1.0.0"}\n' } + : {}), + }, + options: { + packageJson: { + dependencies: { example: "1.0.0", host: "1.0.0" }, + bundleDependencies: bundledPeer ? ["example", "host"] : ["example"], + }, + }, + status: bundledPeer ? 0 : "nonzero", + stderr: bundledPeer ? [] : ["bundled example is missing its root dependency peer host"], + })), + { + name: "accepts an absent optional peer of a bundled dependency", + files: { + "dist/index.js": "export {};\n", + "node_modules/example/package.json": JSON.stringify({ + name: "example", + version: "1.0.0", + peerDependencies: { host: "^1.0.0" }, + peerDependenciesMeta: { host: { optional: true } }, + }), + }, + options: { + packageJson: { + dependencies: { example: "1.0.0" }, + bundleDependencies: ["example"], + }, + }, + status: 0, + }, { name: "accepts npm-selected bundled and hoisted transitive dependency paths", files: {