fix(guard): derive must-not-bundle candidates independently of external

Addresses the suppressed review comment on #2099, which was the sharpest of the
three rounds: the guard stopped looking at a package the moment its `external`
entry was removed — so deleting `"undici"` from the web config would have made
the pre-fix bundle pass. That is self-referential, and #2067 was precisely a
MISSING declaration, so the guard covered its regression but not the bug.

Candidates are now the union of two independently-derived lists: each client's
`external` array, and the root manifest's `dependencies` — root-declared by
rule, reached through `core/`, and required to be external in every client
bundle. Removing a package from an `external` list can no longer remove it from
scrutiny.

That immediately surfaced one latent instance of the same rule violation:
`@modelcontextprotocol/ext-apps` is root-declared and reached through
`core/mcp/apps.ts`, but was absent from the CLI's `external` list and was being
inlined into `index.js` — dragging the transitive v1 SDK and zod-to-json-schema
in with it. It is ESM, so it was not failing the way `undici` did; what it
violated was the rule. Adding it drops the CLI bundle from 590KB to 518KB.

It is also a real example of the shape the previous revision could not see: it
was inlined into the entry, emitting no `<pkg>-HASH.js` chunk at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JTHVxSu8AUgHRLvo1ntZ8H
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-23 22:02:54 -04:00
commit dbe95e58ed

View file

@ -205,12 +205,56 @@ describe("evaluateClient", () => {
);
});
test("flags a root dependency even when the external list omits it", () => {
// THE #2067 SHAPE, and the reason the candidate set is not just `externals`.
// `undici` was missing from the web and TUI external lists — that omission
// WAS the bug — so a guard driven only by those lists would have inspected
// the inlined 1.05MB bundle and reported success. Removing a package from an
// `external` list must not remove it from scrutiny.
const failures = evaluateClient({
name: "web",
build: "clients/web/build",
externals: ["pino"],
rootDependencies: ["undici", "pino"],
files: [
{
name: "undici-HXPKCIY3.js",
source: banner("../../node_modules/undici/lib/core/symbols.js"),
},
],
});
assert.equal(failures.length, 1);
assert.match(failures[0], /"undici" is declared external but was inlined/);
});
test("does not flag a deliberately inlined package that is not a root dependency", () => {
// The TUI inlines `ink-form` / `ink-scroll-view` on purpose (#1952). They are
// declared in the TUI's own manifest, never at the root, which is exactly
// what keeps them out of the candidate set.
assert.deepEqual(
evaluateClient({
name: "tui",
build: "clients/tui/build",
externals: ["react", "ink"],
rootDependencies: ["react"],
files: [
{
name: "index.js",
source: banner("../../node_modules/ink-form/lib/Form.js"),
},
],
}),
[],
);
});
test("passes a clean bundle", () => {
assert.deepEqual(
evaluateClient({
name: "web",
build: "clients/web/build",
externals: ["undici"],
rootDependencies: ["undici"],
files: clean,
}),
[],