Skip to content

Commit 599007c

Browse files
divybotclaudelittledivy
authored
fix(compile): prune managed npm snapshot to graph-reachable packages (#34741)
## Summary `deno compile` used to embed the full managed npm resolution snapshot (everything in the lockfile / package.json). Packages that were resolved (e.g. via `deno.json` `imports` or `package.json` `dependencies`) but never imported by the entrypoint were still bundled into the executable, inflating its size considerably. This PR adds an opt-in flag, `--exclude-unused-npm`, that reduces the resolution snapshot to the closure of packages reachable from npm specifiers in the module graph. Because non-statically-analyzable dynamic imports won't appear in the graph, the optimization is opt-in so the default behavior keeps the full snapshot and continues to work for those cases (as discussed in #29338). Users with such dynamic imports can still pass `--include npm:<pkg>` alongside the flag to force inclusion. The same pruning path is still taken implicitly under `--unstable-npm-lazy-caching` (no change there). ## Test plan - [x] New spec test `tests/specs/compile/compile_exclude_unused_npm` mirrors the existing `unstable_npm_lazy_caching` test but uses the new flag, confirming the unused `@denotest/subtract` package is no longer embedded. - [x] Existing `unstable_npm_lazy_caching` and `npm_pkgs_lockfile_unused` tests continue to express the intended behavior unchanged. Closes #21504 Closes denoland/divybot#437 --------- Co-authored-by: divybot <divybot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Divy Srivastava <me@littledivy.com>
1 parent 4a4983b commit 599007c

8 files changed

Lines changed: 64 additions & 1 deletion

File tree

cli/args/flags.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@ pub struct CompileFlags {
224224
pub bundle: bool,
225225
/// Minify the bundle. Only meaningful with `bundle: true`.
226226
pub minify: bool,
227+
/// Prune the embedded managed npm snapshot to only those packages reachable
228+
/// from npm specifiers in the module graph. Opt-in because non-statically
229+
/// analyzable dynamic imports may not appear in the graph; pass
230+
/// `--include npm:<pkg>` for any such packages.
231+
pub exclude_unused_npm: bool,
227232
}
228233

229234
impl CompileFlags {
@@ -3071,6 +3076,17 @@ On the first invocation of `deno compile`, Deno will download the relevant binar
30713076
.requires("bundle")
30723077
.help_heading(COMPILE_HEADING),
30733078
)
3079+
.arg(
3080+
Arg::new("exclude-unused-npm")
3081+
.long("exclude-unused-npm")
3082+
.help(cstr!("Embed only the npm packages reachable from the module graph (managed npm; no <c>node_modules</> directory).
3083+
<p(245)>Without this flag the full managed npm snapshot from the lockfile / package.json is embedded.
3084+
Reduces binary size when the lockfile contains packages the entrypoint does not import.
3085+
Skips packages that are only reached through non-statically-analyzable dynamic imports;
3086+
pass those with <c>--include npm:<<pkg></> if needed.</>"))
3087+
.action(ArgAction::SetTrue)
3088+
.help_heading(COMPILE_HEADING),
3089+
)
30743090
.arg(watch_arg(false))
30753091
.arg(watch_exclude_arg())
30763092
.arg(no_clear_screen_arg())
@@ -6970,6 +6986,7 @@ fn compile_parse(
69706986
let self_extracting = matches.get_flag("self-extracting");
69716987
let bundle = matches.get_flag("bundle");
69726988
let minify = matches.get_flag("minify");
6989+
let exclude_unused_npm = matches.get_flag("exclude-unused-npm");
69736990
let include = matches
69746991
.remove_many::<String>("include")
69756992
.map(|f| f.collect::<Vec<_>>())
@@ -6996,6 +7013,7 @@ fn compile_parse(
69967013
self_extracting,
69977014
bundle,
69987015
minify,
7016+
exclude_unused_npm,
69997017
});
70007018

70017019
Ok(())
@@ -13700,6 +13718,7 @@ mod tests {
1370013718
self_extracting: false,
1370113719
bundle: false,
1370213720
minify: false,
13721+
exclude_unused_npm: false,
1370313722
}),
1370413723
type_check_mode: TypeCheckMode::Local,
1370513724
code_cache_enabled: true,
@@ -13738,6 +13757,7 @@ mod tests {
1373813757
self_extracting: false,
1373913758
bundle: false,
1374013759
minify: false,
13760+
exclude_unused_npm: false,
1374113761
}),
1374213762
type_check_mode: TypeCheckMode::Local,
1374313763
code_cache_enabled: true,
@@ -13768,6 +13788,7 @@ mod tests {
1376813788
self_extracting: false,
1376913789
bundle: false,
1377013790
minify: false,
13791+
exclude_unused_npm: false,
1377113792
}),
1377213793
import_map_path: Some("import_map.json".to_string()),
1377313794
no_remote: true,
@@ -16369,6 +16390,7 @@ Usage: deno lint [OPTIONS] [files]...\n"
1636916390
self_extracting: false,
1637016391
bundle: false,
1637116392
minify: false,
16393+
exclude_unused_npm: false,
1637216394
}),
1637316395
type_check_mode: TypeCheckMode::Local,
1637416396
preload: svec!["p1.js", "./p2.js"],

cli/standalone/binary.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,13 @@ impl<'a> DenoCompileBinaryWriter<'a> {
434434
CliNpmResolver::Managed(managed) => {
435435
if graph.modules().any(|m| m.npm().is_some()) {
436436
let snapshot = managed.resolution().snapshot();
437-
let snapshot = if self.cli_options.unstable_npm_lazy_caching() {
437+
// When the user opts in (or via the existing unstable lazy-caching
438+
// path), prune the resolution snapshot to packages reachable from
439+
// npm specifiers in the graph. Otherwise embed the full snapshot
440+
// so non-statically-analyzable dynamic imports keep working.
441+
let snapshot = if compile_flags.exclude_unused_npm
442+
|| self.cli_options.unstable_npm_lazy_caching()
443+
{
438444
let reqs = graph
439445
.specifiers()
440446
.filter_map(|(s, _)| {

cli/tools/compile.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,7 @@ mod test {
12691269
self_extracting: false,
12701270
bundle: false,
12711271
minify: false,
1272+
exclude_unused_npm: false,
12721273
},
12731274
&initial_cwd,
12741275
);
@@ -1298,6 +1299,7 @@ mod test {
12981299
self_extracting: false,
12991300
bundle: false,
13001301
minify: false,
1302+
exclude_unused_npm: false,
13011303
},
13021304
&resolve_cwd(None).unwrap(),
13031305
)
@@ -1333,6 +1335,7 @@ mod test {
13331335
self_extracting: false,
13341336
bundle: false,
13351337
minify: false,
1338+
exclude_unused_npm: false,
13361339
},
13371340
&resolve_cwd(None).unwrap(),
13381341
)

cli/tools/installer/global.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ async fn install_global_compiled(
375375
self_extracting: false,
376376
bundle: false,
377377
minify: false,
378+
exclude_unused_npm: false,
378379
};
379380

380381
let mut new_flags = flags.as_ref().clone();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"tempDir": true,
3+
"steps": [
4+
{
5+
"args": "install",
6+
"output": "[WILDCARD]"
7+
},
8+
{
9+
"args": "compile -A --exclude-unused-npm --output out main.ts",
10+
"output": "compile.out"
11+
}
12+
]
13+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[WILDCARD]
2+
3+
Embedded Files
4+
5+
[# Note that this doesn't have @denotest/subtract — only @denotest/add is imported.]
6+
[WILDLINE]
7+
├── .deno_compile_node_modules/localhost/@denotest/add/* ([WILDLINE])
8+
└── main.ts ([WILDLINE])
9+
10+
[WILDCARD]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"imports": {
3+
"@denotest/add": "npm:@denotest/add",
4+
"@denotest/subtract": "npm:@denotest/subtract"
5+
}
6+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { add } from "@denotest/add";
2+
console.log(add);

0 commit comments

Comments
 (0)