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())) } 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, 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 {}