feat(compile): scope --bundle npm embed to packages actually reached (#34532)
After that PR the binary was correct but wasteful: if
any CJS package or native addon survived bundling, we shipped the entire
resolved npm tree. The lodash hello-world embedded 11.6 MB of payload;
`@node-rs/crc32` embedded 10.6 MB.
This reuses the data the path-rewriter already has. Every absolute path
it rewrote in the bundle, and in each worker bundle, is the runtime
require target of a CJS module reachable from the entrypoint. The binary
writer maps each back to its owning npm package by longest-prefix folder
match, walks the dependency closure so packages required transitively at
runtime stay too, and embeds only those `<.deno>/<id>/node_modules/`
directories.
Embedding the `<id>/node_modules/` parent rather than the canonical
package folder is load-bearing for the NAPI-RS layout: the deno linker
creates sibling symlinks for platform-specific optional packages
(`@node-rs/crc32-darwin-arm64` lives as a symlink next to
`@node-rs/crc32`), and walking just the canonical folder leaves those
out, so the require chain breaks at the platform binary.
Not every rewritten path lives inside a node_modules tree. The
CJS-from-ESM wrapper also externalizes local project files imported from
ESM (a sibling .cjs/.js required at runtime) into
`require2(__internalResolveBundlePath("..."))` rather than inlining
them. npm packages survive because their tree is embedded, but local
files were never embedded, so the compiled binary failed at runtime with
"Cannot find module". Referenced paths outside any node_modules tree are
treated as local files and added to the compile include set, so they
ship in the VFS at their real path, which is where
`__internalResolveBundlePath` looks at runtime. Covered by the
`local_cjs_file` spec test.
Real reductions on the same fixtures from the earlier bundle PRs:
- lodash hello-world: 11.62 MB to 1.45 MB embedded payload (~8x)
- `@node-rs/crc32` hello-world: 10.62 MB to 468 KB embedded payload
(~23x)
- chalk hello-world (pure ESM): unchanged at 17 KB
The full resolution snapshot still ships in metadata so node-module
resolution by name works at runtime; only the on-disk content shrinks.
BYONM keeps the prior whole-tree behavior, since precise scoping there
needs workspace-layout awareness and is left to a follow-up. Because
package collection only returns None for BYONM (already handled on its
own resolver arm), the managed arm uses a let-else with an unreachable
invariant rather than a dead whole-tree fallback. B
Bartek Iwańczuk committed
61f96bf1b6114fb012c35df25de0db4aca7d6f55
Parent: be62645
Committed by GitHub <noreply@github.com>
on 6/1/2026, 7:38:31 PM