Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/591-ab6005-module-loads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": minor
---

Hold every emitted JavaScript module to `AB6005` for what it loads, not only for what it imports: `agent-bundle build`, `agent-bundle validate`, and `agent-bundle prepack` now fail a host-pack module or a package build `dist` bundle whose code calls `require("pkg")`, `require.resolve("pkg")`, `createRequire(…)("pkg")` or `.resolve("pkg")` — the factory written out, namespace-qualified, aliased (`import { createRequire as mk }`), or bound to a name first (`const load = createRequire(import.meta.url); load("pkg")`, the shim Rspack emits for a `tools.rspack` `externalsType: 'node-commonjs'` external) — or `import.meta.resolve("pkg")` with a bare specifier that is not a Node built-in; a non-literal argument to any of those calls and a loader passed on as a value rather than called (`const l = require`, `fn(load)`) fail the same way. Node built-ins under either spelling still pass; a relative or `file:` target must be a listed regular `.js`/`.mjs` file inside the tree, which is walked, or, in a host pack, listed valid JSON, which is accepted; prebuilt payload modules stay untouched. Each diagnostic names the call — `… uses unsupported specifier "left-pad" in require("left-pad").`, `… loads a non-literal specifier through load(…), a createRequire(…) loader.` — and the import messages are unchanged. The prepack gate's `require`/`createRequire`/`import.meta.resolve` evidence for `AB7014` comes from the same scanner, which reads code only: a `require("pkg")` inside a comment, string, regular-expression literal, or the text of a template no longer counts (a load inside a template's `${…}` substitution does), so a dependency a packed file mentions only in a docblock is reported as unused, and the `AB7014` recovery names where load evidence can still come from (a prebuilt payload module, packed JavaScript the `files` allowlist adds from outside the artifact and `dist`, a packed declaration reference, an install script, or a `bin` command). `agent-bundle/serve-app-command` and the build's dependency-root discovery locate a dependency only through `node_modules` at the project root or an ancestor; `NODE_PATH`, Node's global module folders, and Yarn Plug'n'Play are no longer consulted. (#602)
64 changes: 47 additions & 17 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,34 +71,64 @@
every dependency of a generated executable — `output.autoExternal: false`,
`bundle: true`, `splitChunks: false`, no `externals`. Rslib's `node` target
leaves only Node built-ins (and `pnpapi`) external, and the only bare
import specifiers `AB6005` accepts in a host-pack module are Node built-ins. The
specifiers `AB6005` accepts in a host-pack module are Node built-ins. The
package build's `dist` bundles are walked by the same `AB6005` rule
(`src/build/package-build.ts` reuses `validateJavaScriptModules` from
`src/build/validate-artifact-modules.ts`), so a generated executable in a
host pack or in `dist` imports nothing but Node built-ins from outside its
tree. The walk reads import specifiers, static and literal dynamic; a
`createRequire(…)(…)` or `import.meta.resolve(…)` call is not an import and
is outside `AB6005` in either output — the prepack gate reads those calls
as dependency evidence. MCP App views (`src/build/mcp-apps.ts`) inline every script and style
into one HTML file. The framework never adds `externals` to a plugin build;
host pack or in `dist` loads nothing but Node built-ins from outside its
tree. The walk reads the recognised load forms in an emitted
`.js`/`.mjs` module (prebuilt payload modules excepted, `.d.ts` never
walked): its ES import records — static, literal dynamic `import()`; a
non-literal one is a finding — and `require(…)`, `require.resolve(…)`,
`createRequire(…)(…)` and `.resolve(…)` with the factory written inline,
namespace-qualified, or imported/destructured under an alias, a loader
declared with `const`/`let`/`var` from `createRequire(…)` and then called
directly or through `.resolve(…)`, and `import.meta.resolve(…)`; optional
calls and a trailing comma after the literal count the same. Comments,
strings, regular-expression literals, and template text are stepped over;
`${…}` template substitutions are code and are scanned; binding and alias
names are read from code only. A Node built-in passes; a relative or
`file:` target must be a listed regular `.js`/`.mjs` file inside the tree
and is walked in turn (in a host pack only, listed valid JSON is accepted
as a terminal and not walked). A bare package name fails, a non-literal
argument fails, and a loader used as a value rather than called fails —
a binding position (parameter, `catch`, destructuring pattern, import
specifier) is not a use as a value; a default initializer (`x = require`)
is. One scanner,
`src/build/module-loads.ts`, reads those loads for `AB6005` and for the
prepack gate alike; its header states the approximations it makes (a `/`
after `)` or an identifier is division, and the hand-authored forms it
does not recognise: `.call`/`.apply`, `globalThis.require`,
`module.require`, `import.meta["resolve"]`, assignment-bound or
second-hop loader aliases). MCP App
views (`src/build/mcp-apps.ts`) inline every script and style into one
HTML file. The framework never adds `externals` to a plugin build;
the `externals` handling in `rslib.ts` (`reservedExternalsViolation`,
`guardReservedExternals`) only rejects reserved specifiers in the resolved
externals, which come from the author's `tools` hatch and Rslib's built-in
list, never from the profile.
- No refactor, toolchain upgrade, or "leaner install" change may enable
`autoExternal` or externalize a dependency on the author's behalf. A package
a consumer must install is the author's explicit decision, and an import
kept external through the `tools` hatch is not a way to make it anywhere:
`AB6005` fails such an import in a host pack and in `dist` alike. What
legitimately puts a package under `dependencies` is a packed declaration
reference, a prebuilt payload module that imports it, an install script,
or a `bin` command packed JavaScript runs — and the prepack gate judges
those: `AB7014` demands that evidence, `AB7015` a specifier a consumer's
npm can install.
a consumer must install is the author's explicit decision, and keeping a
dependency external through the `tools` hatch is not how that decision is
made, in any emitted form: an ES `import` external and the
`node-commonjs` `createRequire` shim both fail `AB6005` in a host pack and
in `dist` alike; a direct `require("pkg")` call, however emitted, is
rejected the same way. `AB7014` lexes every packed
`.js`/`.mjs`/`.cjs` file, including `dist` and artifact files. Because
`AB6005` has already refused a bare load in every walked emitted module
before that inventory runs, the evidence that can still keep a dependency
in a build that passed comes from a prebuilt payload module, packed
JavaScript the `files` allowlist adds from outside the artifact and
`dist`, a packed declaration reference, an install script, or a `bin`
command. The prepack gate judges those: `AB7014` demands that evidence,
`AB7015` a specifier a consumer's npm can install.
- Proof is bytes and processes, not config: every artifact build walks the
compiled host-pack modules and every package build walks its emitted `dist`
bundles (`AB6005` fails a bare package specifier in either), the prepack
gate then judges what remains declared, and the packed pool
bundles (`AB6005` fails a bare package specifier in either, imported or
loaded through `require`, a `createRequire(…)` loader, or
`import.meta.resolve`), the prepack gate then judges what remains
declared, and the packed pool
(`pnpm test:packed`) installs the packed tarball into a clean consumer,
builds, removes the project source, and spawns the generated entry as a
real process (`packed-deleted-source`).
Expand Down
Loading
Loading