Skip to content

feat(plugin-js): js_bundle driver, esm/cjs + node/browser variants - #292

Open
raphaelvigee wants to merge 2 commits into
raphaelvigee/feat-plugin-js-m5-lintfrom
raphaelvigee/feat-plugin-js-ts
Open

feat(plugin-js): js_bundle driver, esm/cjs + node/browser variants#292
raphaelvigee wants to merge 2 commits into
raphaelvigee/feat-plugin-js-m5-lintfrom
raphaelvigee/feat-plugin-js-ts

Conversation

@raphaelvigee

@raphaelvigee raphaelvigee commented Jul 31, 2026

Copy link
Copy Markdown
Member

M6 of the JS/TS heph plugin plan (part 6/6, top of the stack) — the final milestone, plus a first performance baseline for the whole plugin.

A cacheable js_bundle ManagedDriver wrapping esbuild, with format (esm/cjs) and target (node/browser) as plain addr args resolved with a flat default rather than porting the Go plugin's ancestry/universe variant machinery — this plugin's addr model is a single flat workspace, so there's no ancestor chain or cross-subtree variant-pin problem for that machinery to solve (researched and confirmed against crates/plugin-go/src/plugingo/variant.rs and this crate's own flat workspace.rs model before deciding).

Whole-graph cache key by design (the entry point's full transitive closure via ImportGraph::runtime_edges, cross-package recursion unlike js_test's one-hop trim), third-party deps resolved via the same lockfile-driven mechanism as every other driver, tsconfig (plus its extends chain) declared and staged the same way js_typecheck does.

Reviewed by feature-quality/code-quality/hermeticity. Two functional BLOCKERs found and fixed: the discovered third-party import closure was never wired into esbuild's --external flags, so bundling any package with a real npm runtime dependency failed outright; and the output directory didn't vary by format/target, so esm and cjs variants of the same package collided on the same declared output path. A third BLOCKER (missing tsconfig Input) meant path-aliased/JSX/decorator-configured TS entry points silently failed or cached wrong. All three fixed with regression tests, including a real-esbuild end-to-end proof for the external-deps fix.

Explicitly deferred (disclosed in module docs): target=browser doesn't yet change what's resolved (only what esbuild is told to assume) since the resolver has no browser condition/main-field support; a config-reference-scanning helper shared with js_test/js_lint reads a config-referenced file before checking workspace containment rather than after (dormant for js_bundle's only supported config format today, live for js_test already — flagged as a cross-cutting follow-up); rollup/webpack/vite bundlers.

Open item for a future PR: the design doc's v1 scope table lists six drivers including js_format, but the Milestones list only ever defined M0-M6 for the other five (install/typecheck/test/lint/bundle) — js_format has no milestone and isn't built yet.

Performance baseline

Speed was the plugin's stated #1 design goal from the start — this had never actually been measured until now. Added #[ignore]d ad-hoc benches (never run under tst/default cargo test) measuring Provider::import_graph's cold cost (first parse+resolve per package) vs warm cost (the M5 per-package memoization) on synthetic 20- and 150-package workspaces, plus a parse-vs-resolve phase split.

Results (darwin/arm64, this machine only — not transferable to other targets without re-running there):

  • Cold: ~2.5-3ms/package, ~0.16-0.2ms/file (150 packages × 15 files: median 385ms, range 352-679ms across 8 runs — this machine's noise floor is large).
  • Warm: ~500ns total across 150 packages — a cache hit never re-enters the build path at all, confirming M5's fix delivers as designed.
  • Phase split: resolution (oxc_resolver) is 97%+ of cold cost; parsing (oxc_parser) is noise by comparison (~2.6-2.7%).

No prior baseline existed, so this establishes one rather than comparing against one — no regression, no obvious hotspot found. Real gap flagged: there's no JS/TS scenario in crates/bench/crates/bench-corpus (Go-only today), so this path has no standing CI perf-regression coverage yet — building that out is the natural follow-up.

With this PR, M0-M6 of ai-docs/js-plugin-plan.md are all implemented across the stack below.

Test plan

  • cargo build -p plugin-js -p plugin-js-cdylib
  • cargo test -p plugin-js (319 passed, 18 ignored — require real tsc/vitest/jest/oxlint/esbuild binaries not provisioned in this devenv)
  • cargo clippy -p plugin-js -p plugin-js-cdylib --all-targets -- -D warnings (clean)
  • cargo fmt --check -p plugin-js -p plugin-js-cdylib
  • CI (tst, lint) on this PR

🤖 Generated with Claude Code

https://claude.ai/code/session_01M3wZfyPsG8stfRQuybLRjN

M6 of the JS/TS plugin, the final milestone: a cacheable js_bundle
ManagedDriver wrapping esbuild, with format (esm/cjs) and target
(node/browser) as plain addr args resolved with a flat default rather
than porting the Go plugin's ancestry/universe variant machinery — this
plugin's addr model is a single flat workspace, so there's no ancestor
chain or cross-subtree variant-pin problem for that machinery to solve
(researched and confirmed against crates/plugin-go/src/plugingo/variant.rs
and this crate's own flat workspace.rs model before deciding).

Whole-graph cache key by design (the entry point's full transitive
closure via ImportGraph::runtime_edges, cross-package recursion unlike
js_test's one-hop trim), third-party deps resolved via the same
lockfile-driven mechanism as every other driver, tsconfig (plus its
extends chain) declared and staged the same way js_typecheck does.

Reviewed by feature-quality/code-quality/hermeticity. Two functional
BLOCKERs found and fixed: the discovered third-party import closure was
never wired into esbuild's --external flags, so bundling any package with
a real npm runtime dependency failed outright; and the output directory
didn't vary by format/target, so esm and cjs variants of the same package
collided on the same declared output path. A third BLOCKER (missing
tsconfig Input) meant path-aliased/JSX/decorator-configured TS entry
points silently failed or cached wrong. All three fixed with regression
tests, including a real-esbuild end-to-end proof for the external-deps fix.

Explicitly deferred (disclosed in module docs): target=browser doesn't
yet change what's resolved (only what esbuild is told to assume) since
the resolver has no browser condition/main-field support; a
config-reference-scanning helper shared with js_test/js_lint reads a
config-referenced file before checking workspace containment rather than
after (dormant for js_bundle's only supported config format today, live
for js_test already, flagged as a cross-cutting follow-up not specific
to this milestone); rollup/webpack/vite bundlers.

Note for follow-up: the design doc's v1 scope table lists six drivers
including js_format, but the Milestones list only ever defined M0-M6 for
the other five (install/typecheck/test/lint/bundle) — js_format has no
milestone and isn't built. Flagged in plugin-js-cdylib's module doc; needs
either an M7 or a corrected scope table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M3wZfyPsG8stfRQuybLRjN
@raphaelvigee
raphaelvigee changed the base branch from master to raphaelvigee/feat-plugin-js-m5-lint August 5, 2026 00:19
@raphaelvigee raphaelvigee changed the title feat(plugin-js): scaffold JS/TS plugin, hermetic js_install driver feat(plugin-js): js_bundle driver, esm/cjs + node/browser variants Aug 5, 2026
Speed was the plugin's stated #1 design goal from the start
(ai-docs/js-plugin-plan.md) but had never actually been measured. Adds
#[ignore]d ad-hoc benches (never run under tst/cargo test by default) in
importgraph.rs measuring Provider::import_graph's cold cost (first
parse+resolve per package) vs warm cost (M5's per-package memoization) on
synthetic workspaces of 20 and 150 packages, plus a parse-vs-resolve phase
split.

Results (darwin/arm64, this machine only — not transferable to other
targets without re-running there): cold ~2.5-3ms/package, ~0.16-0.2ms/file;
warm ~500ns total across 150 packages (cache hit never re-enters the build
path, confirming M5's fix delivers as designed). Resolution (oxc_resolver)
is 97%+ of cold cost, parsing (oxc_parser) is noise by comparison.

No regression baseline existed before this, so this is establishing one,
not comparing against one. Flagged as a real gap: no JS/TS scenario exists
in crates/bench/crates/bench-corpus (Go-only today), so this path has no
standing CI perf-regression coverage — building that out is the natural
follow-up, left for later rather than folded into this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M3wZfyPsG8stfRQuybLRjN
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