Skip to content

feat(bundler): per-provider tree-shake for vendor chunks - #179

Merged
lukekania merged 2 commits into
milestone/v0.11.0-builder-parityfrom
feat/bundler-per-provider-shake
May 18, 2026
Merged

feat(bundler): per-provider tree-shake for vendor chunks#179
lukekania merged 2 commits into
milestone/v0.11.0-builder-parityfrom
feat/bundler-per-provider-shake

Conversation

@lukekania

Copy link
Copy Markdown
Owner

Part of the v0.11.0 builder-parity milestone (issue 171 in the tracker stays open until the milestone branch lands).

Summary

  • shake::collect_cross_chunk_used_names_per_provider returns a Vec<HashSet<String>> indexed by chunk index; for each chunk i it collects the names other chunks import from any module in i. Reuses the existing analyze_module parse + namespace-import expansion paths, adding a specifier → canonical-path fallback so bare npm specifiers ('@angular/core') attribute to the owning vendor chunk instead of falling through.
  • bundle() derives that specifier-to-path map from the existing all_file_to_ns / specifier_to_namespace tables, then feeds externally_used_per_chunk[idx] into analyze_unused_exports for every chunk — the if is_main gate is gone. Vendor and lazy chunks now get the same cross-chunk consumption signal main has had since PR 131.
  • npm_wrap::wrap_npm_module now accepts unused_exports and drops both the unused export const X = ... declarations and the matching __exports.X = ns.X re-export bridges, so shake decisions actually reach the emitted vendor chunk code (previously they died at the IIFE wrapper).

The old single-provider collect_cross_chunk_used_names and its expand_namespace_imports helper are removed — _per_provider subsumes them and computing the main-chunk set is just result[0].

Behaviour delta

On the test-ng-project fixture (production build), the largest vendor chunk shrinks from ~444 KB to ~416 KB. The gain on real-world @angular/core is modest because most of its exports are referenced by some module inside the package's own internal graph (so intra-chunk reachability keeps them alive even without entry-walk pinning); the bigger wins were left on the table by the old externally_used = None fallback when the package shape made the entry walk effectively reach everything. The unit and integration tests verify the mechanism end-to-end on a synthetic package with a true cross-chunk-only export.

Tests

  • shake::tests::test_collect_cross_chunk_used_names_per_provider_multi_chunk — three-chunk graph (main/lazy/vendor), asserts per-provider sets are attributed correctly and unused names don't leak into the wrong chunk.
  • shake::tests::test_collect_cross_chunk_used_names_per_provider_dotted_filename — port of the previous dotted-filename regression onto the new API.
  • npm_wrap::tests::test_wrap_drops_unused_declaration and ..._reexport_bridge — unit-level proof that wrap_npm_module honours unused_exports.
  • vendor_chunk_splitting_integration::vendor_chunk_drops_unreferenced_exports — full pipeline: a vendor chunk whose npm package re-exports two names from an impl.js declaration file emits only the consumed one in its bundled body.

Test plan

  • cargo build --release -p ngc-rs
  • cargo test -p ngc-bundler (97 tests pass)
  • cargo clippy --workspace -- -D warnings
  • End-to-end: production npx ng build against test-ng-project succeeds, vendor chunk shrinks measurably

@lukekania lukekania mentioned this pull request May 18, 2026
lukekania added 2 commits May 18, 2026 11:55
Generalize cross-chunk used-names collection so every chunk gets its own
externally-used set, not just main. Vendor chunks holding `@angular/core`
or `rxjs` previously pinned every export the package declared because
`externally_used = None` made the shaker fall back to entry-walk
reachability — on `index.mjs`-style packages that reaches almost
everything.

- `shake::collect_cross_chunk_used_names_per_provider` returns
  `Vec<HashSet<String>>` indexed by chunk index; for each chunk i it
  collects the names other chunks import from any module in i.
- `bundle()` builds a bare-specifier → canonical-path map from the
  existing namespace tables so bare imports (`'@angular/core'`) attribute
  to the owning vendor chunk, then feeds `externally_used_per_chunk[idx]`
  into `analyze_unused_exports` for every chunk — the `is_main` gate is
  dropped.
- `npm_wrap::wrap_npm_module` now accepts `unused_exports` and drops both
  the unused `export const X = ...` declarations and the matching
  `__exports.X = ns.X` re-export bridges, so shake decisions reach the
  emitted vendor chunk code.

Bumps version to 0.10.13.
@lukekania
lukekania force-pushed the feat/bundler-per-provider-shake branch from da39973 to 05b30d0 Compare May 18, 2026 09:56
@lukekania
lukekania merged commit 46f03b5 into milestone/v0.11.0-builder-parity May 18, 2026
6 checks passed
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