From 59c73aecc9b26fdde5d6cd525ccc433123e7f55a Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Tue, 28 Apr 2026 11:00:13 +0200 Subject: [PATCH 1/3] Apply sourcemaps by default during prerender in `next build` (#93280) --- docs/01-app/02-guides/memory-usage.mdx | 2 +- docs/01-app/03-api-reference/06-cli/next.mdx | 2 -- packages/next/src/server/config-shared.ts | 4 ++-- packages/next/src/server/config.ts | 8 -------- .../root-suspense-dynamic/fixtures/default/next.config.js | 2 -- .../server-source-maps/fixtures/edge/next.config.js | 1 - 6 files changed, 3 insertions(+), 16 deletions(-) diff --git a/docs/01-app/02-guides/memory-usage.mdx b/docs/01-app/02-guides/memory-usage.mdx index e31fc4a213d2..6e9628b2ca33 100644 --- a/docs/01-app/02-guides/memory-usage.mdx +++ b/docs/01-app/02-guides/memory-usage.mdx @@ -125,7 +125,7 @@ Generating source maps consumes extra memory during the build process. You can disable source map generation by adding `productionBrowserSourceMaps: false` and `experimental.serverSourceMaps: false` to your Next.js configuration. -When using the `cacheComponents` feature, Next.js will use source maps by default during the prerender phase of `next build`. +Next.js will use source maps by default during the prerender phase of `next build`. If you consistently encounter memory issues during that phase (after "Generating static pages"), you can try disabling source maps in that phase by adding `enablePrerenderSourceMaps: false` to your Next.js configuration. diff --git a/docs/01-app/03-api-reference/06-cli/next.mdx b/docs/01-app/03-api-reference/06-cli/next.mdx index 0bd36a3a0db6..b30434dae18b 100644 --- a/docs/01-app/03-api-reference/06-cli/next.mdx +++ b/docs/01-app/03-api-reference/06-cli/next.mdx @@ -288,8 +288,6 @@ This enables several experimental options to make debugging easier: - `experimental.turbopackMinify = false` - Generates source maps for server bundles: - `experimental.serverSourceMaps = true` -- Enables source map consumption in child processes used for prerendering: - - `enablePrerenderSourceMaps = true` - Continues building even after the first prerender error, so you can see all issues at once: - `experimental.prerenderEarlyExit = false` diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 96f187da5fb5..9f61694f4804 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -1635,6 +1635,7 @@ export interface NextConfig { /** * Enables source maps while generating static pages. * Helps with errors during the prerender phase in `next build`. + * Defaults to `true`. Set to `false` to disable. */ enablePrerenderSourceMaps?: boolean @@ -1799,8 +1800,7 @@ export const defaultConfig = Object.freeze({ modularizeImports: undefined, outputFileTracingRoot: process.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT || '', allowedDevOrigins: undefined, - // Will default to cacheComponents value. - enablePrerenderSourceMaps: undefined, + enablePrerenderSourceMaps: true, cacheComponents: false, cacheLife: { default: { diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index d5ca0612ef47..2531b86b0c5d 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -1459,11 +1459,6 @@ function assignDefaultsAndValidate( if (result.cacheComponents) { // TODO: remove once we've finished migrating internally to cacheComponents. result.experimental.ppr = true - - // Prerender sourcemaps are enabled by default when using cacheComponents, unless explicitly disabled. - if (result.enablePrerenderSourceMaps === undefined) { - result.enablePrerenderSourceMaps = true - } } // "use cache" was originally implicitly enabled with the cacheComponents flag, so @@ -2025,9 +2020,6 @@ function enforceExperimentalFeatures( debugPrerender && (phase === PHASE_PRODUCTION_BUILD || phase === PHASE_EXPORT) ) { - // TODO: This is not an experimental feature, but should be enabled alongside other prerender debugging features. - config.enablePrerenderSourceMaps = true - setExperimentalFeatureForDebugPrerender( config.experimental, 'serverSourceMaps', diff --git a/test/e2e/app-dir/root-suspense-dynamic/fixtures/default/next.config.js b/test/e2e/app-dir/root-suspense-dynamic/fixtures/default/next.config.js index c502202e8c88..e64bae22d658 100644 --- a/test/e2e/app-dir/root-suspense-dynamic/fixtures/default/next.config.js +++ b/test/e2e/app-dir/root-suspense-dynamic/fixtures/default/next.config.js @@ -2,8 +2,6 @@ * @type {import('next').NextConfig} */ const nextConfig = { - enablePrerenderSourceMaps: false, - cacheComponents: true, } diff --git a/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js b/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js index e923f0a471b3..86773e2ae9a6 100644 --- a/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js +++ b/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js @@ -2,7 +2,6 @@ * @type {import('next').NextConfig} */ const nextConfig = { - enablePrerenderSourceMaps: true, experimental: { cpus: 1, serverSourceMaps: true, From eb763918295f6101d388cb7513fea97976c2adc1 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:52:08 +0200 Subject: [PATCH 2/3] Turbopack: compute exports without full analysis (#92823) We don't actually need the full analysis to compute exports. This can break some task cycles, and might theoretically improve task latency. The CPU time appears to be marginally higher (but not statistically significant), but no impact on overall build time: ``` * c4fdcc2651 - (14 hours ago) compute exports before final_read_hint() - Niklas Mischkulnig (origin/mischnic/get-exports-without-analyze, mischnic/get-exports-without-analyze) pnpm next build --experimental-build-mode=compile 378.10s user 58.28s system 760% cpu 57.411 total pnpm next build --experimental-build-mode=compile 383.64s user 60.32s system 738% cpu 1:00.10 total pnpm next build --experimental-build-mode=compile 378.70s user 55.65s system 718% cpu 1:00.41 total * 8ddc1cdbdd - (38 minutes ago) Use OIDC for CodSpeed (#93059) - Niklas Mischkulnig (origin/canary, origin/HEAD, canary) pnpm next build --experimental-build-mode=compile 376.95s user 58.30s system 755% cpu 57.597 total pnpm next build --experimental-build-mode=compile 375.85s user 61.75s system 738% cpu 59.230 total pnpm next build --experimental-build-mode=compile 377.75s user 63.00s system 729% cpu 1:00.39 total ``` --- .../crates/turbopack-ecmascript/src/lib.rs | 5 +- .../src/references/exports.rs | 371 ++++++++++++++++++ .../src/references/mod.rs | 325 ++------------- .../src/tree_shake/part/module.rs | 27 +- 4 files changed, 413 insertions(+), 315 deletions(-) create mode 100644 turbopack/crates/turbopack-ecmascript/src/references/exports.rs diff --git a/turbopack/crates/turbopack-ecmascript/src/lib.rs b/turbopack/crates/turbopack-ecmascript/src/lib.rs index 2e1e6d018f1c..206fa9fd8d88 100644 --- a/turbopack/crates/turbopack-ecmascript/src/lib.rs +++ b/turbopack/crates/turbopack-ecmascript/src/lib.rs @@ -115,6 +115,7 @@ use crate::{ analyze_ecmascript_module, async_module::OptionAsyncModule, esm::{UrlRewriteBehavior, base::EsmAssetReferences, export}, + exports::compute_ecmascript_module_exports, }, side_effect_optimization::reference::EcmascriptModulePartReference, swc_comments::{CowComments, ImmutableComments}, @@ -676,7 +677,7 @@ impl EcmascriptAnalyzable for EcmascriptModuleAsset { async_module: analyze_ref.async_module, generate_source_map, original_source_map: analyze_ref.source_map, - exports: analyze_ref.exports, + exports: self.get_exports().to_resolved().await?, async_module_info, } .cell()) @@ -896,7 +897,7 @@ impl ChunkableModule for EcmascriptModuleAsset { impl EcmascriptChunkPlaceable for EcmascriptModuleAsset { #[turbo_tasks::function] async fn get_exports(self: Vc) -> Result> { - Ok(*self.analyze().await?.exports) + Ok(*compute_ecmascript_module_exports(self, None).await?.exports) } #[turbo_tasks::function] diff --git a/turbopack/crates/turbopack-ecmascript/src/references/exports.rs b/turbopack/crates/turbopack-ecmascript/src/references/exports.rs new file mode 100644 index 000000000000..ecde5f1cc9e7 --- /dev/null +++ b/turbopack/crates/turbopack-ecmascript/src/references/exports.rs @@ -0,0 +1,371 @@ +use anyhow::{Result, bail}; +use swc_core::{ + common::source_map::SmallPos, + ecma::ast::{Expr, Ident, ImportDecl, MemberProp, Program, Stmt}, +}; +use tracing::Instrument; +use turbo_rcstr::RcStr; +use turbo_tasks::{ResolvedVc, Vc}; +use turbopack_core::{ + issue::{IssueExt, IssueSource}, + reference::ModuleReference, + resolve::ModulePart, +}; + +use crate::{ + EcmascriptModuleAsset, EcmascriptParsable, ModuleTypeResult, SpecifiedModuleType, + TreeShakingMode, + analyzer::imports::{ImportAnnotations, ImportedSymbol}, + chunk::EcmascriptExports, + parse::ParseResult, + references::{ + TURBOPACK_HELPER_WTF8, + esm::{EsmAssetReference, EsmExports}, + type_issue::SpecifiedModuleTypeIssue, + }, + runtime_functions::{TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE}, + tree_shake::{part_of_module, split_module}, +}; + +#[turbo_tasks::value] +pub struct EcmascriptExportsAnalysis { + pub exports: ResolvedVc, + pub import_references: Box<[ResolvedVc]>, + pub esm_reexport_reference_idxs: Box<[usize]>, + pub esm_evaluation_reference_idxs: Box<[usize]>, +} + +#[turbo_tasks::function] +pub async fn compute_ecmascript_module_exports( + module: ResolvedVc, + part: Option, +) -> Result> { + let raw_module = module.await?; + let source = raw_module.source; + let options = raw_module.options.await?; + let import_externals = options.import_externals; + + let parsed = if let Some(part) = part { + let split_data = split_module(*module); + part_of_module(split_data, part) + } else { + module.failsafe_parse() + }; + + let parsed = parsed.await?; + let ParseResult::Ok { + program, + eval_context, + .. + } = &*parsed + else { + return Ok(EcmascriptExportsAnalysis { + exports: EcmascriptExports::Unknown.resolved_cell(), + import_references: Box::new([]), + esm_reexport_reference_idxs: Box::new([]), + esm_evaluation_reference_idxs: Box::new([]), + } + .cell()); + }; + + let ModuleTypeResult { + module_type: specified_type, + .. + } = *module.determine_module_type().await?; + + let inner_assets = if let Some(assets) = raw_module.inner_assets { + Some(assets.await?) + } else { + None + }; + + let mut esm_reexport_reference_idxs: Vec = vec![]; + let mut esm_evaluation_reference_idxs: Vec = vec![]; + + let span = tracing::trace_span!("esm import references"); + let import_references = async { + let mut import_references = Vec::with_capacity(eval_context.imports.references().len()); + for (i, r) in eval_context.imports.references().enumerate() { + let mut should_add_evaluation = false; + + let resolve_override = if let Some(inner_assets) = &inner_assets + && let Some(req) = r.module_path.as_str() + && let Some(a) = inner_assets.get(req) + { + Some(*a) + } else { + None + }; + + let reference = EsmAssetReference::new( + module, + ResolvedVc::upcast(module), + RcStr::from(&*r.module_path.to_string_lossy()), + IssueSource::from_swc_offsets(source, r.span.lo.to_u32(), r.span.hi.to_u32()), + r.annotations.as_ref().map(|a| (**a).clone()), + match &r.imported_symbol { + &ImportedSymbol::ModuleEvaluation => { + should_add_evaluation = true; + Some(ModulePart::evaluation()) + } + ImportedSymbol::Symbol(name) => Some(ModulePart::export((&**name).into())), + ImportedSymbol::PartEvaluation(part_id) | ImportedSymbol::Part(part_id) => { + if !matches!( + options.tree_shaking_mode, + Some(TreeShakingMode::ModuleFragments) + ) { + bail!( + "Internal imports only exist in reexports only mode when \ + importing {:?} from {}", + r.imported_symbol, + r.module_path.to_string_lossy() + ); + } + if matches!(&r.imported_symbol, ImportedSymbol::PartEvaluation(_)) { + should_add_evaluation = true; + } + Some(ModulePart::internal(*part_id)) + } + ImportedSymbol::Exports => matches!( + options.tree_shaking_mode, + Some(TreeShakingMode::ModuleFragments) + ) + .then(ModulePart::exports), + }, + eval_context + .imports + .import_usage + .get(&i) + .cloned() + .unwrap_or_default(), + import_externals, + options.tree_shaking_mode, + resolve_override, + ) + .resolved_cell(); + + import_references.push(reference); + if should_add_evaluation { + esm_evaluation_reference_idxs.push(i); + } + } + anyhow::Ok(import_references) + } + .instrument(span) + .await?; + + let span = tracing::trace_span!("exports"); + let exports = async { + let esm_star_exports: Vec>> = eval_context + .imports + .reexport_namespaces() + .map(|i| ResolvedVc::upcast(import_references[i])) + .collect(); + let esm_exports = eval_context + .imports + .as_esm_exports(&import_references, eval_context)?; + + for idx in eval_context.imports.reexports_reference_idxs() { + esm_reexport_reference_idxs.push(idx); + } + + anyhow::Ok( + if !esm_exports.is_empty() || !esm_star_exports.is_empty() { + if specified_type == SpecifiedModuleType::CommonJs { + SpecifiedModuleTypeIssue { + // TODO(PACK-4879): this should point at one of the exports + source: IssueSource::from_source_only(source), + specified_type, + } + .resolved_cell() + .emit(); + } + + let esm_exports = EsmExports { + exports: esm_exports, + star_exports: esm_star_exports, + } + .cell(); + + EcmascriptExports::EsmExports(esm_exports.to_resolved().await?) + } else if specified_type == SpecifiedModuleType::EcmaScript { + match detect_dynamic_export(program) { + DetectedDynamicExportType::CommonJs => { + SpecifiedModuleTypeIssue { + // TODO(PACK-4879): this should point at the source location of the + // commonjs export + source: IssueSource::from_source_only(source), + specified_type, + } + .resolved_cell() + .emit(); + + EcmascriptExports::EsmExports( + EsmExports { + exports: Default::default(), + star_exports: Default::default(), + } + .resolved_cell(), + ) + } + DetectedDynamicExportType::Namespace => EcmascriptExports::DynamicNamespace, + DetectedDynamicExportType::Value => EcmascriptExports::Value, + DetectedDynamicExportType::UsingModuleDeclarations + | DetectedDynamicExportType::None => EcmascriptExports::EsmExports( + EsmExports { + exports: Default::default(), + star_exports: Default::default(), + } + .resolved_cell(), + ), + } + } else { + match detect_dynamic_export(program) { + DetectedDynamicExportType::CommonJs => EcmascriptExports::CommonJs, + DetectedDynamicExportType::Namespace => EcmascriptExports::DynamicNamespace, + DetectedDynamicExportType::Value => EcmascriptExports::Value, + DetectedDynamicExportType::UsingModuleDeclarations => { + EcmascriptExports::EsmExports( + EsmExports { + exports: Default::default(), + star_exports: Default::default(), + } + .resolved_cell(), + ) + } + DetectedDynamicExportType::None => EcmascriptExports::EmptyCommonJs, + } + } + .resolved_cell(), + ) + } + .instrument(span) + .await?; + + Ok(EcmascriptExportsAnalysis { + exports, + import_references: import_references.into_boxed_slice(), + esm_reexport_reference_idxs: esm_reexport_reference_idxs.into_boxed_slice(), + esm_evaluation_reference_idxs: esm_evaluation_reference_idxs.into_boxed_slice(), + } + .cell()) +} + +#[derive(Debug)] +enum DetectedDynamicExportType { + CommonJs, + Namespace, + Value, + None, + UsingModuleDeclarations, +} + +// TODO move into ImportMap +fn detect_dynamic_export(p: &Program) -> DetectedDynamicExportType { + use swc_core::ecma::visit::{Visit, VisitWith, visit_obj_and_computed}; + + if let Program::Module(m) = p { + // Check for imports/exports + if m.body.iter().any(|item| { + item.as_module_decl().is_some_and(|module_decl| { + module_decl.as_import().is_none_or(|import| { + !is_turbopack_helper_import(import) && !is_swc_helper_import(import) + }) + }) + }) { + return DetectedDynamicExportType::UsingModuleDeclarations; + } + } + + struct Visitor { + cjs: bool, + value: bool, + namespace: bool, + found: bool, + } + + impl Visit for Visitor { + visit_obj_and_computed!(); + + fn visit_ident(&mut self, i: &Ident) { + // The detection is not perfect, it might have some false positives, e. g. in + // cases where `module` is used in some other way. e. g. `const module = 42;`. + // But a false positive doesn't break anything, it only opts out of some + // optimizations, which is acceptable. + if &*i.sym == "module" || &*i.sym == "exports" { + self.cjs = true; + self.found = true; + } + if &*i.sym == "__turbopack_export_value__" { + self.value = true; + self.found = true; + } + if &*i.sym == "__turbopack_export_namespace__" { + self.namespace = true; + self.found = true; + } + } + + fn visit_expr(&mut self, n: &Expr) { + if self.found { + return; + } + + if let Expr::Member(member) = n + && member.obj.is_ident_ref_to("__turbopack_context__") + && let MemberProp::Ident(prop) = &member.prop + { + const TURBOPACK_EXPORT_VALUE_SHORTCUT: &str = TURBOPACK_EXPORT_VALUE.shortcut; + const TURBOPACK_EXPORT_NAMESPACE_SHORTCUT: &str = + TURBOPACK_EXPORT_NAMESPACE.shortcut; + match &*prop.sym { + TURBOPACK_EXPORT_VALUE_SHORTCUT => { + self.value = true; + self.found = true; + } + TURBOPACK_EXPORT_NAMESPACE_SHORTCUT => { + self.namespace = true; + self.found = true; + } + _ => {} + } + } + + n.visit_children_with(self); + } + + fn visit_stmt(&mut self, n: &Stmt) { + if self.found { + return; + } + n.visit_children_with(self); + } + } + + let mut v = Visitor { + cjs: false, + value: false, + namespace: false, + found: false, + }; + p.visit_with(&mut v); + if v.cjs { + DetectedDynamicExportType::CommonJs + } else if v.value { + DetectedDynamicExportType::Value + } else if v.namespace { + DetectedDynamicExportType::Namespace + } else { + DetectedDynamicExportType::None + } +} + +pub fn is_turbopack_helper_import(import: &ImportDecl) -> bool { + let annotations = ImportAnnotations::parse(import.with.as_deref()); + + annotations.is_some_and(|a| a.get(&TURBOPACK_HELPER_WTF8).is_some()) +} + +pub fn is_swc_helper_import(import: &ImportDecl) -> bool { + import.src.value.starts_with("@swc/helpers/") +} diff --git a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs index 7d3bb61720d2..c5be61e96c91 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs @@ -5,6 +5,7 @@ pub mod constant_condition; pub mod constant_value; pub mod dynamic_expression; pub mod esm; +pub mod exports; pub mod exports_info; pub mod external_module; pub mod hot_module; @@ -28,7 +29,7 @@ use std::{ sync::{Arc, LazyLock}, }; -use anyhow::{Result, bail}; +use anyhow::Result; use bincode::{Decode, Encode}; use constant_condition::{ConstantConditionCodeGen, ConstantConditionValue}; use constant_value::ConstantValueCodeGen; @@ -96,19 +97,18 @@ use worker::WorkerAssetReference; pub use crate::references::esm::export::{FollowExportsResult, follow_reexports}; use crate::{ AnalyzeMode, EcmascriptModuleAsset, EcmascriptModuleAssetType, EcmascriptParsable, - ModuleTypeResult, SpecifiedModuleType, TreeShakingMode, TypeofWindow, + ModuleTypeResult, TreeShakingMode, TypeofWindow, analyzer::{ ConstantNumber, ConstantString, ConstantValue as JsConstantValue, JsValue, JsValueUrlKind, ObjectPart, RequireContextValue, WellKnownFunctionKind, WellKnownObjectKind, builtin::{early_replace_builtin, replace_builtin}, graph::{ConditionalKind, Effect, EffectArg, VarGraph, create_graph}, - imports::{ImportAnnotations, ImportAttributes, ImportMap, ImportedSymbol}, + imports::{ImportAnnotations, ImportAttributes, ImportMap}, linker::link, parse_require_context, side_effects, top_level_await::has_top_level_await, well_known::replace_well_known, }, - chunk::EcmascriptExports, code_gen::{CodeGen, CodeGens, IntoCodeGenReference}, errors, parse::ParseResult, @@ -124,10 +124,11 @@ use crate::{ }, dynamic_expression::DynamicExpression, esm::{ - EsmAssetReference, EsmAsyncAssetReference, EsmBinding, EsmExports, ImportMetaBinding, + EsmAssetReference, EsmAsyncAssetReference, EsmBinding, ImportMetaBinding, ImportMetaRef, UrlAssetReference, UrlRewriteBehavior, base::EsmAssetReferences, module_id::EsmModuleIdAssetReference, }, + exports::{EcmascriptExportsAnalysis, compute_ecmascript_module_exports}, exports_info::{ExportsInfoBinding, ExportsInfoRef}, hot_module::{ModuleHotReferenceAssetReference, ModuleHotReferenceCodeGen}, ident::IdentReplacement, @@ -136,14 +137,13 @@ use crate::{ node::PackageJsonReference, raw::{DirAssetReference, FileSourceReference}, require_context::{RequireContextAssetReference, RequireContextMap}, - type_issue::SpecifiedModuleTypeIssue, typescript::{ TsConfigReference, TsReferencePathAssetReference, TsReferenceTypeAssetReference, }, }, runtime_functions::{ - TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE, TURBOPACK_EXPORTS, TURBOPACK_GLOBAL, - TURBOPACK_REQUIRE_REAL, TURBOPACK_REQUIRE_STUB, TURBOPACK_RUNTIME_FUNCTION_SHORTCUTS, + TURBOPACK_EXPORTS, TURBOPACK_GLOBAL, TURBOPACK_REQUIRE_REAL, TURBOPACK_REQUIRE_STUB, + TURBOPACK_RUNTIME_FUNCTION_SHORTCUTS, }, source_map::parse_source_map_comment, tree_shake::{part_of_module, split_module}, @@ -159,7 +159,6 @@ pub struct AnalyzeEcmascriptModuleResult { pub esm_reexport_references: ResolvedVc, pub code_generation: ResolvedVc, - pub exports: ResolvedVc, pub async_module: ResolvedVc, pub side_effects: ModuleSideEffects, /// `true` when the analysis was successful. @@ -218,7 +217,6 @@ struct AnalyzeEcmascriptModuleResultBuilder { esm_references_rewritten: FxHashMap>>, code_gens: CodeGenCollection, - exports: EcmascriptExports, async_module: ResolvedVc, successful: bool, source_map: Option>>, @@ -238,7 +236,6 @@ impl AnalyzeEcmascriptModuleResultBuilder { esm_references_rewritten: Default::default(), esm_references_free_var: Default::default(), code_gens: Default::default(), - exports: EcmascriptExports::Unknown, async_module: ResolvedVc::cell(None), successful: false, source_map: None, @@ -309,11 +306,6 @@ impl AnalyzeEcmascriptModuleResultBuilder { self.source_map = Some(source_map); } - /// Sets the analysis result ES export. - pub fn set_exports(&mut self, exports: EcmascriptExports) { - self.exports = exports; - } - /// Sets the analysis result ES export. pub fn set_async_module(&mut self, async_module: ResolvedVc) { self.async_module = ResolvedVc::cell(Some(async_module)); @@ -357,7 +349,7 @@ impl AnalyzeEcmascriptModuleResultBuilder { /// Builds the final analysis result. Resolves internal Vcs. pub async fn build( mut self, - import_references: Vec>, + import_references: &[ResolvedVc], track_reexport_references: bool, ) -> Result> { // esm_references_rewritten (and esm_references_free_var) needs to be spliced in at the @@ -430,7 +422,6 @@ impl AnalyzeEcmascriptModuleResultBuilder { esm_reexport_references.unwrap_or_default(), ), code_generation: ResolvedVc::cell(code_generation), - exports: self.exports.resolved_cell(), async_module: self.async_module, side_effects: self.side_effects, successful: self.successful, @@ -574,9 +565,9 @@ async fn analyze_ecmascript_module_internal( }; // Split out our module part if we have one. - let parsed = if let Some(part) = part { + let parsed = if let Some(part) = &part { let split_data = split_module(*module); - part_of_module(split_data, part) + part_of_module(split_data, part.clone()) } else { module.failsafe_parse() }; @@ -619,6 +610,14 @@ async fn analyze_ecmascript_module_internal( .await?; } + let EcmascriptExportsAnalysis { + exports: _, + import_references, + esm_reexport_reference_idxs, + esm_evaluation_reference_idxs, + // This reads the ParseResult, so it has to happen before the final_read_hint. + } = &*compute_ecmascript_module_exports(*module, part).await?; + let parsed = if !analyze_mode.is_code_gen() { // We are never code-gening the module, so we can drop the AST after the analysis. parsed.final_read_hint().await? @@ -639,6 +638,13 @@ async fn analyze_ecmascript_module_internal( return analysis.build(Default::default(), false).await; }; + for i in esm_reexport_reference_idxs { + analysis.add_esm_reexport_reference(*i); + } + for i in esm_evaluation_reference_idxs { + analysis.add_esm_evaluation_reference(*i); + } + let has_side_effect_free_directive = match program { Program::Module(module) => Either::Left( module @@ -759,165 +765,6 @@ async fn analyze_ecmascript_module_internal( .supports_block_scoping() .await?; - let span = tracing::trace_span!("esm import references"); - let import_references = async { - let mut import_references = Vec::with_capacity(eval_context.imports.references().len()); - for (i, r) in eval_context.imports.references().enumerate() { - let mut should_add_evaluation = false; - - let resolve_override = if let Some(inner_assets) = &inner_assets - && let Some(req) = r.module_path.as_str() - && let Some(a) = inner_assets.get(req) - { - Some(*a) - } else { - None - }; - - let reference = EsmAssetReference::new( - module, - ResolvedVc::upcast(module), - RcStr::from(&*r.module_path.to_string_lossy()), - IssueSource::from_swc_offsets(source, r.span.lo.to_u32(), r.span.hi.to_u32()), - r.annotations.as_ref().map(|a| (**a).clone()), - match &r.imported_symbol { - ImportedSymbol::ModuleEvaluation => { - should_add_evaluation = true; - Some(ModulePart::evaluation()) - } - ImportedSymbol::Symbol(name) => Some(ModulePart::export((&**name).into())), - ImportedSymbol::PartEvaluation(part_id) | ImportedSymbol::Part(part_id) => { - if !matches!( - options.tree_shaking_mode, - Some(TreeShakingMode::ModuleFragments) - ) { - bail!( - "Internal imports only exist in reexports only mode when \ - importing {:?} from {}", - r.imported_symbol, - r.module_path.to_string_lossy() - ); - } - if matches!(&r.imported_symbol, ImportedSymbol::PartEvaluation(_)) { - should_add_evaluation = true; - } - Some(ModulePart::internal(*part_id)) - } - ImportedSymbol::Exports => matches!( - options.tree_shaking_mode, - Some(TreeShakingMode::ModuleFragments) - ) - .then(ModulePart::exports), - }, - eval_context - .imports - .import_usage - .get(&i) - .cloned() - .unwrap_or_default(), - import_externals, - options.tree_shaking_mode, - resolve_override, - ) - .resolved_cell(); - - import_references.push(reference); - if should_add_evaluation { - analysis.add_esm_evaluation_reference(i); - } - } - anyhow::Ok(import_references) - } - .instrument(span) - .await?; - - let span = tracing::trace_span!("exports"); - async { - let esm_star_exports: Vec>> = eval_context - .imports - .reexport_namespaces() - .map(|i| ResolvedVc::upcast(import_references[i])) - .collect(); - let esm_exports = eval_context - .imports - .as_esm_exports(&import_references, eval_context)?; - - for idx in eval_context.imports.reexports_reference_idxs() { - analysis.add_esm_reexport_reference(idx); - } - - let exports = if !esm_exports.is_empty() || !esm_star_exports.is_empty() { - if specified_type == SpecifiedModuleType::CommonJs { - SpecifiedModuleTypeIssue { - // TODO(PACK-4879): this should point at one of the exports - source: IssueSource::from_source_only(source), - specified_type, - } - .resolved_cell() - .emit(); - } - - let esm_exports = EsmExports { - exports: esm_exports, - star_exports: esm_star_exports, - } - .cell(); - - EcmascriptExports::EsmExports(esm_exports.to_resolved().await?) - } else if specified_type == SpecifiedModuleType::EcmaScript { - match detect_dynamic_export(program) { - DetectedDynamicExportType::CommonJs => { - SpecifiedModuleTypeIssue { - // TODO(PACK-4879): this should point at the source location of the commonjs - // export - source: IssueSource::from_source_only(source), - specified_type, - } - .resolved_cell() - .emit(); - - EcmascriptExports::EsmExports( - EsmExports { - exports: Default::default(), - star_exports: Default::default(), - } - .resolved_cell(), - ) - } - DetectedDynamicExportType::Namespace => EcmascriptExports::DynamicNamespace, - DetectedDynamicExportType::Value => EcmascriptExports::Value, - DetectedDynamicExportType::UsingModuleDeclarations - | DetectedDynamicExportType::None => EcmascriptExports::EsmExports( - EsmExports { - exports: Default::default(), - star_exports: Default::default(), - } - .resolved_cell(), - ), - } - } else { - match detect_dynamic_export(program) { - DetectedDynamicExportType::CommonJs => EcmascriptExports::CommonJs, - DetectedDynamicExportType::Namespace => EcmascriptExports::DynamicNamespace, - DetectedDynamicExportType::Value => EcmascriptExports::Value, - DetectedDynamicExportType::UsingModuleDeclarations => { - EcmascriptExports::EsmExports( - EsmExports { - exports: Default::default(), - star_exports: Default::default(), - } - .resolved_cell(), - ) - } - DetectedDynamicExportType::None => EcmascriptExports::EmptyCommonJs, - } - }; - analysis.set_exports(exports); - anyhow::Ok(()) - } - .instrument(span) - .await?; - // TODO: we can do this when constructing the var graph let span = tracing::trace_span!("async module handling"); async { @@ -989,7 +836,7 @@ async fn analyze_ecmascript_module_internal( collect_affecting_sources: options.analyze_mode.is_tracing_assets(), tracing_only: !options.analyze_mode.is_code_gen(), is_esm, - import_references: &import_references, + import_references, imports: &eval_context.imports, inner_assets, }; @@ -3867,124 +3714,6 @@ pub static TURBOPACK_HELPER: Lazy = Lazy::new(|| atom!("__turbopack-helper pub static TURBOPACK_HELPER_WTF8: Lazy = Lazy::new(|| atom!("__turbopack-helper__").into()); -pub fn is_turbopack_helper_import(import: &ImportDecl) -> bool { - let annotations = ImportAnnotations::parse(import.with.as_deref()); - - annotations.is_some_and(|a| a.get(&TURBOPACK_HELPER_WTF8).is_some()) -} - -pub fn is_swc_helper_import(import: &ImportDecl) -> bool { - import.src.value.starts_with("@swc/helpers/") -} - -#[derive(Debug)] -enum DetectedDynamicExportType { - CommonJs, - Namespace, - Value, - None, - UsingModuleDeclarations, -} - -fn detect_dynamic_export(p: &Program) -> DetectedDynamicExportType { - use swc_core::ecma::visit::{Visit, VisitWith, visit_obj_and_computed}; - - if let Program::Module(m) = p { - // Check for imports/exports - if m.body.iter().any(|item| { - item.as_module_decl().is_some_and(|module_decl| { - module_decl.as_import().is_none_or(|import| { - !is_turbopack_helper_import(import) && !is_swc_helper_import(import) - }) - }) - }) { - return DetectedDynamicExportType::UsingModuleDeclarations; - } - } - - struct Visitor { - cjs: bool, - value: bool, - namespace: bool, - found: bool, - } - - impl Visit for Visitor { - visit_obj_and_computed!(); - - fn visit_ident(&mut self, i: &Ident) { - // The detection is not perfect, it might have some false positives, e. g. in - // cases where `module` is used in some other way. e. g. `const module = 42;`. - // But a false positive doesn't break anything, it only opts out of some - // optimizations, which is acceptable. - if &*i.sym == "module" || &*i.sym == "exports" { - self.cjs = true; - self.found = true; - } - if &*i.sym == "__turbopack_export_value__" { - self.value = true; - self.found = true; - } - if &*i.sym == "__turbopack_export_namespace__" { - self.namespace = true; - self.found = true; - } - } - - fn visit_expr(&mut self, n: &Expr) { - if self.found { - return; - } - - if let Expr::Member(member) = n - && member.obj.is_ident_ref_to("__turbopack_context__") - && let MemberProp::Ident(prop) = &member.prop - { - const TURBOPACK_EXPORT_VALUE_SHORTCUT: &str = TURBOPACK_EXPORT_VALUE.shortcut; - const TURBOPACK_EXPORT_NAMESPACE_SHORTCUT: &str = - TURBOPACK_EXPORT_NAMESPACE.shortcut; - match &*prop.sym { - TURBOPACK_EXPORT_VALUE_SHORTCUT => { - self.value = true; - self.found = true; - } - TURBOPACK_EXPORT_NAMESPACE_SHORTCUT => { - self.namespace = true; - self.found = true; - } - _ => {} - } - } - - n.visit_children_with(self); - } - - fn visit_stmt(&mut self, n: &Stmt) { - if self.found { - return; - } - n.visit_children_with(self); - } - } - - let mut v = Visitor { - cjs: false, - value: false, - namespace: false, - found: false, - }; - p.visit_with(&mut v); - if v.cjs { - DetectedDynamicExportType::CommonJs - } else if v.value { - DetectedDynamicExportType::Value - } else if v.namespace { - DetectedDynamicExportType::Namespace - } else { - DetectedDynamicExportType::None - } -} - /// Detects whether a list of arguments is specifically /// `(process.argv[0], ['-e', ...])`. This is useful for detecting if a node /// process is being spawned to interpret a string of JavaScript code, and does diff --git a/turbopack/crates/turbopack-ecmascript/src/tree_shake/part/module.rs b/turbopack/crates/turbopack-ecmascript/src/tree_shake/part/module.rs index c61d858ee560..a84627b1a1b5 100644 --- a/turbopack/crates/turbopack-ecmascript/src/tree_shake/part/module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/tree_shake/part/module.rs @@ -20,7 +20,8 @@ use crate::{ }, parse::ParseResult, references::{ - FollowExportsResult, analyze_ecmascript_module, esm::FoundExportType, follow_reexports, + FollowExportsResult, analyze_ecmascript_module, esm::FoundExportType, + exports::compute_ecmascript_module_exports, follow_reexports, }, rename::module::EcmascriptModuleRenameModule, tree_shake::{ @@ -105,7 +106,7 @@ impl EcmascriptAnalyzable for EcmascriptModulePartAsset { async_module: analyze_ref.async_module, generate_source_map, original_source_map: analyze_ref.source_map, - exports: analyze_ref.exports, + exports: self.get_exports().to_resolved().await?, async_module_info, } .cell()) @@ -230,7 +231,7 @@ impl EcmascriptModulePartAsset { #[turbo_tasks::function] pub async fn is_async_module(self: Vc) -> Result> { let this = self.await?; - let result = analyze(*this.full_module, this.part.clone()); + let result = analyze_ecmascript_module(*this.full_module, Some(this.part.clone())); if let Some(async_module) = *result.await?.async_module.await? { Ok(async_module.is_self_async(self.references())) @@ -331,7 +332,7 @@ impl Module for EcmascriptModulePartAsset { return Ok(Vc::cell(references)); } - let analyze = analyze(*self.full_module, self.part.clone()); + let analyze = analyze_ecmascript_module(*self.full_module, Some(self.part.clone())); Ok(analyze.references()) } @@ -350,8 +351,12 @@ impl Module for EcmascriptModulePartAsset { #[turbo_tasks::value_impl] impl EcmascriptChunkPlaceable for EcmascriptModulePartAsset { #[turbo_tasks::function] - async fn get_exports(self: Vc) -> Result> { - Ok(*self.analyze().await?.exports) + async fn get_exports(&self) -> Result> { + Ok( + *compute_ecmascript_module_exports(*self.full_module, Some(self.part.clone())) + .await? + .exports, + ) } #[turbo_tasks::function] @@ -391,18 +396,10 @@ impl ChunkableModule for EcmascriptModulePartAsset { impl EcmascriptModulePartAsset { #[turbo_tasks::function] pub(super) fn analyze(&self) -> Vc { - analyze(*self.full_module, self.part.clone()) + analyze_ecmascript_module(*self.full_module, Some(self.part.clone())) } } -#[turbo_tasks::function] -fn analyze( - module: Vc, - part: ModulePart, -) -> Vc { - analyze_ecmascript_module(module, Some(part)) -} - #[turbo_tasks::value_impl] impl EvaluatableAsset for EcmascriptModulePartAsset {} From 7df1a40bb7338a36482240f3742a7537458af1c9 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:00:04 +0200 Subject: [PATCH 3/3] Turbopack: cheaper check in map_next_dynamic (#93283) Currently doesn't have a measureable performance impact. Attempt the cheap downcast first and only do the ident reading and comparison in the uncommon case --- crates/next-api/src/dynamic_imports.rs | 62 +++++++++++++------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/crates/next-api/src/dynamic_imports.rs b/crates/next-api/src/dynamic_imports.rs index dad3bfdc4ad1..8a09e0816220 100644 --- a/crates/next-api/src/dynamic_imports.rs +++ b/crates/next-api/src/dynamic_imports.rs @@ -120,38 +120,36 @@ pub struct DynamicImportEntries( pub async fn map_next_dynamic( graph: ResolvedVc, ) -> Result> { - let actions = graph - .await? - .iter_nodes() - .map(|module| async move { - if module - .ident() - .await? - .layer - .as_ref() - .is_some_and(|layer| layer.name() == "app-client" || layer.name() == "client") - && let Some(dynamic_entry_module) = + let actions = + graph + .await? + .iter_nodes() + .map(|module| async move { + if let Some(dynamic_entry_module) = ResolvedVc::try_downcast_type::(module) - { - return Ok(Some(( - module, - DynamicImportEntriesMapType::DynamicEntry(dynamic_entry_module), - ))); - } - // TODO add this check once these modules have the correct layer - // if layer.is_some_and(|layer| &**layer == "app-rsc") { - if let Some(client_reference_module) = - ResolvedVc::try_downcast_type::(module) - { - return Ok(Some(( - module, - DynamicImportEntriesMapType::ClientReference(client_reference_module), - ))); - } - // } - Ok(None) - }) - .try_flat_join() - .await?; + && module.ident().await?.layer.as_ref().is_some_and(|layer| { + layer.name() == "app-client" || layer.name() == "client" + }) + { + return Ok(Some(( + module, + DynamicImportEntriesMapType::DynamicEntry(dynamic_entry_module), + ))); + } + // TODO add this check once these modules have the correct layer + // if layer.is_some_and(|layer| &**layer == "app-rsc") { + if let Some(client_reference_module) = + ResolvedVc::try_downcast_type::(module) + { + return Ok(Some(( + module, + DynamicImportEntriesMapType::ClientReference(client_reference_module), + ))); + } + // } + Ok(None) + }) + .try_flat_join() + .await?; Ok(Vc::cell(actions.into_iter().collect())) }