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:
parent
2f83549bc3
commit
dbe95e58ed
2 changed files with 87 additions and 10 deletions
|
|
@ -136,14 +136,32 @@ export function findInlinedExternals({ externals, files }) {
|
|||
/**
|
||||
* The per-client verdict, as pure data so every branch is unit-testable.
|
||||
*
|
||||
* Note it **fails closed on an empty `external` list**. `parseExternals` keys
|
||||
* off the literal text `external: [`, so an ordinary tsup refactor — hoisting
|
||||
* the array to a constant, or a line break after the colon — would make it
|
||||
* return nothing, and a guard that checks zero packages reports success. Every
|
||||
* enrolled bundle has externals today, so "none parsed" means the parser lost
|
||||
* track of the config, not that the invariant holds.
|
||||
* The candidate set is the **union of two independently-derived lists**, and the
|
||||
* second is what makes this guard cover #2067 rather than merely its regression:
|
||||
*
|
||||
* - `externals` — what the client's tsup config declares. Catches a package that
|
||||
* was meant to stay out and got pulled in anyway.
|
||||
* - `rootDependencies` — the root manifest's runtime dependencies. These are
|
||||
* root-declared *by rule* (see AGENTS.md), reached through `core/`, and must be
|
||||
* external in every client bundle. Deriving them independently is essential,
|
||||
* because #2067 was a **missing** declaration: `undici` was absent from the web
|
||||
* and TUI `external` lists, so a check driven only by those lists would have
|
||||
* looked at the inlined 1.05MB bundle and reported success. Removing a package
|
||||
* from an `external` list must not be able to remove it from scrutiny.
|
||||
*
|
||||
* It also **fails closed on an empty `external` list**. `parseExternals` keys off
|
||||
* the literal text `external: [`, so an ordinary tsup refactor — hoisting the
|
||||
* array to a constant, or a line break after the colon — would make it return
|
||||
* nothing. Every enrolled bundle has externals today, so "none parsed" means the
|
||||
* parser lost track of the config, not that the invariant holds.
|
||||
*/
|
||||
export function evaluateClient({ name, build, externals, files }) {
|
||||
export function evaluateClient({
|
||||
name,
|
||||
build,
|
||||
externals,
|
||||
rootDependencies = [],
|
||||
files,
|
||||
}) {
|
||||
if (externals.length === 0) {
|
||||
return [
|
||||
`${name}: no \`external\` entries could be parsed from its tsup config. ` +
|
||||
|
|
@ -153,7 +171,11 @@ export function evaluateClient({ name, build, externals, files }) {
|
|||
];
|
||||
}
|
||||
|
||||
const { noBanners, violations } = findInlinedExternals({ externals, files });
|
||||
const candidates = [...new Set([...externals, ...rootDependencies])];
|
||||
const { noBanners, violations } = findInlinedExternals({
|
||||
externals: candidates,
|
||||
files,
|
||||
});
|
||||
if (noBanners) {
|
||||
return [
|
||||
`${name}: ${build} has no esbuild module banners, so this guard cannot see ` +
|
||||
|
|
@ -169,6 +191,10 @@ export function evaluateClient({ name, build, externals, files }) {
|
|||
|
||||
function main() {
|
||||
const failures = [];
|
||||
const rootDependencies = Object.keys(
|
||||
JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"))
|
||||
.dependencies ?? {},
|
||||
);
|
||||
for (const client of BUNDLED_CLIENTS) {
|
||||
const buildDir = join(repoRoot, client.build);
|
||||
const entry = join(buildDir, "index.js");
|
||||
|
|
@ -192,6 +218,7 @@ function main() {
|
|||
name: client.name,
|
||||
build: client.build,
|
||||
externals,
|
||||
rootDependencies,
|
||||
files,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
[],
|
||||
|
|
|
|||
Loading…
Reference in a new issue