diff --git a/changelog.d/13890-als-seed-dynamic-import.md b/changelog.d/13890-als-seed-dynamic-import.md new file mode 100644 index 00000000000..3e978dc9d4f --- /dev/null +++ b/changelog.d/13890-als-seed-dynamic-import.md @@ -0,0 +1,6 @@ +A program with a dynamic `import()` no longer gets a +`globalThis.AsyncLocalStorage` that node does not have. The entry seeds that +global only for Next.js programs, and every module loaded by `import()` used +to count as a Next.js module. Such a module now gets its path initializer +registered lazily, like an on-demand CommonJS module, so startup also skips +one registry insert per deferred module. diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs index 0d5c58f0a44..a3b5530b800 100644 --- a/crates/perry/src/commands/compile/run_pipeline.rs +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -3900,9 +3900,14 @@ pub fn run_with_parse_cache( let nextjs_path_init_modules: Vec<(String, String)> = if is_entry { ctx.native_modules .iter() - .filter(|(p, module)| { - module.init_kind == perry_hir::ModuleInitKind::Deferred - || self::collect_modules::is_nextjs_runtime_module(p) + // A module is on this list by WHERE it is (a Next.js server + // module, or a compilePackages module), never by how it is + // loaded: a non-empty list makes the entry seed + // `globalThis.AsyncLocalStorage`, which node has only in a + // Next.js process. A module reached only through `import()` + // gets its path anchor from the lazy list below. + .filter(|(p, _)| { + self::collect_modules::is_nextjs_runtime_module(p) // A `perry.compilePackages` module may be reachable // ONLY through a runtime-computed require (Next's // require-hook aliases `styled-jsx` to its resolved @@ -3919,13 +3924,19 @@ pub fn run_with_parse_cache( } else { Vec::new() }; - // A require edge runs its target's init directly; only a program that - // can require by path (not closed) needs the anchor of an on-demand - // module, and only once it looks a path up: registered lazily. + // A require edge runs its target's init directly, and an `import()` + // site calls its target's init directly; only a program that can + // require by path (not closed) needs the anchor of an on-demand or + // deferred module, and only once it looks a path up: registered lazily. let lazy_path_init_modules: Vec<(String, String)> = if is_entry && !ctx.cjs_closed { ctx.native_modules .iter() - .filter(|(_, module)| module.init_kind == perry_hir::ModuleInitKind::OnDemand) + .filter(|(_, module)| { + matches!( + module.init_kind, + perry_hir::ModuleInitKind::OnDemand | perry_hir::ModuleInitKind::Deferred + ) + }) .map(|(p, m)| (p.to_string_lossy().into_owned(), sanitize_name(&m.name))) .filter(|pair| !nextjs_path_init_modules.contains(pair)) .collect() diff --git a/test-files/dynamic_import_als_seed_target.ts b/test-files/dynamic_import_als_seed_target.ts new file mode 100644 index 00000000000..1e2b62b1086 --- /dev/null +++ b/test-files/dynamic_import_als_seed_target.ts @@ -0,0 +1,2 @@ +// Helper for test_gap_als_seed_dynamic_import.ts: reached only through import(). +export const x: number = 1; diff --git a/test-files/test_gap_als_seed_dynamic_import.ts b/test-files/test_gap_als_seed_dynamic_import.ts new file mode 100644 index 00000000000..51be62ca8ef --- /dev/null +++ b/test-files/test_gap_als_seed_dynamic_import.ts @@ -0,0 +1,8 @@ +// A program with a dynamic `import()` must not get `globalThis.AsyncLocalStorage`: +// node has no such global (only Next.js assigns one). The entry seeds it for +// Next.js server modules alone; a module reached only through `import()` used +// to put the program on that path, so this printed "function". + +const m = await import("./dynamic_import_als_seed_target.ts"); +console.log(typeof (globalThis as any).AsyncLocalStorage, m.x); +console.log("AsyncLocalStorage" in globalThis);