Skip to content

build: compile on one Rspack engine with Rslib 1.0 and Rsbuild 2.2 (#566) - #575

Merged
ScriptedAlchemy merged 12 commits into
mainfrom
build/rslib-1
Sep 5, 2026
Merged

build: compile on one Rspack engine with Rslib 1.0 and Rsbuild 2.2 (#566)#575
ScriptedAlchemy merged 12 commits into
mainfrom
build/rslib-1

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Closes the two P1 findings of #566 Section 1: agent-bundle now builds on one Rspack engine, and the Rslib v0→v1 migration blockers specific to this repo are resolved with tests.

Versions

Package Before After
@rslib/core 0.23.2 1.0.0
@rsbuild/core 2.2.1 (direct) + 2.1.13 (inside Rslib) 2.2.3 (one copy)
@rspack/core / @rspack/binding-* 2.2.1 + 2.1.10 (two native bindings) 2.2.2 (one)
@rstest/* 0.11.10 0.11.12

Bumped in root, packages/{agent-bundle,rsc-runtime,rsc-markdown-stream,create-agent-bundle,workbench}, every examples/*, and the three create-agent-bundle templates. packages/rsc-runtime devDependency @rspack/core moves 2.2.12.2.2 in lockstep with what @rsbuild/core@2.2.3 resolves (~2.2.2); it is pinned exactly only because every devDependency in this repo is (no peer requires it — react-server-dom-rspack@0.1.0 peers ^2.2.0-0). pnpm-workspace.yaml drops the @rsbuild/core@2.2.1 release-age exclusion and the effect-rstest>@rstest/core peer override (0.11.12 satisfies effect-rstest's ^0.11.11 natively). Lockfile: one @rspack/core@2.2.2, one @rsbuild/core@2.2.3, one @rslib/core@1.0.0.

@rstest/adapter-rslib@0.11.12 peers @rslib/core: ">=0.18.6 || ^1.0.0-0"; Rstest v0.11.12 (2026-09-03) ships "chore(deps): update Rslib to 1.0.0 (web-infra-dev/rstest#1778)".

Migration blockers (rslib.rs/guide/upgrade/v0-to-v1)

Blocker Resolution
new URL('./…', import.meta.url) in the compiler's own build Rewritten to fileURLToPath + path.join: src/build/entry-shell.ts:45 (runtimeModulePath, cliEntryRuntimePath delegates), src/build/entries.ts:58, src/host-contracts/codex-plugin-validation.ts:214, src/dev/playground/lifecycle-replay-service.ts:192, packages/create-agent-bundle/src/scaffold-cli.ts:65.
Generated entries emit new URL("../", import.meta.url) / new Worker(new URL('./<worker>.mjs', import.meta.url)) into user builds Parser opt-out, not rspackIgnore comments, so the generated code stays readable: src/build/rslib.ts:577 sets module.parser.javascript.{url,worker}: false in enforceInvariants, and preserveResourceReferences (rslib.ts:70, wired at :713 via tools.bundlerChain) turns off Rslib 1's own rslib:new-url rule, whose rule-level parser option would otherwise outrank the global one. Proven by tests/build.test.ts:1201 (every URL/Worker expression survives verbatim, no asset emitted), the projection pool, and the packed harness.
externalsType default module-importmodern-module Kept at Rslib 1's default in both packages/agent-bundle/rslib.config.ts:88 and the compiler profile (rslib.ts:694), with the reasoning in code comments: the only CommonJS require()s of externals are Node builtins inside bundled dependencies (the TypeScript parser; yaml), and modern-module reproduces the createRequire() shim chunk 0.x emitted for them byte for byte (dist/6872.js before ≡ dist/6686.js after), whereas pinning module-import would have changed them into hoisted import * as namespaces. No createRequire parser flag is needed: the shim chunk is reachable only from the parser chunk, never from the runtime entries the compiler re-bundles into plugin artifacts. createRequire shim counts in the built example artifacts are identical before/after.
autoExternal: falseoutput: { autoExternal: false } src/build/rslib.ts:685. Asserted, not assumed (see below).
rslib.inspectConfig() without mode inspectProductionConfig (rslib.ts:83, used at :826) passes { mode: 'production' } and restores the process's NODE_ENV afterwards (Rslib's inspectConfig writes it). Test: tests/build.test.ts:1166 runs a build under NODE_ENV=development, asserts one bundler config was lowered and NODE_ENV is unchanged. src/build/mcp-apps.ts already passed the mode.
dts: true under TypeScript 7 → tsgo, redirect.dts.extension on See "Types" below: file sets identical, every changed line is a .ts.js specifier rewrite.
syntax: 'es2022' explicit Confirmed in packages/agent-bundle/rslib.config.ts, the compiler profile (rslib.ts), packages/rsc-runtime, packages/rsc-markdown-stream, packages/create-agent-bundle; engines.node inference does not change output.

Also: the compiler profile sets performance.buildCache: false (rslib.ts:668) — Rslib 1 defaults the persistent cache on, and a plugin build must be a function of its inputs, not of the previous run.

Self-contained plugin output

The contract: every dependency of a generated executable is bundled; Node builtins are the only external modules; the framework never adds externals to a plugin build. Verified three ways:

  1. Lowered configtests/self-contained-bundler-config.test.ts (integration pool) builds a fixture executable that imports node:path and yaml, captures inspection.origin.{environmentConfigs,bundlerConfigs}, and asserts: composed lib bundle: true, splitChunks: false, output.autoExternal: false, no top-level autoExternal, no output.externals; lowered output.autoExternal === false survives normalization; every externals declaration on both the environment and the Rspack config is a Node builtin name, /^node:/, or pnpapi (Rslib's own node-target list, 56 declarations; no function, no object map, no package name); externalsType === 'modern-module'; output.asyncChunks === false; the emitted bundle's only imports are node:path and node:module (the createRequire shim), yaml is inlined, and dist/ holds exactly one file. Note: the lowered optimization.splitChunks under Rslib 1 is { chunks: 'async' }, not false — Rslib's ESM layer writes that over Rsbuild's lowering; with asyncChunks: false there is nothing to split, which the readdir assertion proves directly.
  2. MCP App views — same file: composeMcpAppsRsbuildConfigcreateRsbuild().inspectConfig({ mode: 'production' }); environment splitChunks: false, inlineScripts: true, inlineStyles: true, output.externals empty, no autoExternal; the Rspack config has no externals and asyncChunks: false. The comprehensive example's built MCP App output is byte-identical before/after.
  3. Packed pool, emitted JStests/packed-consumer.test.ts (emittedModuleReport, line 86) walks every .js/.mjs a real consumer builds from the installed tarball (all three host trees of the artifact plus the package build's dist/), lexes them with es-module-lexer, and fails on any non-literal dynamic import, any bare non-builtin specifier, or any relative specifier that does not resolve to a file inside the same tree; it also checks @modelcontextprotocol/server is inlined into the MCP bundle. Externals found: node:fs, node:path, node:process, node:url, node:child_process, node:crypto, node:fs/promises, node:os, node:module, node:readline, node:stream — nothing else. The same rule is enforced inside every build by AB6005 (src/build/validate-artifact-modules.ts); the author's own tools hatch is the only way a non-builtin may remain external.

The plugin-build lowered configs differ from 0.23 only in ways the write-up below attributes; in particular the 0.23 externals array carried a Rslib-internal warn-only function (composeExternalsWarnConfig, deleted upstream) and never a package external.

One-engine guard (agent-bundle's own dependency tree)

tests/public-api-packed.test.ts:168 installs the packed tarball into a scratch consumer and walks its node_modules: exactly one @rspack/core, one @rsbuild/core, one @rslib/core, one @rspack/binding-*, with the binding's version equal to @rspack/core's. Distinct from the plugin-output assertions above; a future split like the rsc-runtime pin drifting from Rsbuild's ~ range fails this test.

Before/after evidence

Full write-up (every hunk of both lowered-config diffs attributed to a cause with Rslib/Rsbuild source references): /tmp/rslib1-after/diff-writeup.md on the author's machine; summary:

Difference Where Cause Impact
cache.buildDependencies + rslib.config.ts package build Rslib 1 forwards _privateMeta to Rsbuild config edits now invalidate the persistent cache
function external removed both Rslib 0.23 warn-only composeExternalsWarnConfig deleted none (never externalized)
26 regex+string externals → 26 escaped (?:$|[/\\]) regexes after builtins package build autoExternal now implemented by Rsbuild output.autoExternal none (same matches)
parser.javascript.worker: false{ url: 'new-url-relative' }; JS-rule parser.url: falserslib:new-url rule package build Rslib 1 asset/worker handling sources rewritten to fileURLToPath + join
global url: false, worker: false; rslib:new-url rule parser url: false plugin builds compiler enforceInvariants + preserveResourceReferences pins 0.23 behavior
splitChunks.minSize: 0 package build Rsbuild 2.2 server default surfaced by Rslib 1 none observed
output.filename fn, chunkFilename [id]~0; runtimeChunk: undefined dropped plugin builds Rslib 1 multi-compiler naming inert (asyncChunks: false)
DefinePlugin loses import.meta.env.*, process.env.BASE_URL, process.env.ASSET_PREFIX both Rslib 1 removeEnvPresetDefines none — no source, example, or doc uses them
autoCjsNodeBuiltin: true removed; externalsTypemodern-module both Rslib 1 default createRequire shim identical
lib.autoExternaloutput.autoExternal compiler input upgrade guide none

dist (/tmp/rslib1-{before,after}/dist-*.txt): agent-bundle 411 → 411 files, −2,333 B (−0.015%), one chunk id renamed (6872.js6686.js, same 808 B — the crypto builtin shim); @agent-bundle/runtime 60 → 60 files, +20 B; measured before #571, its shared chunks carried Rslib 1's multi-compiler ~0 suffix — moot now that #571 builds the runtime as one lib entry (chunk names are plain ids again on the merged head); rsc-markdown-stream byte-identical; create-agent-bundle scaffold-cli.js −463 B. Built example artifacts: hooks-and-scripts and comprehensive byte-identical; host-test differs only in embedded pnpm store path strings and Rspack 2.2.2 concatenated-module identifier suffixes.

Types (attw, publint): .d.ts file sets identical (333/333 agent-bundle, 48/48 runtime); 263 + 1 files changed, all 984 changed lines are relative specifier rewrites .ts.js (static imports, re-exports, inline import("…") type paths); after normalizing that, diff -r is empty — tsgo introduced no formatting, ordering, or comment differences. One cosmetic residue: four adapter declarations keep declare module '../core/types.ts' un-rewritten while their imports say .js; tsc --traceResolution resolves both spellings to dist/core/types.d.ts and a strict consumer typecheck shows the AgentBundleConfigExtensions augmentation lands identically before/after. attw --pack --profile esm-only: byte-identical output, 13/13 entry points green (node16 from ESM, bundler). publint 0.3.24: "All good!" for agent-bundle (identical to baseline), @agent-bundle/runtime, rsc-markdown-stream, create-agent-bundle.

Rsdoctor (@rsdoctor/rspack-plugin 1.6.3 on the agent-bundle package build, run from a lane-only config outside the repo; nothing committed): 109 assets / 16.61 MiB total, of which the library is 78 files / 13.81 MiB (gzip 2.37 MB) and the copied Workbench 31 files / 2.80 MiB; 333 .d.ts; 884 modules (297 src TS, 34 JSON, 94 zod, 1 typescript, 458 external references); 21 entrypoints; no duplicate packages. Top chunks: the bundled TypeScript 5.9.3 parser dist/1178.js 10.57 MB (73% of library bytes, 1.58 MB gzip; a static import of api, index, eval, config), 9923.js 1.58 MB (dev/build/eval, 355 modules), 1802.js 440 kB (adapters/install), 8954.js 227 kB (config/skills/schemas), 2649.js 191 kB (zod). Cold-cache compile ≈ 11.8 s of Rspack phases inside a 13.6 s process (builtin:swc-loader on 297 files is the only loader; tsgo DTS 2.9 s in parallel). Flags: 8 × E1007 side-effects-only on node:* externals (false-positive class), one Rspack top-level-await chunk-cycle warning. Pre-existing follow-ups, out of scope here: lazy-load the parser at its five import sites, minify that chunk (1.5 MB is pure re-indentation), decide zod bundling policy.

Gates run on this branch state

pnpm install --frozen-lockfile, pnpm build, pnpm typecheck, pnpm lint, pnpm test:unit (3590 passed), pnpm test:route-unit (83), pnpm test:projection (172), pnpm test:integration:run (1072 passed, 33 skipped), pnpm test:packed (28 passed, 1 skipped), pnpm --filter './examples/*' check (all examples build and test on Rslib 1.0), pnpm docs:site:build (1906 pages, parity and dead-link checks green).

Docs

docs/entry-conventions.md, docs/effect-conventions.md (parked @rslib/core 0.23.2 pin retired), website/docs/{en,zh}/guide/authoring/package-entries.mdx, website/docs/{en,zh}/reference/configuration.mdx, website/docs/{en,zh}/reference/cli.mdx, examples/rsc-agent-runtime/README.md: the "two bundler engine copies" caveat becomes the one-engine statement, the URL/Worker preservation invariant is stated, and inspect --bundler is documented as lowering in production mode regardless of NODE_ENV. Changeset: .changeset/rslib-1.md (agent-bundle, @agent-bundle/runtime, create-agent-bundle: patch).

Merged origin/main after #568, #570, #571 (as requested) and re-ran every gate on the merged head: build, typecheck, lint, unit (3637), integration (1073), packed (29), projection (172), lint:release (attw ×3 + declaration-import check), docs:site:build. packages/rsc-runtime's @rspack/core devDependency is 2.2.2, in lockstep with @rsbuild/core@2.2.3's ~2.2.2; the lockfile carries one @rspack/core.

Self-review

Reviewer: change-risk-reviewer (gpt-5.6-sol-medium) could not start — its TraceDecay MCP dependency failed discovery — so per AGENTS.md the review ran as a generalPurpose subagent on the same model (gpt-5.6-sol-medium), asked for concrete merge risks only against origin/main...HEAD, with eleven targeted checks (NODE_ENV restore, global parser.url side effects, rslib:new-url rule name and hatch ordering, path-rewrite equivalence, bundled scaffold-cli ownDirectory, changeset packages, en/zh parity, engine-guard platform fragility, buildCache scope, inspect snapshots, dead modules).

Findings and dispositions:

  1. src/build/rslib.ts inspectProductionConfig — should-fix: when NODE_ENV was unset and the inspection threw, the process was left at production. Fixed in 261ff7a: the finally now deletes NODE_ENV when it was unset (restores it otherwise), and tests/build.test.ts gained leaves NODE_ENV unset when a failing inspection had set it. End state of a successful buildWithRslib is unchanged (rslib.build() sets production when it finds NODE_ENV unset, as 0.23 did).
  2. tests/self-contained-bundler-config.test.ts:74 — should-fix: the "Node builtins only" predicate also accepts pnpapi. Dismissed: pnpapi is part of Rslib's own node-target external list (and webpack/Rspack's node externals preset) — a virtual module that exists only inside a Yarn PnP runtime, names no package, and cannot be bundled. The framework adds nothing; the test's comment states the exception, and the packed emitted-JS walk confirms no pnpapi import is emitted.

All other checks: no risk found (the reviewer confirmed the rslib:new-url rule name against Rslib 1.0's dist, that user tools.bundlerChain hatches run before the invariant, that every rewritten candidate path equals the former URL resolution in both dist/ and .ts layouts, that @rspack/binding-* optional dependencies carry os/cpu/libc filters so one binding installs, and that the lockfile carries one version each of Rspack/Rsbuild/Rslib).

Second pass (same reviewer, after 261ff7a): no further findings; the pnpapi dismissal accepted.

Codex review thread (P2, website/docs/en/reference/configuration.mdx:190): the exported AgentBundleToolsConfig TSDoc in src/core/types.ts still described two engine copies and would render into the generated API pages. Fixed: the TSDoc now states the one-engine reality and the engine-identity caveat; the two internal comments referring to the old "dual-engine reality" (src/build/meta.ts, src/build/rslib.ts) were updated in the same commit. Replied on the thread.

)

Bump @rslib/core to 1.0.0 and @rsbuild/core to 2.2.3 in every workspace
package, example, and scaffolder template, and @rstest/* to 0.11.12, so a
consumer installs one @rspack/core (2.2.2) and one native binding.

Compiler (src/build/rslib.ts): output.autoExternal: false (v1 form); URL
and worker parsing off for plugin builds so generated `new URL(…)` and
`new Worker(new URL(…))` expressions survive verbatim; inspectConfig runs
in production mode with NODE_ENV restored; persistent cache off.
externalsType stays Rslib's ESM default, which reproduces the 0.x
createRequire shim for CommonJS requires of Node builtins.

Own sources resolve sibling modules with fileURLToPath + path.join
instead of new URL(…, import.meta.url).

Tests: lowered-config and packed assertions that plugin output is
self-contained (Node builtins the only externals), a packed guard for a
single Rspack engine, and inspect coverage under NODE_ENV=development.
@changeset-bot

changeset-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: eec78a4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
agent-bundle Patch
@agent-bundle/runtime Patch
create-agent-bundle Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T01:27:48.419492Z e9deb59 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e9deb59fb9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +188 to +190
The hatch executes on one bundler engine — the `@rspack/core` behind agent-bundle's pinned
`@rsbuild/core`, shared by the Rslib-compiled outputs (artifact scripts, MCP entries, hooks, the
package build) and the MCP App views — but that engine is agent-bundle's dependency, not your

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the public tools TSDoc to match the one-engine claim

This new one-engine documentation conflicts with the exported AgentBundleToolsConfig TSDoc in src/core/types.ts, which still says the hatch runs under two engine copies, names Rslib's nested 2.1.x stack, and will be rendered into the generated public API pages. Users consulting that API reference therefore receive the opposite engine-identity guidance; update the source TSDoc as part of this migration so the generated and handwritten documentation agree.

AGENTS.md reference: AGENTS.md:L78-L85

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3bcaa47: the AgentBundleToolsConfig TSDoc in src/core/types.ts now states the one-engine reality (and the engine-identity caveat: the engine is agent-bundle's dependency, so hatch authors still use the { rspack } utils argument rather than an imported @rspack/core). The two internal comments that pointed at the old "dual-engine reality" (src/build/meta.ts, src/build/rslib.ts) were updated in the same commit.

@pkg-pr-new

pkg-pr-new Bot commented Sep 5, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@575
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@575
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/rsc-markdown-stream@575
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@575

commit: eec78a4

@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) September 5, 2026 02:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant