From 053140802b6de514d6ebd1190e143cd7eb0b98a6 Mon Sep 17 00:00:00 2001 From: xusd320 Date: Mon, 27 Apr 2026 10:37:50 +0800 Subject: [PATCH] feat(turbopack): add async-to-promise transpilation bounds for old browser capability matching --- .../src/chunk/chunking_context.rs | 50 +-- .../crates/turbopack-core/src/chunk/mod.rs | 4 +- .../crates/turbopack-core/src/environment.rs | 104 ++++-- turbopack/crates/turbopack-core/src/ident.rs | 45 +-- .../turbopack-core/src/module_graph/mod.rs | 72 ++-- .../crates/turbopack-core/src/resolve/mod.rs | 78 +---- .../turbopack-core/src/resolve/origin.rs | 50 +-- .../turbopack-core/src/resolve/parse.rs | 14 - .../turbopack-core/src/source_map/utils.rs | 23 +- .../crates/turbopack-ecmascript/Cargo.toml | 3 +- .../src/analyzer/graph.rs | 144 +------- .../src/analyzer/imports.rs | 323 +++++++++++++++--- .../src/analyzer/well_known.rs | 44 +-- .../src/chunk/chunk_type.rs | 46 +-- .../turbopack-ecmascript/src/chunk/item.rs | 26 -- .../turbopack-ecmascript/src/code_gen.rs | 7 + .../crates/turbopack-ecmascript/src/lib.rs | 108 +++--- .../crates/turbopack-ecmascript/src/minify.rs | 54 +-- .../src/references/async_module.rs | 227 ++++++++++-- .../src/references/cjs.rs | 16 + .../src/references/esm/base.rs | 10 +- .../src/references/esm/dynamic.rs | 8 + .../src/references/esm/module_item.rs | 10 +- .../src/references/external_module.rs | 164 ++++----- .../src/references/mod.rs | 208 +++++------ .../src/references/pattern_mapping.rs | 8 - .../src/runtime_functions.rs | 6 +- .../turbopack-ecmascript/src/transform/mod.rs | 44 +-- .../src/tree_shake/graph.rs | 2 +- .../src/tree_shake/mod.rs | 33 +- 30 files changed, 933 insertions(+), 998 deletions(-) diff --git a/turbopack/crates/turbopack-core/src/chunk/chunking_context.rs b/turbopack/crates/turbopack-core/src/chunk/chunking_context.rs index d19a238a876..2a4c5a36136 100644 --- a/turbopack/crates/turbopack-core/src/chunk/chunking_context.rs +++ b/turbopack/crates/turbopack-core/src/chunk/chunking_context.rs @@ -50,59 +50,12 @@ pub enum MangleType { Deterministic, } -#[derive( - Debug, - TaskInput, - Clone, - Copy, - PartialEq, - Eq, - Hash, - Deserialize, - TraceRawVcs, - DeterministicHash, - NonLocalValue, - Encode, - Decode, -)] -#[serde(rename_all = "kebab-case")] -pub struct CompressOptions { - pub passes: Option, - pub sequences: Option, - pub keep_classnames: Option, - pub keep_fnames: Option, -} - -#[derive( - Debug, - TaskInput, - Clone, - Copy, - PartialEq, - Eq, - Hash, - Deserialize, - TraceRawVcs, - DeterministicHash, - NonLocalValue, - Encode, - Decode, -)] -#[serde(rename_all = "kebab-case")] -pub enum CompressType { - Default, - Options(CompressOptions), -} - #[turbo_tasks::value(shared)] #[derive(Debug, TaskInput, Clone, Copy, Hash, DeterministicHash, Deserialize)] pub enum MinifyType { // TODO instead of adding a new property here, // refactor that to Minify(MinifyOptions) to allow defaults on MinifyOptions - Minify { - mangle: Option, - compress: Option, - }, + Minify { mangle: Option }, NoMinify, } @@ -110,7 +63,6 @@ impl Default for MinifyType { fn default() -> Self { Self::Minify { mangle: Some(MangleType::OptimalSize), - compress: Some(CompressType::Default), } } } diff --git a/turbopack/crates/turbopack-core/src/chunk/mod.rs b/turbopack/crates/turbopack-core/src/chunk/mod.rs index 721cd820f3e..f456940c253 100644 --- a/turbopack/crates/turbopack-core/src/chunk/mod.rs +++ b/turbopack/crates/turbopack-core/src/chunk/mod.rs @@ -28,8 +28,8 @@ pub use crate::chunk::{ }, chunking_context::{ AssetSuffix, ChunkGroupResult, ChunkGroupType, ChunkingConfig, ChunkingConfigs, - ChunkingContext, ChunkingContextExt, CompressOptions, CompressType, EntryChunkGroupResult, - MangleType, MinifyType, SourceMapSourceType, SourceMapsType, UnusedReferences, UrlBehavior, + ChunkingContext, ChunkingContextExt, EntryChunkGroupResult, MangleType, MinifyType, + SourceMapSourceType, SourceMapsType, UnusedReferences, UrlBehavior, }, data::{ChunkData, ChunkDataOption, ChunksData}, evaluate::{EvaluatableAsset, EvaluatableAssetExt, EvaluatableAssets}, diff --git a/turbopack/crates/turbopack-core/src/environment.rs b/turbopack/crates/turbopack-core/src/environment.rs index 663d18b28d0..b24851fb5a6 100644 --- a/turbopack/crates/turbopack-core/src/environment.rs +++ b/turbopack/crates/turbopack-core/src/environment.rs @@ -357,6 +357,26 @@ impl EdgeWorkerEnvironment { #[turbo_tasks::value(transparent, serialization = "skip")] pub struct RuntimeVersions(#[turbo_tasks(trace_ignore)] pub Versions); +/// Checks if a browser version field is either absent or at least the given version. +/// Supports major-only, major.minor, and major.minor.patch comparisons. +macro_rules! version_at_least { + ($data:expr, $field:ident, $major:expr) => { + $data.$field.is_none_or(|v| v.major >= $major) + }; + ($data:expr, $field:ident, $major:expr, $minor:expr) => { + $data + .$field + .is_none_or(|v| v.major > $major || (v.major == $major && v.minor >= $minor)) + }; + ($data:expr, $field:ident, $major:expr, $minor:expr, $patch:expr) => { + $data.$field.is_none_or(|v| { + v.major > $major + || (v.major == $major && v.minor > $minor) + || (v.major == $major && v.minor == $minor && v.patch >= $patch) + }) + }; +} + #[turbo_tasks::value_impl] impl RuntimeVersions { /// Whether the environment supports arrow functions. @@ -376,22 +396,18 @@ impl RuntimeVersions { // "opera_mobile": "34", // "electron": "0.36" let data = &self.0; - let supported = data.chrome.is_none_or(|v| v.major >= 47) - && data.opera.is_none_or(|v| v.major >= 34) - && data.edge.is_none_or(|v| v.major >= 13) - && data.firefox.is_none_or(|v| v.major >= 43) - && data.safari.is_none_or(|v| v.major >= 10) - && data.node.is_none_or(|v| v.major >= 6) - && data.deno.is_none_or(|v| v.major >= 1) - && data.ios.is_none_or(|v| v.major >= 10) - && data.samsung.is_none_or(|v| v.major >= 5) - && data.rhino.is_none_or(|v| { - v.major > 1 - || (v.major == 1 && v.minor > 7) - || (v.major == 1 && v.minor == 7 && v.patch >= 13) - }) - && data.opera_mobile.is_none_or(|v| v.major >= 34) - && data.electron.is_none_or(|v| v.major > 0 || v.minor >= 36); + let supported = version_at_least!(data, chrome, 47) + && version_at_least!(data, opera, 34) + && version_at_least!(data, edge, 13) + && version_at_least!(data, firefox, 43) + && version_at_least!(data, safari, 10) + && version_at_least!(data, node, 6) + && version_at_least!(data, deno, 1) + && version_at_least!(data, ios, 10) + && version_at_least!(data, samsung, 5) + && version_at_least!(data, rhino, 1, 7, 13) + && version_at_least!(data, opera_mobile, 34) + && version_at_least!(data, electron, 0, 36); Vc::cell(supported) } @@ -412,19 +428,49 @@ impl RuntimeVersions { // "opera_mobile": "37", // "electron": "1.1" let data = &self.0; - let supported = data.chrome.is_none_or(|v| v.major >= 50) - && data.opera.is_none_or(|v| v.major >= 37) - && data.edge.is_none_or(|v| v.major >= 14) - && data.firefox.is_none_or(|v| v.major >= 53) - && data.safari.is_none_or(|v| v.major >= 11) - && data.node.is_none_or(|v| v.major >= 6) - && data.deno.is_none_or(|v| v.major >= 1) - && data.ios.is_none_or(|v| v.major >= 11) - && data.samsung.is_none_or(|v| v.major >= 5) - && data.opera_mobile.is_none_or(|v| v.major >= 37) - && data - .electron - .is_none_or(|v| v.major > 1 || (v.major == 1 && v.minor >= 1)); + let supported = version_at_least!(data, chrome, 50) + && version_at_least!(data, opera, 37) + && version_at_least!(data, edge, 14) + && version_at_least!(data, firefox, 53) + && version_at_least!(data, safari, 11) + && version_at_least!(data, node, 6) + && version_at_least!(data, deno, 1) + && version_at_least!(data, ios, 11) + && version_at_least!(data, samsung, 5) + && version_at_least!(data, opera_mobile, 37) + && version_at_least!(data, electron, 1, 1); + + Vc::cell(supported) + } + + /// Whether the environment supports async/await syntax. + #[turbo_tasks::function] + pub fn supports_async_await(&self) -> Vc { + // https://github.com/babel/babel/blob/b0e3517dc566880e76b5f1f4dcf7fcecba58337d/packages/babel-compat-data/data/plugins.json#L295-L307 + // "chrome": "55", + // "opera": "42", + // "edge": "15", + // "firefox": "52", + // "safari": "11", + // "node": "7.6", + // "deno": "1", + // "ios": "11", + // "samsung": "6", + // "opera_mobile": "42", + // "electron": "1.6" + let data = &self.0; + + let supported = version_at_least!(data, chrome, 55) + && version_at_least!(data, opera, 42) + && version_at_least!(data, edge, 15) + && version_at_least!(data, firefox, 52) + && version_at_least!(data, safari, 11) + && version_at_least!(data, node, 7, 6) + && version_at_least!(data, deno, 1) + && version_at_least!(data, ios, 11) + && version_at_least!(data, samsung, 6) + && version_at_least!(data, opera_mobile, 42) + && version_at_least!(data, electron, 1, 6); Vc::cell(supported) } diff --git a/turbopack/crates/turbopack-core/src/ident.rs b/turbopack/crates/turbopack-core/src/ident.rs index 1cb20b4c785..ee0bb10b9d3 100644 --- a/turbopack/crates/turbopack-core/src/ident.rs +++ b/turbopack/crates/turbopack-core/src/ident.rs @@ -10,7 +10,7 @@ use turbo_tasks::{ trace::TraceRawVcs, turbofmt, }; use turbo_tasks_fs::FileSystemPath; -use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher, encode_hex, hash_xxh3_hash64}; +use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher, encode_base38, hash_xxh3_hash64}; use crate::resolve::ModulePart; @@ -269,27 +269,6 @@ impl AssetIdent { fragment.deterministic_hash(&mut hasher); has_hash = true; } - if !assets.is_empty() { - // Use XOR to combine asset hashes in an order-independent way - // This ensures chunks with the same modules but different order get the same hash - let mut asset_hashes = Vec::with_capacity(assets.len()); - for (key, ident) in assets.iter() { - let mut asset_hasher = Xxh3Hash64Hasher::new(); - key.deterministic_hash(&mut asset_hasher); - ident - .to_string() - .await? - .deterministic_hash(&mut asset_hasher); - asset_hashes.push(asset_hasher.finish()); - } - asset_hashes.sort_unstable(); - - 2_u8.deterministic_hash(&mut hasher); - for h in asset_hashes { - h.deterministic_hash(&mut hasher); - } - has_hash = true; - } for (key, ident) in assets.iter() { 2_u8.deterministic_hash(&mut hasher); key.deterministic_hash(&mut hasher); @@ -357,8 +336,9 @@ impl AssetIdent { } if has_hash { - let hash = encode_hex(hasher.finish()); - let truncated_hash = &hash[..8]; + let hash = encode_base38(hasher.finish()); + // 7 base38 chars ≈ 36 bits of collision resistance + let truncated_hash = &hash[..7]; write!(name, "_{truncated_hash}")?; } @@ -379,17 +359,18 @@ impl AssetIdent { } } if i > 0 { - let hash = encode_hex(hash_xxh3_hash64(&name.as_bytes()[..i])); - let truncated_hash = &hash[..5]; + let hash = encode_base38(hash_xxh3_hash64(&name.as_bytes()[..i])); + // 4 base38 chars ≈ 21 bits — just a short disambiguator prefix + let truncated_hash = &hash[..4]; name = format!("{}_{}", truncated_hash, &name[i..]); } // We need to make sure that `.json` and `.json.js` doesn't end up with the same // name. So when we add an extra extension when want to mark that with a "._" // suffix. - // if !removed_extension { - // name += "._"; - // } - // name += &expected_extension; + if !removed_extension { + name += "._"; + } + name += &expected_extension; Ok(Vc::cell(name.into())) } } @@ -455,8 +436,8 @@ impl ValueToString for AssetIdent { } } -pub fn escape_file_path(s: &str) -> String { - static SEPARATOR_REGEX: Lazy = Lazy::new(|| Regex::new(r"[/#?:\[\]<>@\s()]").unwrap()); +fn escape_file_path(s: &str) -> String { + static SEPARATOR_REGEX: Lazy = Lazy::new(|| Regex::new(r"[/#?:]").unwrap()); SEPARATOR_REGEX.replace_all(s, "_").to_string() } diff --git a/turbopack/crates/turbopack-core/src/module_graph/mod.rs b/turbopack/crates/turbopack-core/src/module_graph/mod.rs index 95f4be07d79..85d37134e42 100644 --- a/turbopack/crates/turbopack-core/src/module_graph/mod.rs +++ b/turbopack/crates/turbopack-core/src/module_graph/mod.rs @@ -698,56 +698,21 @@ pub struct ModuleGraph { #[turbo_tasks::value_impl] impl ModuleGraph { + /// Analyze the module graph and potentially remove unused references (by determining the used + /// exports and removing unused imports). #[turbo_tasks::function(operation)] - pub async fn from_single_graph(graph: OperationVc) -> Result> { - let graph = Self::create(vec![graph], None) - .read_strongly_consistent() - .await?; - Ok(ReadRef::cell(graph)) - } - - #[turbo_tasks::function(operation)] - pub async fn from_graphs(graphs: Vec>) -> Result> { - let graph = Self::create(graphs, None) - .read_strongly_consistent() - .await?; - Ok(ReadRef::cell(graph)) - } - - /// Analyze the module graph and remove unused references (by determining the used exports and - /// removing unused imports). - /// - /// In particular, this removes ModuleReference-s that list only unused exports in the - /// `import_usage()` - #[turbo_tasks::function(operation)] - pub async fn from_single_graph_without_unused_references( - graph: OperationVc, - binding_usage: OperationVc, - ) -> Result> { - let graph = Self::create(vec![graph], Some(binding_usage)) - .read_strongly_consistent() - .await?; - Ok(ReadRef::cell(graph)) - } - - /// Analyze the module graph and remove unused references (by determining the used exports and - /// removing unused imports). - /// - /// In particular, this removes ModuleReference-s that list only unused exports in the - /// `import_usage()` - #[turbo_tasks::function(operation)] - pub async fn from_graphs_without_unused_references( + pub async fn from_graphs( graphs: Vec>, - binding_usage: OperationVc, + binding_usage: Option>, ) -> Result> { - let graph = Self::create(graphs, Some(binding_usage)) + let graph = Self::from_graphs_inner(graphs, binding_usage) .read_strongly_consistent() .await?; Ok(ReadRef::cell(graph)) } #[turbo_tasks::function(operation)] - async fn create( + async fn from_graphs_inner( graphs: Vec>, binding_usage: Option>, ) -> Result> { @@ -2103,15 +2068,18 @@ pub mod tests { false, ); - let module_graph = ModuleGraph::from_graphs(vec![ - parent_graph, - SingleModuleGraph::new_with_entries_visited( - ResolvedVc::cell(vec![ChunkGroupEntry::Entry(vec![a_module])]), - VisitedModules::from_graph(parent_graph), - false, - false, - ), - ]) + let module_graph = ModuleGraph::from_graphs( + vec![ + parent_graph, + SingleModuleGraph::new_with_entries_visited( + ResolvedVc::cell(vec![ChunkGroupEntry::Entry(vec![a_module])]), + VisitedModules::from_graph(parent_graph), + false, + false, + ), + ], + None, + ) .connect(); let child_graph = module_graph .iter_graphs() @@ -2366,7 +2334,9 @@ pub mod tests { .await? .into_iter() .collect(); - let module_graph = ModuleGraph::from_single_graph(graph).connect().await?; + let module_graph = ModuleGraph::from_graphs(vec![graph], None) + .connect() + .await?; Ok(SetupGraph { module_graph, diff --git a/turbopack/crates/turbopack-core/src/resolve/mod.rs b/turbopack/crates/turbopack-core/src/resolve/mod.rs index 5a0dd3f2e06..f4d20af2d58 100644 --- a/turbopack/crates/turbopack-core/src/resolve/mod.rs +++ b/turbopack/crates/turbopack-core/src/resolve/mod.rs @@ -20,7 +20,7 @@ use turbo_tasks::{ FxIndexMap, FxIndexSet, NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, ValueToStringRef, Vc, trace::TraceRawVcs, }; -use turbo_tasks_fs::{DiskFileSystem, FileSystem, FileSystemEntryType, FileSystemPath}; +use turbo_tasks_fs::{FileSystemEntryType, FileSystemPath}; use turbo_unix_path::normalize_request; use crate::{ @@ -479,7 +479,6 @@ pub enum ExternalType { EcmaScriptModule, Global, Script, - Umd, } impl Display for ExternalType { @@ -490,7 +489,6 @@ impl Display for ExternalType { ExternalType::Url => write!(f, "url"), ExternalType::Global => write!(f, "global"), ExternalType::Script => write!(f, "script"), - ExternalType::Umd => write!(f, "umd"), } } } @@ -2055,77 +2053,18 @@ async fn resolve_internal_inline( .await? } Request::Windows { - path, - query, - fragment, + path: _, + query: _, + fragment: _, } => { - if let Some(path_str) = path.as_constant_string() { - let sys_path = std::path::Path::new(path_str.as_str()); - - let mut candidate_disk_fses = Vec::new(); - - if let Some(disk_fs_vc) = - ResolvedVc::try_downcast_type::(lookup_path.fs) - { - candidate_disk_fses.push(disk_fs_vc); - } - - for module in &options_value.modules { - let fs = match module { - ResolveModules::Nested(root, _) => root.fs, - ResolveModules::Path { dir, .. } => dir.fs, - }; - if let Some(disk_fs_vc) = - ResolvedVc::try_downcast_type::(fs) - { - candidate_disk_fses.push(disk_fs_vc); - } - } - - for disk_fs_vc in candidate_disk_fses { - let disk_fs = disk_fs_vc.await?; - if let Some(fs_path) = disk_fs.try_from_sys_path(disk_fs_vc, sys_path, None) - { - let root_path = disk_fs_vc.root().owned().await?; - let (relative_lookup_path, relative_path) = if let Some(relative_path) = - lookup_path.get_relative_path_to(&fs_path) - { - (lookup_path.clone(), relative_path) - } else if let Some(relative_path) = - root_path.get_relative_path_to(&fs_path) - { - (root_path.clone(), relative_path) - } else { - continue; - }; - - return resolve_relative_request( - relative_lookup_path, - request, - options, - options_value, - &Pattern::Constant(relative_path), - query.clone(), - false, - fragment.clone(), - ) - .await; - } - } - } - if !has_alias { ResolvingIssue { severity: resolve_error_severity(options).await?, - request_type: "windows import".to_string(), + request_type: "windows import: not implemented yet".to_string(), request: request.to_resolved().await?, file_path: lookup_path.clone(), resolve_options: options.to_resolved().await?, - error_message: Some( - "Windows absolute path imports can only be resolved if the path is \ - within the project root. Please use a relative path instead." - .to_string(), - ), + error_message: Some("windows imports are not implemented yet".to_string()), source: None, } .resolved_cell() @@ -3084,10 +3023,7 @@ async fn resolve_import_map_result( ExternalType::EcmaScriptModule => { node_esm_resolve_options(alias_lookup_path.root().owned().await?) } - ExternalType::Script - | ExternalType::Url - | ExternalType::Global - | ExternalType::Umd => options, + ExternalType::Script | ExternalType::Url | ExternalType::Global => options, }, ) .await? diff --git a/turbopack/crates/turbopack-core/src/resolve/origin.rs b/turbopack/crates/turbopack-core/src/resolve/origin.rs index f6379060c02..7e8b8868a98 100644 --- a/turbopack/crates/turbopack-core/src/resolve/origin.rs +++ b/turbopack/crates/turbopack-core/src/resolve/origin.rs @@ -6,7 +6,7 @@ use turbo_tasks::{ResolvedVc, Upcast, Vc}; use turbo_tasks_fs::FileSystemPath; use super::{ModuleResolveResult, options::ResolveOptions, parse::Request}; -use crate::{context::AssetContext, module::OptionModule, reference_type::ReferenceType}; +use crate::{context::AssetContext, reference_type::ReferenceType}; /// A location where resolving can occur from. It carries some meta information /// that are needed for resolving from here. @@ -24,14 +24,6 @@ pub trait ResolveOrigin { #[turbo_tasks::function] fn asset_context(self: Vc) -> Vc>; - /// Get an inner asset form this origin that doesn't require resolving but - /// is directly attached - #[turbo_tasks::function] - fn get_inner_asset(self: Vc, request: Vc) -> Vc { - let _ = request; - Vc::cell(None) - } - /// Get the resolve options that apply for this origin. #[turbo_tasks::function] async fn resolve_options(self: Vc) -> Result> { @@ -62,18 +54,18 @@ impl ResolveOriginExt for T where T: ResolveOrigin + Upcast>, { - fn resolve_asset( + async fn resolve_asset( self: Vc, request: Vc, options: Vc, reference_type: ReferenceType, - ) -> impl Future>> + Send { - resolve_asset( - Vc::upcast_non_strict(self), - request, - options, + ) -> Result> { + Ok(self.asset_context().to_resolved().await?.resolve_asset( + self.origin_path().owned().await?, + *request.to_resolved().await?, + *options.to_resolved().await?, reference_type, - ) + )) } fn with_transition(self: ResolvedVc, transition: RcStr) -> Vc> { @@ -87,27 +79,6 @@ where } } -async fn resolve_asset( - resolve_origin: Vc>, - request: Vc, - options: Vc, - reference_type: ReferenceType, -) -> Result> { - if let Some(asset) = *resolve_origin.get_inner_asset(request).await? { - return Ok(*ModuleResolveResult::module(asset)); - } - Ok(resolve_origin - .asset_context() - .to_resolved() - .await? - .resolve_asset( - resolve_origin.origin_path().owned().await?, - *request.to_resolved().await?, - *options.to_resolved().await?, - reference_type, - )) -} - /// A resolve origin for some path and context without additional modifications. #[turbo_tasks::value] pub struct PlainResolveOrigin { @@ -163,9 +134,4 @@ impl ResolveOrigin for ResolveOriginWithTransition { .asset_context() .with_transition(self.transition.clone()) } - - #[turbo_tasks::function] - fn get_inner_asset(&self, request: Vc) -> Vc { - self.previous.get_inner_asset(request) - } } diff --git a/turbopack/crates/turbopack-core/src/resolve/parse.rs b/turbopack/crates/turbopack-core/src/resolve/parse.rs index 380673ff9bd..6807001d003 100644 --- a/turbopack/crates/turbopack-core/src/resolve/parse.rs +++ b/turbopack/crates/turbopack-core/src/resolve/parse.rs @@ -163,20 +163,6 @@ impl Request { return Request::Empty; } - // Handle webpack-style tilde prefix (~) for node_modules resolution - // This is commonly used in CSS preprocessors like less-loader and sass-loader - // Only strip ~ if it's followed by a module path (not a relative path like ~/home) - // for utoopack issue: https://github.com/utooland/utoo/issues/2309 - let r = if let Some(remainder) = r - .strip_prefix('~') - .filter(|s| !s.is_empty() && !s.starts_with('/') && !s.starts_with('\\')) - .filter(|s| MODULE_PATH.is_match(s)) - { - remainder.into() - } else { - r - }; - if let Some(remainder) = r.strip_prefix("//") { return Request::Uri { protocol: rcstr!("//"), diff --git a/turbopack/crates/turbopack-core/src/source_map/utils.rs b/turbopack/crates/turbopack-core/src/source_map/utils.rs index 32ac7a26689..d967e219877 100644 --- a/turbopack/crates/turbopack-core/src/source_map/utils.rs +++ b/turbopack/crates/turbopack-core/src/source_map/utils.rs @@ -114,24 +114,15 @@ pub async fn resolve_source_map_sources( let fs_path = if let Ok(original_source_url_obj) = Url::parse(&maybe_file_url) { // We have an absolute URL, try to parse it as a `file://` URL - #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] - { - if let Ok(sys_path) = original_source_url_obj.to_file_path() { - if let Some((disk_fs_vc, disk_fs)) = disk_fs { - disk_fs.try_from_sys_path(*disk_fs_vc, &sys_path, Some(origin)) - } else { - None - } + if let Ok(sys_path) = original_source_url_obj.to_file_path() { + if let Some((disk_fs_vc, disk_fs)) = disk_fs { + disk_fs.try_from_sys_path(*disk_fs_vc, &sys_path, Some(origin)) } else { - // this is an absolute URL with a non-`file://` scheme, just assume it's valid - // and don't modify anything - return Ok(()); + None } - } - #[cfg(all(target_family = "wasm", target_os = "unknown"))] - { - // On WASM targets, to_file_path() is not available, - // just assume it's a valid absolute URL and don't modify anything + } else { + // this is an absolute URL with a non-`file://` scheme, just assume it's valid + // and don't modify anything return Ok(()); } } else { diff --git a/turbopack/crates/turbopack-ecmascript/Cargo.toml b/turbopack/crates/turbopack-ecmascript/Cargo.toml index f848a9c8d37..95dddb6fbb2 100644 --- a/turbopack/crates/turbopack-ecmascript/Cargo.toml +++ b/turbopack/crates/turbopack-ecmascript/Cargo.toml @@ -45,7 +45,7 @@ serde_json = { workspace = true, features = ["raw_value"] } swc_sourcemap = { workspace = true } smallvec = { workspace = true } strsim = { workspace = true } -tokio = { workspace = true, features = ["io-util"] } +tokio = { workspace = true } tracing = { workspace = true } turbo-bincode = { workspace = true } turbo-esregex = { workspace = true } @@ -92,4 +92,3 @@ turbo-tasks-backend = { workspace = true } turbo-tasks-malloc = { workspace = true } turbo-tasks-testing = { workspace = true } turbopack-test-utils = { workspace = true } -tokio = { workspace = true } diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph.rs index a0bd98fbe06..454bd3667d2 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph.rs @@ -31,9 +31,7 @@ use crate::{ AnalyzeMode, SpecifiedModuleType, analyzer::{WellKnownObjectKind, is_unresolved}, code_gen::CodeGen, - references::{ - constant_value::parse_single_expr_lit, esm::EsmModuleItem, for_each_ident_in_pat, - }, + references::{constant_value::parse_single_expr_lit, esm::EsmModuleItem}, utils::{AstPathRange, unparen}, }; @@ -308,30 +306,6 @@ impl AssignmentScopes { } } -#[derive(Clone, Debug)] -pub enum DeclUsage { - SideEffects, - Bindings(FxHashSet), -} -impl Default for DeclUsage { - fn default() -> Self { - DeclUsage::Bindings(Default::default()) - } -} -impl DeclUsage { - fn add_usage(&mut self, user: &Id) { - match self { - Self::Bindings(set) => { - set.insert(user.clone()); - } - Self::SideEffects => {} - } - } - fn make_side_effects(&mut self) { - *self = Self::SideEffects; - } -} - #[derive(Debug)] pub struct VarGraph { pub values: FxHashMap, @@ -345,13 +319,6 @@ pub struct VarGraph { pub effects: Vec, // Some unconditional codegens, usually for ESM items. pub code_gens: Vec, - - // ident -> immediate usage (top level decl) - pub decl_usages: FxHashMap, - // import -> immediate usage (top level decl) - pub import_usages: FxHashMap, - // export name -> top level decl - pub exports: FxHashMap, } impl VarGraph { @@ -376,9 +343,6 @@ pub fn create_graph( free_var_ids: Default::default(), effects: Default::default(), code_gens: Default::default(), - decl_usages: Default::default(), - import_usages: Default::default(), - exports: Default::default(), }; m.visit_with_ast_path( @@ -429,7 +393,9 @@ impl EvalContext { Self { unresolved_mark, top_level_mark, - imports: module.map_or(ImportMap::default(), |m| ImportMap::analyze(m, comments)), + imports: module.map_or(ImportMap::default(), |m| { + ImportMap::analyze(unresolved_mark, m, comments) + }), force_free_values, } } @@ -1014,15 +980,6 @@ mod analyzer_state { early_return_stack: Vec, lexical_stack: Vec, var_decl_kind: Option, - - cur_top_level_decl_name: Option, - } - - impl AnalyzerState { - /// Returns the identifier of the current top level declaration. - pub(super) fn cur_top_level_decl_name(&self) -> &Option { - &self.cur_top_level_decl_name - } } impl Analyzer<'_> { @@ -1317,23 +1274,6 @@ mod analyzer_state { } always_returns } - - /// Runs `visitor` with the current top level declaration identifier - pub(super) fn enter_top_level_decl( - &mut self, - name: &Ident, - visitor: impl FnOnce(&mut Self) -> T, - ) -> T { - let is_top_level_fn = self.state.cur_top_level_decl_name.is_none(); - if is_top_level_fn { - self.state.cur_top_level_decl_name = Some(name.to_id()); - } - let result = visitor(self); - if is_top_level_fn { - self.state.cur_top_level_decl_name = None; - } - result - } } } @@ -2128,10 +2068,8 @@ impl VisitAstPath for Analyzer<'_> { decl: &'ast FnDecl, ast_path: &mut AstNodePath>, ) { - let fn_value = self.enter_top_level_decl(&decl.ident, |this| { - this.enter_fn(&*decl.function, |this| { - decl.visit_children_with_ast_path(this, ast_path); - }) + let fn_value = self.enter_fn(&*decl.function, |this| { + decl.visit_children_with_ast_path(this, ast_path); }); // Take all effects produced by the function and move them to hoisted effects since @@ -2608,17 +2546,6 @@ impl VisitAstPath for Analyzer<'_> { if let Some((esm_reference_index, export)) = self.eval_context.imports.get_binding(&ident.to_id()) { - let usage = self - .data - .import_usages - .entry(esm_reference_index) - .or_default(); - if let Some(top_level) = self.state.cur_top_level_decl_name() { - usage.add_usage(top_level); - } else { - usage.make_side_effects(); - } - // Optimization: Look for a MemberExpr to see if we only access a few members from the // module, add those specific effects instead of depending on the entire module. // @@ -2645,7 +2572,7 @@ impl VisitAstPath for Analyzer<'_> { } else { self.add_effect(Effect::ImportedBinding { esm_reference_index, - export, + export: export.map(|e| RcStr::from(e.as_str())), ast_path: as_parent_path(ast_path), span: ident.span(), }) @@ -2665,24 +2592,6 @@ impl VisitAstPath for Analyzer<'_> { span: ident.span(), }) } - - if !is_unresolved(ident, self.eval_context.unresolved_mark) { - if let Some(top_level) = self.state.cur_top_level_decl_name() { - if !(ident.sym == top_level.0 && ident.ctxt == top_level.1) { - self.data - .decl_usages - .entry(ident.to_id()) - .or_default() - .add_usage(top_level); - } - } else { - self.data - .decl_usages - .entry(ident.to_id()) - .or_default() - .make_side_effects(); - } - } } fn visit_this_expr<'ast: 'r, 'r>( @@ -2998,32 +2907,6 @@ impl VisitAstPath for Analyzer<'_> { node: &'ast ExportDecl, ast_path: &mut swc_core::ecma::visit::AstNodePath<'r>, ) { - match &node.decl { - Decl::Class(node) => { - self.data - .exports - .insert(node.ident.sym.clone(), node.ident.to_id()); - } - Decl::Fn(node) => { - self.data - .exports - .insert(node.ident.sym.clone(), node.ident.to_id()); - } - Decl::Var(node) => { - for VarDeclarator { name, .. } in &node.decls { - for_each_ident_in_pat(name, &mut |name, ctxt| { - self.data.exports.insert(name.clone(), (name.clone(), ctxt)); - }); - } - } - Decl::Using(_) => { - // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export#:~:text=You%20cannot%20use%20export%20on%20a%20using%20or%20await%20using%20declaration - unreachable!("using declarations can not be exported"); - } - Decl::TsInterface(_) | Decl::TsTypeAlias(_) | Decl::TsEnum(_) | Decl::TsModule(_) => { - // ignore typescript for code generation - } - }; self.add_esm_module_item(ast_path); node.visit_children_with_ast_path(self, ast_path); } @@ -3036,19 +2919,6 @@ impl VisitAstPath for Analyzer<'_> { if node.is_type_only { return; } - let export_name = node - .exported - .as_ref() - .unwrap_or(&node.orig) - .atom() - .into_owned(); - self.data.exports.insert( - export_name, - match &node.orig { - ModuleExportName::Ident(ident) => ident.to_id(), - ModuleExportName::Str(_) => unreachable!("exporting a string should be impossible"), - }, - ); node.visit_children_with_ast_path(self, ast_path); } diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs index 1c548e9f250..175544b9423 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs @@ -11,7 +11,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::SmallVec; use swc_core::{ atoms::Wtf8Atom, - common::{BytePos, Span, Spanned, SyntaxContext, comments::Comments}, + common::{BytePos, Mark, Span, Spanned, SyntaxContext, comments::Comments}, ecma::{ ast::*, atoms::{Atom, atom}, @@ -22,7 +22,7 @@ use swc_core::{ use turbo_frozenmap::FrozenMap; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc}; -use turbopack_core::loader::WebpackLoaderItem; +use turbopack_core::{loader::WebpackLoaderItem, resolve::ImportUsage}; use super::{JsValue, ModuleValue, top_level_await::has_top_level_await}; use crate::{ @@ -30,6 +30,7 @@ use crate::{ analyzer::{ ConstantValue, ObjectPart, graph::{AssignmentScope, AssignmentScopes, EvalContext}, + is_unresolved, }, magic_identifier::{MAGIC_IDENTIFIER_DEFAULT_EXPORT, MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM}, references::{ @@ -254,6 +255,88 @@ impl Display for ImportAnnotations { } } +#[derive(Clone, Debug)] +pub enum DeclUsage { + SideEffects, + Bindings(FxHashSet), +} +impl Default for DeclUsage { + fn default() -> Self { + DeclUsage::Bindings(Default::default()) + } +} +impl DeclUsage { + fn add_usage(&mut self, user: &Id) { + match self { + Self::Bindings(set) => { + set.insert(user.clone()); + } + Self::SideEffects => {} + } + } + fn make_side_effects(&mut self) { + *self = Self::SideEffects; + } +} + +#[derive(Default, Debug)] +pub(crate) struct ProgramDeclUsage { + // ident -> immediate usage (top level decl) + pub(crate) decl_usages: FxHashMap, + // import -> immediate usage (top level decl) + pub(crate) import_usages: FxHashMap, + // export name -> top level decl + pub(crate) exports: FxHashMap, +} +impl ProgramDeclUsage { + fn compute_import_usage(&self) -> FxHashMap { + let mut import_usage = + FxHashMap::with_capacity_and_hasher(self.import_usages.len(), Default::default()); + for (reference, usage) in &self.import_usages { + // TODO make this more efficient, i.e. cache the result? + if let DeclUsage::Bindings(ids) = usage { + // compute transitive closure of `ids` over `top_level_mappings` + let mut visited = ids.clone(); + let mut stack = ids.iter().collect::>(); + let mut has_global_usage = false; + while let Some(id) = stack.pop() { + match self.decl_usages.get(id) { + Some(DeclUsage::SideEffects) => { + has_global_usage = true; + break; + } + Some(DeclUsage::Bindings(callers)) => { + for caller in callers { + if visited.insert(caller.clone()) { + stack.push(caller); + } + } + } + _ => {} + } + } + + // Collect all `visited` declarations which are exported + import_usage.insert( + *reference, + if has_global_usage { + ImportUsage::TopLevel + } else { + ImportUsage::Exports( + self.exports + .iter() + .filter(|(_, id)| visited.contains(*id)) + .map(|(exported, _)| exported.clone()) + .collect(), + ) + }, + ); + } + } + import_usage + } +} + /// A version of [crate::references::esm::export::EsmExport] with usize instead of the module /// reference Vc, and missing the liveness fields. #[derive(Debug)] @@ -323,6 +406,8 @@ pub(crate) struct ImportMap { /// whether an export is live or not. pub(super) assignment_scopes: FxHashMap, + pub(crate) import_usage: FxHashMap, + /// Map from exported name to local binding id (includes the syntax context). pub(crate) exports_ids: FxHashMap, } @@ -465,10 +550,9 @@ impl ImportMap { self.attributes.get(&span.lo).unwrap_or_default() } - // TODO this could return &str instead of String to avoid cloning - pub fn get_binding(&self, id: &Id) -> Option<(usize, Option)> { + pub fn get_binding(&self, id: &Id) -> Option<(usize, Option<&Atom>)> { if let Some((i, i_sym)) = self.imports.get(id) { - return Some((*i, Some(i_sym.as_str().into()))); + return Some((*i, Some(i_sym))); } if let Some(i) = self.namespace_imports.get(id) { return Some((*i, None)); @@ -555,13 +639,19 @@ impl ImportMap { } /// Analyze ES import - pub(super) fn analyze(m: &Program, comments: Option<&dyn Comments>) -> Self { + pub(super) fn analyze( + unresolved_mark: Mark, + m: &Program, + comments: Option<&dyn Comments>, + ) -> Self { let mut data = ImportMap::default(); let mut analyzer = Analyzer { + unresolved_mark, data: &mut data, comments, namespace_imports_to_specifier: FxIndexMap::default(), - is_in_fn: false, + state: Default::default(), + program_decl_usage: Default::default(), }; // A prepass to detect imports to be able to rewrite import+export pairs to true reexports @@ -665,6 +755,8 @@ impl ImportMap { m.visit_with(&mut analyzer); + data.import_usage = analyzer.program_decl_usage.compute_import_usage(); + data } @@ -675,14 +767,69 @@ impl ImportMap { } } +mod analyzer_state { + use swc_core::ecma::ast::{Id, Ident}; + + use super::Analyzer; + + #[derive(Default)] + pub(super) struct AnalyzerState { + is_in_fn: bool, + cur_top_level_decl_name: Option, + } + + impl AnalyzerState { + /// Returns the identifier of the current top level declaration. + pub(super) fn cur_top_level_decl_name(&self) -> &Option { + &self.cur_top_level_decl_name + } + + /// Returns whether the current context is inside a function. + pub(super) fn is_in_fn(&self) -> bool { + self.is_in_fn + } + } + + impl Analyzer<'_> { + /// Runs `visitor` with the current top level declaration identifier + pub(super) fn enter_top_level_decl( + &mut self, + name: &Ident, + visitor: impl FnOnce(&mut Self) -> T, + ) -> T { + let is_top_level_fn = self.state.cur_top_level_decl_name.is_none(); + if is_top_level_fn { + self.state.cur_top_level_decl_name = Some(name.to_id()); + } + let result = visitor(self); + if is_top_level_fn { + self.state.cur_top_level_decl_name = None; + } + result + } + + /// Runs `visitor` with the right is_in_fn value + pub(super) fn enter_fn(&mut self, visitor: impl FnOnce(&mut Self) -> T) -> T { + let old_is_in_fn = self.state.is_in_fn; + self.state.is_in_fn = true; + let result = visitor(self); + self.state.is_in_fn = old_is_in_fn; + result + } + } +} + struct Analyzer<'a> { + unresolved_mark: Mark, data: &'a mut ImportMap, comments: Option<&'a dyn Comments>, /// Map from local identifier of namespace imports to module path, used temporarily during /// analysis to detect dynamic accesses to namespace imports. namespace_imports_to_specifier: FxIndexMap, - is_in_fn: bool, + program_decl_usage: ProgramDeclUsage, + + state: analyzer_state::AnalyzerState, } impl Analyzer<'_> { @@ -709,7 +856,7 @@ impl Analyzer<'_> { } fn register_assignment_scope(&mut self, id: Id) { - let scope = if self.is_in_fn { + let scope = if self.state.is_in_fn() { AssignmentScope::Function } else { AssignmentScope::ModuleEval @@ -727,6 +874,10 @@ impl Analyzer<'_> { } impl Visit for Analyzer<'_> { + fn visit_import_decl(&mut self, _: &ImportDecl) { + // We already handled import above. Skip as the Idents in here confuse the analysis + } + fn visit_export_all(&mut self, export: &ExportAll) { if export.type_only { return; @@ -743,6 +894,7 @@ impl Visit for Analyzer<'_> { ); self.data.reexport_namespaces.push(i); self.data.has_exports = true; + export.visit_children_with(self); } fn visit_named_export(&mut self, export: &NamedExport) { @@ -830,7 +982,11 @@ impl Visit for Analyzer<'_> { // This is a export of an imported binding. Rewrite to a true // reexport. if let Some(export) = export { - Export::ImportedBinding(index, export, is_fake_esm) + Export::ImportedBinding( + index, + RcStr::from(export.as_str()), + is_fake_esm, + ) } else { Export::ImportedNamespace(index) } @@ -850,23 +1006,27 @@ impl Visit for Analyzer<'_> { } fn visit_export_decl(&mut self, n: &ExportDecl) { - n.visit_children_with(self); self.data.has_exports = true; - match &n.decl { Decl::Class(n) => { let name = RcStr::from(n.ident.sym.as_str()); self.data .exports .insert(name.clone(), Export::LocalBinding(name.clone(), false)); - self.data.exports_ids.insert(name, n.ident.to_id()); + self.data.exports_ids.insert(name.clone(), n.ident.to_id()); + self.program_decl_usage + .exports + .insert(name, n.ident.to_id()); } Decl::Fn(n) => { let name = RcStr::from(n.ident.sym.as_str()); self.data .exports .insert(name.clone(), Export::LocalBinding(name.clone(), false)); - self.data.exports_ids.insert(name, n.ident.to_id()); + self.data.exports_ids.insert(name.clone(), n.ident.to_id()); + self.program_decl_usage + .exports + .insert(name, n.ident.to_id()); } Decl::Var(..) => { let ids: Vec = find_pat_ids(&n.decl); @@ -875,7 +1035,8 @@ impl Visit for Analyzer<'_> { self.data .exports .insert(name.clone(), Export::LocalBinding(name.clone(), false)); - self.data.exports_ids.insert(name, id); + self.data.exports_ids.insert(name.clone(), id.clone()); + self.program_decl_usage.exports.insert(name, id); } } Decl::Using(_) => { @@ -886,10 +1047,11 @@ impl Visit for Analyzer<'_> { // ignore typescript for code generation } } + + n.visit_children_with(self); } fn visit_export_default_decl(&mut self, n: &ExportDefaultDecl) { - n.visit_children_with(self); self.data.has_exports = true; let id = match &n.decl { @@ -919,11 +1081,14 @@ impl Visit for Analyzer<'_> { rcstr!("default"), Export::LocalBinding(RcStr::from(id.0.as_str()), false), ); - self.data.exports_ids.insert(rcstr!("default"), id); + self.data.exports_ids.insert(rcstr!("default"), id.clone()); + self.program_decl_usage + .exports + .insert(rcstr!("default"), id); + n.visit_children_with(self); } fn visit_export_default_expr(&mut self, n: &ExportDefaultExpr) { - n.visit_children_with(self); self.data.has_exports = true; self.data.exports.insert( @@ -938,21 +1103,32 @@ impl Visit for Analyzer<'_> { SyntaxContext::empty(), ), ); + n.visit_children_with(self); } fn visit_export_named_specifier(&mut self, n: &ExportNamedSpecifier) { - if let ModuleExportName::Ident(local) = &n.orig { - let exported = n.exported.as_ref().unwrap_or(&n.orig); - self.data - .exports_ids - .insert(exported.atom().as_str().into(), local.to_id()); - } + self.data.has_exports = true; + + let ModuleExportName::Ident(local) = &n.orig else { + unreachable!("exporting a string should be impossible") + }; + let exported = RcStr::from(n.exported.as_ref().unwrap_or(&n.orig).atom().as_str()); + self.data + .exports_ids + .insert(exported.clone(), local.to_id()); + self.program_decl_usage + .exports + .insert(exported, local.to_id()); + n.visit_children_with(self); } fn visit_export_default_specifier(&mut self, n: &ExportDefaultSpecifier) { + self.data.has_exports = true; + self.data .exports_ids .insert(rcstr!("default"), n.exported.to_id()); + n.visit_children_with(self); } fn visit_program(&mut self, m: &Program) { @@ -1022,42 +1198,42 @@ impl Visit for Analyzer<'_> { } fn visit_getter_prop(&mut self, node: &GetterProp) { - let old_is_in_fn = self.is_in_fn; - self.is_in_fn = true; - node.visit_children_with(self); - self.is_in_fn = old_is_in_fn; + self.enter_fn(|this| { + node.visit_children_with(this); + }); } fn visit_setter_prop(&mut self, node: &SetterProp) { - let old_is_in_fn = self.is_in_fn; - self.is_in_fn = true; - node.visit_children_with(self); - self.is_in_fn = old_is_in_fn; + self.enter_fn(|this| { + node.visit_children_with(this); + }); } fn visit_function(&mut self, node: &Function) { - let old_is_in_fn = self.is_in_fn; - self.is_in_fn = true; - node.visit_children_with(self); - self.is_in_fn = old_is_in_fn; + self.enter_fn(|this| { + node.visit_children_with(this); + }); } fn visit_constructor(&mut self, node: &Constructor) { - let old_is_in_fn = self.is_in_fn; - self.is_in_fn = true; - node.visit_children_with(self); - self.is_in_fn = old_is_in_fn; + self.enter_fn(|this| { + node.visit_children_with(this); + }); } fn visit_arrow_expr(&mut self, node: &ArrowExpr) { - let old_is_in_fn = self.is_in_fn; - self.is_in_fn = true; - node.visit_children_with(self); - self.is_in_fn = old_is_in_fn; + self.enter_fn(|this| { + node.visit_children_with(this); + }); } fn visit_member_expr(&mut self, node: &MemberExpr) { if let MemberProp::Ident(..) | MemberProp::PrivateName(..) = &node.prop && node.obj.is_ident() { - // Skip if obj is a Expr::Ident, so that it doesn't get added to full_star_imports below - // in visit_expr. + // Skip traversing if obj is a Expr::Ident, so that it doesn't get added to + // full_star_imports below in visit_expr. + + // TODO this currently doesn't properly mark the import in self.program_decl_usage, see + // todo in + // turbopack/crates/turbopack-tests/tests/execution/turbopack/remove-unused-imports/ + // import-star/input/index.js return; } node.visit_children_with(self); @@ -1069,9 +1245,8 @@ impl Visit for Analyzer<'_> { if let Some(module_path) = self.namespace_imports_to_specifier.get(&i.to_id()) { self.data.full_star_imports.insert(module_path.clone()); } - } else { - pat.visit_children_with(self); } + pat.visit_children_with(self); } fn visit_simple_assign_target(&mut self, node: &SimpleAssignTarget) { @@ -1080,18 +1255,52 @@ impl Visit for Analyzer<'_> { if let Some(module_path) = self.namespace_imports_to_specifier.get(&i.to_id()) { self.data.full_star_imports.insert(module_path.clone()); } - } else { - node.visit_children_with(self); } + node.visit_children_with(self); } fn visit_expr(&mut self, node: &Expr) { - if let Expr::Ident(i) = node { - if let Some(module_path) = self.namespace_imports_to_specifier.get(&i.to_id()) { - self.data.full_star_imports.insert(module_path.clone()); + if let Expr::Ident(i) = node + && let Some(module_path) = self.namespace_imports_to_specifier.get(&i.to_id()) + { + self.data.full_star_imports.insert(module_path.clone()); + } + node.visit_children_with(self); + } + + fn visit_ident(&mut self, node: &Ident) { + let id = node.to_id(); + if let Some((esm_reference_index, _)) = self.data.get_binding(&id) { + // An import binding + let usage = self + .program_decl_usage + .import_usages + .entry(esm_reference_index) + .or_default(); + if let Some(top_level) = self.state.cur_top_level_decl_name() { + usage.add_usage(top_level); + } else { + usage.make_side_effects(); } } else { - node.visit_children_with(self); + // A regular variable + if !is_unresolved(node, self.unresolved_mark) { + if let Some(top_level) = self.state.cur_top_level_decl_name() { + if &id != top_level { + self.program_decl_usage + .decl_usages + .entry(id) + .or_default() + .add_usage(top_level); + } + } else { + self.program_decl_usage + .decl_usages + .entry(id) + .or_default() + .make_side_effects(); + } + } } } @@ -1102,6 +1311,12 @@ impl Visit for Analyzer<'_> { node.visit_children_with(self); } + fn visit_fn_decl(&mut self, node: &FnDecl) { + self.enter_top_level_decl(&node.ident, |this| { + node.visit_children_with(this); + }); + } + fn visit_decl(&mut self, node: &Decl) { match node { Decl::Class(c) => { diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known.rs index 656b380b670..2579736d852 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known.rs @@ -4,13 +4,8 @@ use anyhow::Result; use turbo_rcstr::rcstr; use turbo_tasks::Vc; use turbopack_core::compile_time_info::CompileTimeInfo; -#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] use url::Url; -#[cfg_attr( - all(target_family = "wasm", target_os = "unknown"), - allow(unused_imports) -)] use super::{ ConstantValue, JsValue, JsValueUrlKind, ModuleValue, WellKnownFunctionKind, WellKnownObjectKind, }; @@ -522,34 +517,23 @@ fn require_context_require_resolve( Ok(m.as_str().into()) } -#[cfg_attr( - all(target_family = "wasm", target_os = "unknown"), - allow(unused_variables) -)] fn path_to_file_url(args: Vec) -> JsValue { if args.len() == 1 { if let Some(path) = args[0].as_str() { - #[cfg(all(target_family = "wasm", target_os = "unknown"))] - { - unreachable!() - } - #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] - { - Url::from_file_path(path) - .map(|url| JsValue::Url(String::from(url).into(), JsValueUrlKind::Absolute)) - .unwrap_or_else(|_| { - JsValue::unknown( - JsValue::call( - Box::new(JsValue::WellKnownFunction( - WellKnownFunctionKind::PathToFileUrl, - )), - args, - ), - true, - "url not parseable: path is relative or has an invalid prefix", - ) - }) - } + Url::from_file_path(path) + .map(|url| JsValue::Url(String::from(url).into(), JsValueUrlKind::Absolute)) + .unwrap_or_else(|_| { + JsValue::unknown( + JsValue::call( + Box::new(JsValue::WellKnownFunction( + WellKnownFunctionKind::PathToFileUrl, + )), + args, + ), + true, + "url not parseable: path is relative or has an invalid prefix", + ) + }) } else { JsValue::unknown( JsValue::call( diff --git a/turbopack/crates/turbopack-ecmascript/src/chunk/chunk_type.rs b/turbopack/crates/turbopack-ecmascript/src/chunk/chunk_type.rs index 79475993fd1..b8912b2285f 100644 --- a/turbopack/crates/turbopack-ecmascript/src/chunk/chunk_type.rs +++ b/turbopack/crates/turbopack-ecmascript/src/chunk/chunk_type.rs @@ -1,10 +1,8 @@ use anyhow::{Result, bail}; -use turbo_rcstr::RcStr; use turbo_tasks::{ResolvedVc, TryJoinIterExt, ValueDefault, ValueToString, Vc}; use turbopack_core::chunk::{ - AsyncModuleInfo, Chunk, ChunkItem, ChunkItemBatchGroup, ChunkItemExt, - ChunkItemOrBatchWithAsyncModuleInfo, ChunkType, ChunkingContext, ModuleId, - round_chunk_item_size, + AsyncModuleInfo, Chunk, ChunkItem, ChunkItemBatchGroup, ChunkItemOrBatchWithAsyncModuleInfo, + ChunkType, ChunkingContext, round_chunk_item_size, }; use super::{EcmascriptChunk, EcmascriptChunkContent, EcmascriptChunkItem}; @@ -29,42 +27,12 @@ impl ChunkType for EcmascriptChunkType { chunk_items: Vec, batch_groups: Vec>, ) -> Result>> { - // Convert chunk items first - let converted_chunk_items: Vec<_> = chunk_items - .iter() - .map(EcmascriptChunkItemOrBatchWithAsyncInfo::from_chunk_item_or_batch) - .try_join() - .await?; - - // Sort chunk items by their module ID for deterministic content ordering - // This ensures chunks with the same modules produce identical content - let mut items_with_id: Vec<_> = converted_chunk_items - .into_iter() - .map(|item| async move { - let id: ModuleId = match &item { - EcmascriptChunkItemOrBatchWithAsyncInfo::ChunkItem(item) => { - item.chunk_item.id().await? - } - EcmascriptChunkItemOrBatchWithAsyncInfo::Batch(batch) => { - let batch_ref = batch.await?; - if let Some(first_item) = batch_ref.chunk_items.first() { - first_item.chunk_item.id().await? - } else { - ModuleId::String(RcStr::default()) - } - } - }; - Ok((id, item)) - }) - .try_join() - .await?; - - items_with_id.sort_by(|a, b| a.0.cmp(&b.0)); - - let sorted_items: Vec<_> = items_with_id.into_iter().map(|(_, item)| item).collect(); - let content = EcmascriptChunkContent { - chunk_items: sorted_items, + chunk_items: chunk_items + .iter() + .map(EcmascriptChunkItemOrBatchWithAsyncInfo::from_chunk_item_or_batch) + .try_join() + .await?, batch_groups: batch_groups .into_iter() .map(|batch_group| { diff --git a/turbopack/crates/turbopack-ecmascript/src/chunk/item.rs b/turbopack/crates/turbopack-ecmascript/src/chunk/item.rs index 7d382eaa647..e617d17fc44 100644 --- a/turbopack/crates/turbopack-ecmascript/src/chunk/item.rs +++ b/turbopack/crates/turbopack-ecmascript/src/chunk/item.rs @@ -27,7 +27,6 @@ use crate::{ EcmascriptModuleContent, chunk::{chunk_type::EcmascriptChunkType, placeable::EcmascriptChunkPlaceable}, references::async_module::{AsyncModuleOptions, OptionAsyncModuleOptions}, - runtime_functions::TURBOPACK_ASYNC_MODULE, utils::StringifyJs, }; @@ -160,22 +159,6 @@ impl EcmascriptChunkItemContent { code += "\n"; } - if self.options.async_module.is_some() { - write!(code, "return {TURBOPACK_ASYNC_MODULE}")?; - if self.options.supports_arrow_functions { - code += "(async ("; - } else { - code += "(async function("; - } - code += "__turbopack_handle_async_dependencies__, __turbopack_async_result__"; - if self.options.supports_arrow_functions { - code += ") => {"; - } else { - code += "){"; - } - code += " try {\n"; - } - let source_map = match &self.rewrite_source_path { RewriteSourcePath::AbsoluteFilePath(path) => { absolute_fileify_source_map(self.source_map.as_ref(), path.clone()).await? @@ -193,15 +176,6 @@ impl EcmascriptChunkItemContent { code.push_source(&self.inner_code, source_map); - if let Some(opts) = &self.options.async_module { - write!( - code, - "__turbopack_async_result__();\n}} catch(e) {{ __turbopack_async_result__(e); }} \ - }}, {});", - opts.has_top_level_await - )?; - } - code += "})"; Ok(code.build().cell_persisted()) diff --git a/turbopack/crates/turbopack-ecmascript/src/code_gen.rs b/turbopack/crates/turbopack-ecmascript/src/code_gen.rs index 91fcadfcca1..e387238af9a 100644 --- a/turbopack/crates/turbopack-ecmascript/src/code_gen.rs +++ b/turbopack/crates/turbopack-ecmascript/src/code_gen.rs @@ -43,6 +43,9 @@ use crate::{ }, }; +/// Callback that wraps all body stmts after code-gen merge. +pub type BodyWrapperFn = Box) -> Vec + Send + Sync>; + #[derive(Default)] pub struct CodeGeneration { /// ast nodes matching the span will be visitor by the visitor @@ -52,6 +55,10 @@ pub struct CodeGeneration { pub late_stmts: Vec, pub early_late_stmts: Vec, pub comments: Option, + /// An optional callback that wraps all body stmts after they are merged. + /// Used by async_module to wrap the entire module body in an async closure + /// at the AST level, so that SWC's preset-env can transpile it. + pub body_wrapper: Option, } impl CodeGeneration { diff --git a/turbopack/crates/turbopack-ecmascript/src/lib.rs b/turbopack/crates/turbopack-ecmascript/src/lib.rs index e724353e4d2..c9635b9b90f 100644 --- a/turbopack/crates/turbopack-ecmascript/src/lib.rs +++ b/turbopack/crates/turbopack-ecmascript/src/lib.rs @@ -91,14 +91,11 @@ use turbopack_core::{ compile_time_info::CompileTimeInfo, context::AssetContext, ident::AssetIdent, - module::{Module, ModuleSideEffects, OptionModule}, + module::{Module, ModuleSideEffects}, module_graph::ModuleGraph, reference::ModuleReferences, reference_type::InnerAssets, - resolve::{ - FindContextFileResult, find_context_file, origin::ResolveOrigin, package_json, - parse::Request, - }, + resolve::{FindContextFileResult, find_context_file, origin::ResolveOrigin, package_json}, source::Source, source_map::GenerateSourceMap, }; @@ -977,19 +974,6 @@ impl ResolveOrigin for EcmascriptModuleAsset { fn asset_context(&self) -> Vc> { *self.asset_context } - - #[turbo_tasks::function] - async fn get_inner_asset(&self, request: Vc) -> Result> { - Ok(Vc::cell(if let Some(inner_assets) = &self.inner_assets { - if let Some(request) = request.await?.request() { - inner_assets.await?.get(&request).copied() - } else { - None - } - } else { - None - })) - } } /// The transformed contents of an Ecmascript module. @@ -1237,14 +1221,8 @@ impl EcmascriptModuleContent { .try_join() .await?; - let ( - merged_ast, - comments, - source_maps, - original_source_maps, - lookup_table, - fallback_import_idents, - ) = merge_modules(contents, &entry_points, &globals_merged).await?; + let (merged_ast, comments, source_maps, original_source_maps, lookup_table) = + merge_modules(contents, &entry_points, &globals_merged).await?; // Use the options from an arbitrary module, since they should all be the same with // regards to minify_type and chunking_context. @@ -1273,35 +1251,22 @@ impl EcmascriptModuleContent { }; let first_entry = entry_points.first().unwrap().0; - let module_ids = modules - .iter() - .map(async |(module, exposure)| { - Ok(( - *module, - *exposure, - module.chunk_item_id(*options.chunking_context).await?, - )) + let additional_ids = modules + .keys() + // Additionally set this module factory for all modules that are exposed. The whole + // group might be imported via a different entry import in different chunks (we only + // ensure that the modules are in the same order, not that they form a subgraph that + // is always imported from the same root module). + // + // Also skip the first entry, which is the name of the chunk item. + .filter(|m| { + **m != first_entry + && *modules.get(*m).unwrap() == MergeableModuleExposure::External }) + .map(|m| m.chunk_item_id(*options.chunking_context)) .try_join() - .await?; - let additional_ids = module_ids - .into_iter() - .filter_map(|(module, exposure, module_id)| { - if module == first_entry { - return None; - } - let fallback_ident: Atom = - crate::magic_identifier::mangle(&format!("imported module {module_id}")) - .into(); - if exposure == MergeableModuleExposure::External - || fallback_import_idents.contains(&fallback_ident) - { - Some(module_id) - } else { - None - } - }) - .collect::>(); + .await? + .into(); emit_content(content, additional_ids) .instrument(tracing::info_span!("emit code")) @@ -1335,7 +1300,6 @@ async fn merge_modules( Vec, SmallVec<[ResolvedVc>; 1]>, Arc>>, - FxHashSet, )> { struct SetSyntaxContextVisitor<'a> { modules_header_width: u32, @@ -1635,12 +1599,10 @@ async fn merge_modules( merged_ast.visit_mut_with(&mut swc_core::ecma::transforms::base::hygiene::hygiene()); drop(span); - let fallback_import_idents = inserted_imports.keys().cloned().collect(); - - Ok((merged_ast, inserted, fallback_import_idents)) + Ok((merged_ast, inserted)) }); - let (merged_ast, inserted, fallback_import_idents) = match result { + let (merged_ast, inserted) = match result { Ok(v) => v, Err((content_idx, err)) => { return Err( @@ -1689,7 +1651,6 @@ async fn merge_modules( source_maps, original_source_maps, Arc::new(Mutex::new(lookup_table)), - fallback_import_idents, )) } @@ -2272,6 +2233,7 @@ fn process_content_with_code_gens( let mut hoisted_stmts = FxIndexMap::default(); let mut early_late_stmts = FxIndexMap::default(); let mut late_stmts = FxIndexMap::default(); + let mut body_wrapper = None; for code_gen in code_gens { for CodeGenerationHoistedStmt { key, stmt } in code_gen.hoisted_stmts.drain(..) { hoisted_stmts.entry(key).or_insert(stmt); @@ -2292,6 +2254,10 @@ fn process_content_with_code_gens( visitors.push((path, &**visitor)); } } + if let Some(wrapper) = code_gen.body_wrapper.take() { + debug_assert!(body_wrapper.is_none(), "multiple body_wrappers detected"); + body_wrapper = Some(wrapper); + } } GLOBALS.set(globals, || { @@ -2321,6 +2287,23 @@ fn process_content_with_code_gens( .chain(late_stmts.into_values()) .map(ModuleItem::Stmt), ); + + // Apply body wrapper LAST — wraps all stmts (including hoisted + late) + // in the async module closure at the AST level. + if let Some(wrapper) = body_wrapper { + let stmts: Vec = body + .drain(..) + .filter_map(|item| match item { + ModuleItem::Stmt(stmt) => Some(stmt), + ModuleItem::ModuleDecl(_) => { + debug_assert!(false, "Unexpected ModuleDecl item after code gen merge"); + None + } + }) + .collect(); + let wrapped = wrapper(stmts); + body.extend(wrapped.into_iter().map(ModuleItem::Stmt)); + } } Program::Script(Script { body, .. }) => { body.splice( @@ -2334,6 +2317,13 @@ fn process_content_with_code_gens( .into_values() .chain(late_stmts.into_values()), ); + + // Apply body wrapper for scripts too. + if let Some(wrapper) = body_wrapper { + let stmts: Vec = std::mem::take(body); + let wrapped = wrapper(stmts); + *body = wrapped; + } } }; } diff --git a/turbopack/crates/turbopack-ecmascript/src/minify.rs b/turbopack/crates/turbopack-ecmascript/src/minify.rs index 64b5f68f371..a73dc60b7df 100644 --- a/turbopack/crates/turbopack-ecmascript/src/minify.rs +++ b/turbopack/crates/turbopack-ecmascript/src/minify.rs @@ -27,55 +27,14 @@ use swc_core::{ }; use tracing::instrument; use turbopack_core::{ - chunk::{CompressType, MangleType}, + chunk::MangleType, code_builder::{Code, CodeBuilder}, }; use crate::parse::{IdentCollector, generate_js_source_map}; -fn default_compress_options(mangle: Option) -> CompressOptions { - CompressOptions { - // Only run 2 passes, this is a tradeoff between performance and - // compression size. Default is 3 passes. - passes: 2, - keep_classnames: mangle.is_none(), - keep_fnames: mangle.is_none(), - ..Default::default() - } -} - -pub fn get_compress_options( - compress: Option, - mangle: Option, -) -> Option { - compress.map(|compress| match compress { - CompressType::Default => default_compress_options(mangle), - CompressType::Options(custom) => { - let mut options = default_compress_options(mangle); - if let Some(passes) = custom.passes { - options.passes = passes as usize; - } - if let Some(sequences) = custom.sequences { - options.sequences = sequences; - } - if let Some(keep_classnames) = custom.keep_classnames { - options.keep_classnames = keep_classnames; - } - if let Some(keep_fnames) = custom.keep_fnames { - options.keep_fnames = keep_fnames; - } - options - } - }) -} - #[instrument(level = "info", name = "minify ecmascript code", skip_all)] -pub fn minify( - code: Code, - source_maps: bool, - mangle: Option, - compress: Option, -) -> Result { +pub fn minify(code: Code, source_maps: bool, mangle: Option) -> Result { // Pass None for the debug ID so we don't needlessly compute it for the pre-minified content, it // will be added by the Code object returned from this function let source_maps = source_maps.then(|| code.generate_source_map_ref(None)); @@ -135,7 +94,14 @@ pub fn minify( Some(&comments), None, &MinifyOptions { - compress, + compress: Some(CompressOptions { + // Only run 2 passes, this is a tradeoff between performance and + // compression size. Default is 3 passes. + passes: 2, + keep_classnames: mangle.is_none(), + keep_fnames: mangle.is_none(), + ..Default::default() + }), mangle: mangle.map(|mangle| { let reserved = vec![atom!("AbortSignal")]; match mangle { diff --git a/turbopack/crates/turbopack-ecmascript/src/references/async_module.rs b/turbopack/crates/turbopack-ecmascript/src/references/async_module.rs index 6983ab682d1..cb18f8bcd4b 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/async_module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/async_module.rs @@ -2,7 +2,13 @@ use anyhow::Result; use bincode::{Decode, Encode}; use swc_core::{ common::DUMMY_SP, - ecma::ast::{ArrayLit, ArrayPat, Expr, Ident}, + ecma::{ + ast::{ + ArrayLit, ArrayPat, ArrowExpr, AwaitExpr, BlockStmt, Bool, Expr, FnDecl, FnExpr, Ident, + Invalid, Lit, Stmt, YieldExpr, + }, + visit::{VisitMut, VisitMutWith}, + }, quote, }; use turbo_rcstr::rcstr; @@ -18,7 +24,7 @@ use turbopack_core::{ use crate::{ ScopeHoistingContext, - code_gen::{CodeGeneration, CodeGenerationHoistedStmt}, + code_gen::{BodyWrapperFn, CodeGeneration, CodeGenerationHoistedStmt}, references::esm::base::ReferencedAsset, utils::AstSyntaxContext, }; @@ -211,49 +217,208 @@ impl AsyncModule { references: Vc, chunking_context: Vc>, ) -> Result { + let this = self.await?; + + let supports_async_await = *chunking_context + .environment() + .runtime_versions() + .supports_async_await() + .await?; + if let Some(async_module_info) = async_module_info { let async_idents = self .get_async_idents(async_module_info, references, chunking_context) .await?; + let has_top_level_await = this.has_top_level_await; + let body_wrapper: Option = Some(Box::new(move |body_stmts| { + wrap_body_in_async_module(body_stmts, has_top_level_await, supports_async_await) + })); + if !async_idents.is_empty() { let idents = async_idents .iter() .map(|(ident, ctxt)| Ident::new(ident.clone().into(), DUMMY_SP, **ctxt)) .collect::>(); - return Ok(CodeGeneration::hoisted_stmts([ - CodeGenerationHoistedStmt::new(rcstr!("__turbopack_async_dependencies__"), - quote!( - "var __turbopack_async_dependencies__ = __turbopack_handle_async_dependencies__($deps);" - as Stmt, - deps: Expr = Expr::Array(ArrayLit { - span: DUMMY_SP, - elems: idents - .iter() - .map(|ident| { Some(Expr::Ident(ident.clone()).into()) }) - .collect(), - }) - ) - ), - CodeGenerationHoistedStmt::new(rcstr!("__turbopack_async_dependencies__ await"), - quote!( - "($deps = __turbopack_async_dependencies__.then ? (await \ - __turbopack_async_dependencies__)() : __turbopack_async_dependencies__);" as Stmt, - deps: AssignTarget = ArrayPat { - span: DUMMY_SP, - elems: idents - .into_iter() - .map(|ident| { Some(ident.into()) }) - .collect(), - optional: false, - type_ann: None, - }.into(), - )), - ].to_vec())); + return Ok(CodeGeneration { + hoisted_stmts: [ + CodeGenerationHoistedStmt::new(rcstr!("__turbopack_async_dependencies__"), + quote!( + "var __turbopack_async_dependencies__ = __turbopack_handle_async_dependencies__($deps);" + as Stmt, + deps: Expr = Expr::Array(ArrayLit { + span: DUMMY_SP, + elems: idents + .iter() + .map(|ident| { Some(Expr::Ident(ident.clone()).into()) }) + .collect(), + }) + ) + ), + CodeGenerationHoistedStmt::new(rcstr!("__turbopack_async_dependencies__ await"), { + let mut stmt = quote!( + "($deps = __turbopack_async_dependencies__.then ? (await \ + __turbopack_async_dependencies__)() : __turbopack_async_dependencies__);" as Stmt, + deps: AssignTarget = ArrayPat { + span: DUMMY_SP, + elems: idents + .into_iter() + .map(|ident| { Some(ident.into()) }) + .collect(), + optional: false, + type_ann: None, + }.into(), + ); + if !supports_async_await { + replace_await_with_yield(&mut stmt); + } + stmt + }), + ].to_vec(), + body_wrapper, + ..Default::default() + }); } + + return Ok(CodeGeneration { + body_wrapper, + ..Default::default() + }); } Ok(CodeGeneration::empty()) } } + +/// Wraps a list of module body statements in the Turbopack async module closure: +/// +/// ```js +/// return __turbopack_context__.a( +/// async function(__turbopack_handle_async_dependencies__, __turbopack_async_result__) { +/// try { +/// ...body_stmts... +/// __turbopack_async_result__(); +/// } catch(e) { +/// __turbopack_async_result__(e); +/// } +/// }, +/// has_top_level_await +/// ); +/// ``` +pub(crate) fn wrap_body_in_async_module( + body_stmts: Vec, + has_top_level_await: bool, + supports_async_await: bool, +) -> Vec { + let mut try_body = body_stmts; + try_body.push(quote!("__turbopack_async_result__();" as Stmt)); + + // For generator-based wrapping, convert all await expressions in the body to yield + if !supports_async_await { + for stmt in &mut try_body { + replace_await_with_yield(stmt); + } + } + + let mut try_catch = quote!("try {} catch(e) { __turbopack_async_result__(e); }" as Stmt); + if let Stmt::Try(try_stmt) = &mut try_catch { + try_stmt.block.stmts = try_body; + } else { + unreachable!("quote! should produce a TryStmt"); + } + + // Use async function or generator function depending on environment support + let handler = if supports_async_await { + let mut handler = quote!( + "async function(__turbopack_handle_async_dependencies__, __turbopack_async_result__) {}" + as Expr + ); + if let Expr::Fn(fn_expr) = &mut handler { + fn_expr.function.body = Some(BlockStmt { + span: DUMMY_SP, + stmts: vec![try_catch], + ctxt: Default::default(), + }); + } else { + unreachable!("quote! should produce a FnExpr"); + } + handler + } else { + // Legacy: wrap a generator IIFE inside a regular function with an inline + // driver. The generator is created and stepped through immediately, + // resolving yielded promises. This keeps the generator-driving logic + // out of the shared runtime so modern environments pay zero cost. + let mut gen_fn = quote!("function*() {}" as Expr); + if let Expr::Fn(fn_expr) = &mut gen_fn { + fn_expr.function.body = Some(BlockStmt { + span: DUMMY_SP, + stmts: vec![try_catch], + ctxt: Default::default(), + }); + } else { + unreachable!("quote! should produce a FnExpr"); + } + + let gen_init = quote!("var __gen = $gen_fn();" as Stmt, gen_fn: Expr = gen_fn); + + let step_call = quote!( + "(function __step(k, a) { try { var r = __gen[k](a); } catch(e) { \ + __turbopack_async_result__(e); return; } if (!r.done) \ + Promise.resolve(r.value).then(function(v) { __step('next', v); }, function(e) { \ + __step('throw', e); }); })('next');" as Stmt + ); + + let mut handler = quote!( + "function(__turbopack_handle_async_dependencies__, __turbopack_async_result__) {}" + as Expr + ); + if let Expr::Fn(fn_expr) = &mut handler { + fn_expr.function.body = Some(BlockStmt { + span: DUMMY_SP, + stmts: vec![gen_init, step_call], + ctxt: Default::default(), + }); + } else { + unreachable!("quote! should produce a FnExpr"); + } + handler + }; + + vec![quote!( + "return __turbopack_context__.a($handler, $tla);" as Stmt, + handler: Expr = handler, + tla: Expr = Expr::Lit(Lit::Bool(Bool { span: DUMMY_SP, value: has_top_level_await })), + )] +} + +/// Replaces `AwaitExpr` nodes with `YieldExpr` in the given statement, +/// stopping at function boundaries (nested async functions are already +/// downleveled by SWC's preset-env before this runs). +fn replace_await_with_yield(stmt: &mut Stmt) { + struct AwaitToYield; + impl VisitMut for AwaitToYield { + fn visit_mut_expr(&mut self, expr: &mut Expr) { + expr.visit_mut_children_with(self); + if let Expr::Await(_) = expr { + let old_expr = std::mem::replace(expr, Expr::Invalid(Invalid { span: DUMMY_SP })); + if let Expr::Await(AwaitExpr { span, arg }) = old_expr { + *expr = Expr::Yield(YieldExpr { + span, + delegate: false, + arg: Some(arg), + }); + } else { + unreachable!(); + } + } + } + + // Stop at function boundaries — only transform top-level awaits, + // not awaits inside nested functions. + fn visit_mut_fn_expr(&mut self, _: &mut FnExpr) {} + fn visit_mut_fn_decl(&mut self, _: &mut FnDecl) {} + fn visit_mut_arrow_expr(&mut self, _: &mut ArrowExpr) {} + } + stmt.visit_mut_with(&mut AwaitToYield); +} diff --git a/turbopack/crates/turbopack-ecmascript/src/references/cjs.rs b/turbopack/crates/turbopack-ecmascript/src/references/cjs.rs index a0ad52a68c1..e05026f496d 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/cjs.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/cjs.rs @@ -11,6 +11,7 @@ use turbo_tasks::{ use turbopack_core::{ chunk::{ChunkingContext, ChunkingType}, issue::IssueSource, + module::Module, reference::ModuleReference, reference_type::CommonJsReferenceSubType, resolve::{ModuleResolveResult, ResolveErrorMode, origin::ResolveOrigin, parse::Request}, @@ -28,6 +29,7 @@ use crate::{ runtime_functions::TURBOPACK_CACHE, }; +/// Generic CommonJS reference that doesn't perform any codegen. Used for tracing #[turbo_tasks::value] #[derive(Hash, Debug, ValueToString)] #[value_to_string("generic commonjs {request}")] @@ -86,6 +88,7 @@ pub struct CjsRequireAssetReference { issue_source: IssueSource, error_mode: ResolveErrorMode, chunking_type_attribute: Option, + resolve_override: Option>>, } impl CjsRequireAssetReference { @@ -95,6 +98,7 @@ impl CjsRequireAssetReference { issue_source: IssueSource, error_mode: ResolveErrorMode, chunking_type_attribute: Option, + resolve_override: Option>>, ) -> Self { CjsRequireAssetReference { origin, @@ -102,6 +106,7 @@ impl CjsRequireAssetReference { issue_source, error_mode, chunking_type_attribute, + resolve_override, } } } @@ -110,6 +115,10 @@ impl CjsRequireAssetReference { impl ModuleReference for CjsRequireAssetReference { #[turbo_tasks::function] fn resolve_reference(&self) -> Vc { + if let Some(resolved) = &self.resolve_override { + return *ModuleResolveResult::module(*resolved); + } + cjs_resolve( *self.origin, *self.request, @@ -216,6 +225,7 @@ pub struct CjsRequireResolveAssetReference { issue_source: IssueSource, error_mode: ResolveErrorMode, chunking_type_attribute: Option, + resolve_override: Option>>, } impl CjsRequireResolveAssetReference { @@ -225,6 +235,7 @@ impl CjsRequireResolveAssetReference { issue_source: IssueSource, error_mode: ResolveErrorMode, chunking_type_attribute: Option, + resolve_override: Option>>, ) -> Self { CjsRequireResolveAssetReference { origin, @@ -232,6 +243,7 @@ impl CjsRequireResolveAssetReference { issue_source, error_mode, chunking_type_attribute, + resolve_override, } } } @@ -240,6 +252,10 @@ impl CjsRequireResolveAssetReference { impl ModuleReference for CjsRequireResolveAssetReference { #[turbo_tasks::function] fn resolve_reference(&self) -> Vc { + if let Some(resolved) = &self.resolve_override { + return *ModuleResolveResult::module(*resolved); + } + cjs_resolve( *self.origin, *self.request, diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs index 8132b954fa0..76f38b7e78a 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs @@ -333,6 +333,7 @@ pub struct EsmAssetReference { pub import_externals: bool, pub tree_shaking_mode: Option, pub is_pure_import: bool, + pub resolve_override: Option>>, } impl EsmAssetReference { @@ -356,6 +357,7 @@ impl EsmAssetReference { import_usage: ImportUsage, import_externals: bool, tree_shaking_mode: Option, + resolve_override: Option>>, ) -> Self { EsmAssetReference { module, @@ -368,6 +370,7 @@ impl EsmAssetReference { import_externals, tree_shaking_mode, is_pure_import: false, + resolve_override, } } @@ -381,6 +384,7 @@ impl EsmAssetReference { import_usage: ImportUsage, import_externals: bool, tree_shaking_mode: Option, + resolve_override: Option>>, ) -> Self { EsmAssetReference { module, @@ -393,6 +397,7 @@ impl EsmAssetReference { import_externals, tree_shaking_mode, is_pure_import: true, + resolve_override, } } pub(crate) fn get_referenced_asset(self: Vc) -> Vc { @@ -404,6 +409,9 @@ impl EsmAssetReference { impl ModuleReference for EsmAssetReference { #[turbo_tasks::function] async fn resolve_reference(&self) -> Result> { + if let Some(resolved) = &self.resolve_override { + return Ok(*ModuleResolveResult::module(*resolved)); + } let ty = if let Some(loader) = self.annotations.as_ref().and_then(|a| a.turbopack_loader()) { // Resolve the loader path relative to the importing file @@ -792,7 +800,7 @@ pub struct InvalidExport { #[turbo_tasks::value_impl] impl Issue for InvalidExport { fn severity(&self) -> IssueSeverity { - IssueSeverity::Warning + IssueSeverity::Error } async fn title(&self) -> Result { diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/dynamic.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/dynamic.rs index a857c16b599..215695995cb 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/dynamic.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/dynamic.rs @@ -12,6 +12,7 @@ use turbopack_core::{ chunk::{ChunkingContext, ChunkingType}, environment::ChunkLoading, issue::IssueSource, + module::Module, reference::ModuleReference, reference_type::EcmaScriptModulesReferenceSubType, resolve::{ @@ -46,6 +47,7 @@ pub struct EsmAsyncAssetReference { /// Detected from destructured await, member access on await, .then() /// callback destructuring, or webpackExports/turbopackExports comments. pub export_usage: ExportUsage, + pub resolve_override: Option>>, } impl EsmAsyncAssetReference { @@ -67,6 +69,7 @@ impl EsmAsyncAssetReference { error_mode: ResolveErrorMode, import_externals: bool, export_usage: ExportUsage, + resolve_override: Option>>, ) -> Self { EsmAsyncAssetReference { origin, @@ -76,6 +79,7 @@ impl EsmAsyncAssetReference { error_mode, import_externals, export_usage, + resolve_override, } } } @@ -84,6 +88,10 @@ impl EsmAsyncAssetReference { impl ModuleReference for EsmAsyncAssetReference { #[turbo_tasks::function] async fn resolve_reference(&self) -> Result> { + if let Some(resolved) = &self.resolve_override { + return Ok(*ModuleResolveResult::module(*resolved)); + } + esm_resolve( *self.get_origin().to_resolved().await?, *self.request, diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/module_item.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/module_item.rs index 7ece462b863..dddeb9c3bcd 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/module_item.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/module_item.rs @@ -44,6 +44,7 @@ impl EsmModuleItem { _chunking_context: Vc>, ) -> Result { let mut visitors = Vec::new(); + let supports_block_scoping = self.supports_block_scoping; visitors.push(create_visitor!( self.path, @@ -56,7 +57,14 @@ impl EsmModuleItem { let decl = Decl::Var(Box::new(VarDecl { span: DUMMY_SP, ctxt: Default::default(), - kind: swc_core::ecma::ast::VarDeclKind::Var, + kind: if supports_block_scoping { + swc_core::ecma::ast::VarDeclKind::Const + } else { + // This is not entirely correct: this hides TDZ errors with + // circular imports, but there is no way to model this runtime + // behavior well for older browsers. + swc_core::ecma::ast::VarDeclKind::Var + }, declare: false, decls: vec![VarDeclarator { span: DUMMY_SP, diff --git a/turbopack/crates/turbopack-ecmascript/src/references/external_module.rs b/turbopack/crates/turbopack-ecmascript/src/references/external_module.rs index 5feada09a8c..fb81bbd7c1b 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/external_module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/external_module.rs @@ -37,8 +37,8 @@ use crate::{ }, references::async_module::{AsyncModule, OptionAsyncModule}, runtime_functions::{ - TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE, TURBOPACK_EXTERNAL_IMPORT, - TURBOPACK_EXTERNAL_REQUIRE, TURBOPACK_LOAD_SCRIPT, + TURBOPACK_ASYNC_MODULE, TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE, + TURBOPACK_EXTERNAL_IMPORT, TURBOPACK_EXTERNAL_REQUIRE, TURBOPACK_LOAD_BY_URL, }, utils::StringifyJs, }; @@ -52,7 +52,6 @@ pub enum CachedExternalType { EcmaScriptViaImport, Global, Script, - Umd, } #[derive( @@ -75,7 +74,6 @@ impl Display for CachedExternalType { CachedExternalType::EcmaScriptViaImport => write!(f, "esm_import"), CachedExternalType::Global => write!(f, "global"), CachedExternalType::Script => write!(f, "script"), - CachedExternalType::Umd => write!(f, "umd"), } } } @@ -148,14 +146,44 @@ impl CachedExternalModule { } #[turbo_tasks::function] - pub fn content(&self) -> Result> { + pub fn content(&self, supports_async_await: bool) -> Result> { let mut code = RopeBuilder::default(); + let needs_async_wrapper = self.external_type == CachedExternalType::EcmaScriptViaImport + || self.external_type == CachedExternalType::Script; + + // Use "yield" in legacy environments so the generator driver can step + // through async operations. + let kw = if supports_async_await { + "await" + } else { + "yield" + }; + + // Open async module wrapper + if needs_async_wrapper { + if supports_async_await { + writeln!( + code, + "return {TURBOPACK_ASYNC_MODULE}(async \ + function(__turbopack_handle_async_dependencies__, \ + __turbopack_async_result__) {{\ntry {{" + )?; + } else { + writeln!( + code, + "return {TURBOPACK_ASYNC_MODULE}(\ + function(__turbopack_handle_async_dependencies__, \ + __turbopack_async_result__) {{\nvar __gen = function*() {{\ntry {{" + )?; + } + } + match self.external_type { CachedExternalType::EcmaScriptViaImport => { writeln!( code, - "var mod = await {TURBOPACK_EXTERNAL_IMPORT}({});", + "var mod = {kw} {TURBOPACK_EXTERNAL_IMPORT}({});", StringifyJs(&self.request()) )?; } @@ -171,16 +199,6 @@ impl CachedExternalModule { CachedExternalType::Global => { if self.request.is_empty() { writeln!(code, "var mod = {{}};")?; - } else if self.request.contains(' ') { - // Handle requests with '/' by splitting into nested global access - let global_access = self - .request - .split(' ') - .fold("globalThis".to_string(), |acc, part| { - format!("{}[{}]", acc, StringifyJs(part)) - }); - - writeln!(code, "var mod = {global_access};")?; } else { writeln!( code, @@ -189,79 +207,44 @@ impl CachedExternalModule { )?; } } - CachedExternalType::Umd => { - // request format is: "root React commonjs react" - let parts = self.request.split(' ').collect::>(); - let global_name = parts[1]; - let module_name = parts[3]; - - writeln!( - code, - "let mod; if (typeof exports === 'object' && typeof module === 'object') {{ \ - mod = {TURBOPACK_EXTERNAL_REQUIRE}({}, () => require({})); }} else {{ mod = \ - globalThis[{}] }}", - StringifyJs(module_name), - StringifyJs(module_name), - StringifyJs(global_name), - )?; - } CachedExternalType::Script => { - // Parse the request format: "variableName@url" - // e.g., "foo@https://test.test.com" if let Some(at_index) = self.request.find('@') { let variable_name = &self.request[..at_index]; let url = &self.request[at_index + 1..]; - // Similar to webpack's approach: wrap in a promise that checks variable before - // and after loading - writeln!(code, "var mod = await (async () => {{")?; - - // First check if variable already exists (avoid redundant loading) - writeln!( - code, - " if (typeof globalThis[{}] !== 'undefined') {{", - StringifyJs(variable_name) - )?; - writeln!( - code, - " return globalThis[{}];", - StringifyJs(variable_name) - )?; - writeln!(code, " }}")?; + writeln!(code, "var mod;")?; + writeln!(code, "try {{")?; - // Load the script if variable doesn't exist writeln!( code, - " await {TURBOPACK_LOAD_SCRIPT}({});", + " {kw} {TURBOPACK_LOAD_BY_URL}({});", StringifyJs(url) )?; - // After loading, check again if the variable is available writeln!( code, - " if (typeof globalThis[{}] !== 'undefined') {{", + " if (typeof global[{}] === 'undefined') {{", StringifyJs(variable_name) )?; writeln!( code, - " return globalThis[{}];", - StringifyJs(variable_name) + " throw new Error('Variable {} is not available on global object after \ + loading {}');", + StringifyJs(variable_name), + StringifyJs(url) )?; writeln!(code, " }}")?; + writeln!(code, " mod = global[{}];", StringifyJs(variable_name))?; - // Variable not found after loading - throw error + writeln!(code, "}} catch (error) {{")?; writeln!( code, - " const error = new Error('Loading script failed.\\n(missing: {})');", - StringifyJs(url) + " throw new Error('Failed to load external URL module {}: ' + \ + (error.message || error));", + StringifyJs(&self.request) )?; - writeln!(code, " error.name = 'ScriptExternalLoadError';")?; - writeln!(code, " error.type = 'missing';")?; - writeln!(code, " error.request = {};", StringifyJs(url))?; - writeln!(code, " throw error;")?; - writeln!(code, "}})();")?; + writeln!(code, "}}")?; } else { - // Invalid format - throw error writeln!( code, "throw new Error('Invalid URL external format. Expected \"variable@url\", \ @@ -285,6 +268,26 @@ impl CachedExternalModule { writeln!(code, "{TURBOPACK_EXPORT_VALUE}(mod);")?; } + // Close async module wrapper + if needs_async_wrapper { + writeln!(code, "__turbopack_async_result__();")?; + writeln!(code, "}} catch(e) {{ __turbopack_async_result__(e); }}")?; + if supports_async_await { + writeln!(code, "}}, true);")?; + } else { + // Close the generator IIFE and add the step driver + writeln!(code, "}}();")?; + writeln!( + code, + "(function __step(k, a) {{ try {{ var r = __gen[k](a); }} catch(e) {{ \ + __turbopack_async_result__(e); return; }} if (!r.done) \ + Promise.resolve(r.value).then(function(v) {{ __step('next', v); }}, \ + function(e) {{ __step('throw', e); }}); }})('next');" + )?; + writeln!(code, "}}, true);")?; + } + } + Ok(EcmascriptModuleContent { inner_code: code.build(), source_map: None, @@ -306,16 +309,7 @@ fn externals_fs_root() -> Vc { impl Module for CachedExternalModule { #[turbo_tasks::function] async fn ident(&self) -> Result> { - // For script externals, simplify the path by using variable name - // instead of the full url to avoid long filenames - let path_str = if self.external_type == CachedExternalType::Script - && let Some(at_index) = self.request.rfind('@').filter(|&i| i > 0) - { - self.request[..at_index].to_string() - } else { - self.request.to_string() - }; - let mut ident = AssetIdent::from_path(externals_fs_root().await?.join(&path_str)?) + let mut ident = AssetIdent::from_path(externals_fs_root().await?.join(&self.request)?) .with_layer(Layer::new(rcstr!("external"))) .with_modifier(self.request.clone()) .with_modifier(self.external_type.to_string().into()); @@ -359,9 +353,7 @@ impl Module for CachedExternalModule { ) .await? } - CachedExternalType::Global - | CachedExternalType::Script - | CachedExternalType::Umd => { + CachedExternalType::Global | CachedExternalType::Script => { origin .resolve_asset( Request::parse_string(self.request.clone()), @@ -467,16 +459,26 @@ impl EcmascriptChunkPlaceable for CachedExternalModule { } #[turbo_tasks::function] - fn chunk_item_content( + async fn chunk_item_content( self: Vc, chunking_context: Vc>, _module_graph: Vc, async_module_info: Option>, _estimated: bool, - ) -> Vc { + ) -> Result> { let async_module_options = self.get_async_module().module_options(async_module_info); - EcmascriptChunkItemContent::new(self.content(), chunking_context, async_module_options) + let supports_async_await = *chunking_context + .environment() + .runtime_versions() + .supports_async_await() + .await?; + + Ok(EcmascriptChunkItemContent::new( + self.content(supports_async_await), + chunking_context, + async_module_options, + )) } #[turbo_tasks::function] diff --git a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs index 1006dfa10ba..7d3bb61720d 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs @@ -42,7 +42,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use swc_core::{ atoms::{Atom, Wtf8Atom, atom}, common::{ - GLOBALS, Globals, Span, Spanned, SyntaxContext, + GLOBALS, Globals, Span, Spanned, comments::{CommentKind, Comments}, errors::{DiagnosticId, HANDLER, Handler, Level}, source_map::SmallPos, @@ -77,7 +77,7 @@ use turbopack_core::{ issue::{IssueExt, IssueSeverity, IssueSource, StyledString, analyze::AnalyzeIssue}, module::{Module, ModuleSideEffects}, reference::{ModuleReference, ModuleReferences}, - reference_type::CommonJsReferenceSubType, + reference_type::{CommonJsReferenceSubType, InnerAssets}, resolve::{ ExportUsage, FindContextFileResult, ImportUsage, ModulePart, ResolveErrorMode, find_context_file, @@ -101,7 +101,7 @@ use crate::{ ConstantNumber, ConstantString, ConstantValue as JsConstantValue, JsValue, JsValueUrlKind, ObjectPart, RequireContextValue, WellKnownFunctionKind, WellKnownObjectKind, builtin::{early_replace_builtin, replace_builtin}, - graph::{ConditionalKind, DeclUsage, Effect, EffectArg, VarGraph, create_graph}, + graph::{ConditionalKind, Effect, EffectArg, VarGraph, create_graph}, imports::{ImportAnnotations, ImportAttributes, ImportMap, ImportedSymbol}, linker::link, parse_require_context, side_effects, @@ -479,6 +479,8 @@ struct AnalysisState<'a> { import_references: &'a [ResolvedVc], // The import map from the eval context, used to match dep strings to import references. imports: &'a ImportMap, + // Resolve overrides for imports + inner_assets: Option>, } impl AnalysisState<'_> { @@ -557,6 +559,12 @@ async fn analyze_ecmascript_module_internal( analysis.ident = source.ident().to_string().owned().await?; } + let inner_assets = if let Some(assets) = raw_module.inner_assets { + Some(assets.await?) + } else { + None + }; + // Is this a typescript file that requires analyzing type references? let analyze_types = match &ty { EcmascriptModuleAssetType::Typescript { analyze_types, .. } => *analyze_types, @@ -751,63 +759,21 @@ async fn analyze_ecmascript_module_internal( .supports_block_scoping() .await?; - let mut var_graph = { - let _span = tracing::trace_span!("analyze variable values").entered(); - set_handler_and_globals(&handler, globals, || { - create_graph(program, eval_context, analyze_mode, supports_block_scoping) - }) - }; - - let mut import_usage = - FxHashMap::with_capacity_and_hasher(var_graph.import_usages.len(), Default::default()); - for (reference, usage) in &var_graph.import_usages { - // TODO make this more efficient, i.e. cache the result? - if let DeclUsage::Bindings(ids) = usage { - // compute transitive closure of `ids` over `top_level_mappings` - let mut visited = ids.clone(); - let mut stack = ids.iter().collect::>(); - let mut has_global_usage = false; - while let Some(id) = stack.pop() { - match var_graph.decl_usages.get(id) { - Some(DeclUsage::SideEffects) => { - has_global_usage = true; - break; - } - Some(DeclUsage::Bindings(callers)) => { - for caller in callers { - if visited.insert(caller.clone()) { - stack.push(caller); - } - } - } - _ => {} - } - } - - // Collect all `visited` declarations which are exported - import_usage.insert( - *reference, - if has_global_usage { - ImportUsage::TopLevel - } else { - ImportUsage::Exports( - var_graph - .exports - .iter() - .filter(|(_, id)| visited.contains(*id)) - .map(|(exported, _)| exported.as_str().into()) - .collect(), - ) - }, - ); - } - } - 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), @@ -843,9 +809,15 @@ async fn analyze_ecmascript_module_internal( ) .then(ModulePart::exports), }, - import_usage.get(&i).cloned().unwrap_or_default(), + eval_context + .imports + .import_usage + .get(&i) + .cloned() + .unwrap_or_default(), import_externals, options.tree_shaking_mode, + resolve_override, ) .resolved_cell(); @@ -979,6 +951,13 @@ async fn analyze_ecmascript_module_internal( .instrument(span) .await?; + let mut var_graph = { + let _span = tracing::trace_span!("analyze variable values").entered(); + set_handler_and_globals(&handler, globals, || { + create_graph(program, eval_context, analyze_mode, supports_block_scoping) + }) + }; + let span = tracing::trace_span!("effects processing"); async { analysis.code_gens.extend(take(&mut var_graph.code_gens)); @@ -1012,6 +991,7 @@ async fn analyze_ecmascript_module_internal( is_esm, import_references: &import_references, imports: &eval_context.imports, + inner_assets, }; enum Action { @@ -1348,38 +1328,16 @@ async fn analyze_ecmascript_module_internal( continue; } - let attributes = eval_context.imports.get_attributes(span); - - // Keep ignored runtime bindings untouched so they can execute as-is at runtime - // instead of being rewritten to __turbopack_context__ members. - let is_ignored_runtime_binding = attributes.ignore && &*var == "require"; - if is_ignored_runtime_binding { - continue; - } - // FreeVar("require") might be turbopackIgnore-d - let linked_value = analysis_state - .link_value(JsValue::FreeVar(var.clone()), attributes) - .await?; - - // Call handle_free_var if the value is not unknown, or if it might be in - // free_var_references (e.g., when Object is too large and - // gets converted to Unknown in link_value, but we still - // need to add code generation via ConstantValueCodeGen) - let might_be_in_free_var_references = { - let free_var_js = JsValue::FreeVar(var.clone()); - if let Some((name, _)) = free_var_js.get_definable_name(None) { - analysis_state - .compile_time_info_ref - .free_var_references - .get(&name) - .await? - .is_some() - } else { - false - } - }; - if !linked_value.is_unknown() || might_be_in_free_var_references { + if !analysis_state + .link_value( + JsValue::FreeVar(var.clone()), + eval_context.imports.get_attributes(span), + ) + .await? + .is_unknown() + { + // Call handle free var handle_free_var( &ast_path, JsValue::FreeVar(var), @@ -1473,6 +1431,7 @@ async fn analyze_ecmascript_module_internal( original_reference.import_usage.clone(), original_reference.import_externals, original_reference.tree_shaking_mode, + original_reference.resolve_override, ) .resolved_cell() }, @@ -1840,6 +1799,7 @@ async fn handle_dynamic_import) + Send + Sync>( handler, origin, source, + &state.inner_assets, ignore_dynamic_requests, analysis, error_mode, @@ -1856,6 +1816,7 @@ async fn handle_dynamic_import_with_linked_args( handler: &Handler, origin: ResolvedVc>, source: ResolvedVc>, + inner_assets: &Option>, ignore_dynamic_requests: bool, analysis: &mut AnalyzeEcmascriptModuleResultBuilder, error_mode: ResolveErrorMode, @@ -1899,6 +1860,16 @@ async fn handle_dynamic_import_with_linked_args( return Ok(()); } } + + let resolve_override = if let Some(inner_assets) = &inner_assets + && let Some(req) = pat.as_constant_string() + && let Some(a) = inner_assets.get(req) + { + Some(*a) + } else { + None + }; + analysis.add_reference_code_gen( EsmAsyncAssetReference::new( origin, @@ -1908,6 +1879,7 @@ async fn handle_dynamic_import_with_linked_args( error_mode, import_externals, export_usage, + resolve_override, ), ast_path.to_vec().into(), ); @@ -2199,6 +2171,7 @@ where handler, origin, source, + &state.inner_assets, ignore_dynamic_requests, analysis, error_mode, @@ -2225,6 +2198,16 @@ where return Ok(()); } } + + let resolve_override = if let Some(inner_assets) = &state.inner_assets + && let Some(req) = pat.as_constant_string() + && let Some(a) = inner_assets.get(req) + { + Some(*a) + } else { + None + }; + analysis.add_reference_code_gen( CjsRequireAssetReference::new( origin, @@ -2232,6 +2215,7 @@ where issue_source(source, span), error_mode, attributes.chunking_type, + resolve_override, ), ast_path.to_vec().into(), ); @@ -2283,6 +2267,7 @@ where issue_source(source, span), error_mode, attributes.chunking_type, + None, ), ast_path.to_vec().into(), ); @@ -2330,6 +2315,16 @@ where return Ok(()); } } + + let resolve_override = if let Some(inner_assets) = &state.inner_assets + && let Some(req) = pat.as_constant_string() + && let Some(a) = inner_assets.get(req) + { + Some(*a) + } else { + None + }; + analysis.add_reference_code_gen( CjsRequireResolveAssetReference::new( origin, @@ -2337,6 +2332,7 @@ where issue_source(source, span), error_mode, attributes.chunking_type, + resolve_override, ), ast_path.to_vec().into(), ); @@ -3276,9 +3272,12 @@ async fn handle_free_var_reference( ), Default::default(), export.clone().map(ModulePart::export), + // TODO This could be optimized. E.g. referencing `Buffer` in some top + // level function could set ImportUsage properly here ImportUsage::TopLevel, state.import_externals, state.tree_shaking_mode, + None, ) .resolved_cell()) }) @@ -3836,41 +3835,6 @@ async fn require_context_visitor( )) } -pub(crate) fn for_each_ident_in_pat(pat: &Pat, f: &mut impl FnMut(&Atom, SyntaxContext)) { - match pat { - Pat::Ident(BindingIdent { id, .. }) => { - f(&id.sym, id.ctxt); - } - Pat::Array(ArrayPat { elems, .. }) => elems.iter().for_each(|e| { - if let Some(e) = e { - for_each_ident_in_pat(e, f); - } - }), - Pat::Rest(RestPat { arg, .. }) => { - for_each_ident_in_pat(arg, f); - } - Pat::Object(ObjectPat { props, .. }) => { - props.iter().for_each(|p| match p { - ObjectPatProp::KeyValue(KeyValuePatProp { value, .. }) => { - for_each_ident_in_pat(value, f); - } - ObjectPatProp::Assign(AssignPatProp { key, .. }) => { - f(&key.sym, key.ctxt); - } - ObjectPatProp::Rest(RestPat { arg, .. }) => { - for_each_ident_in_pat(arg, f); - } - }); - } - Pat::Assign(AssignPat { left, .. }) => { - for_each_ident_in_pat(left, f); - } - Pat::Invalid(_) | Pat::Expr(_) => { - panic!("Unexpected pattern while enumerating idents"); - } - } -} - #[derive(Hash, Debug, Clone, Eq, PartialEq, TraceRawVcs, Encode, Decode)] pub struct AstPath( #[bincode(with_serde)] diff --git a/turbopack/crates/turbopack-ecmascript/src/references/pattern_mapping.rs b/turbopack/crates/turbopack-ecmascript/src/references/pattern_mapping.rs index dfaad32566d..59c4c902c72 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/pattern_mapping.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/pattern_mapping.rs @@ -28,7 +28,6 @@ use turbopack_core::{ }; use crate::{ - chunk::EcmascriptChunkPlaceable, references::util::{ request_to_string, throw_module_not_found_error_expr, throw_module_not_found_expr, throw_module_not_found_expr_async, @@ -356,13 +355,6 @@ async fn to_single_pattern_mapping( } }; if let Some(chunkable) = ResolvedVc::try_downcast::>(module) { - // Check if it's an EcmascriptChunkPlaceable - // Non-JS modules should be ignored as their side effects - // are handled through their own chunk types - if ResolvedVc::try_downcast::>(module).is_none() { - return Ok(SinglePatternMapping::Ignored); - } - match resolve_type { ResolveType::AsyncChunkLoader => { let ident = chunking_context.async_loader_chunk_item_ident(*chunkable); diff --git a/turbopack/crates/turbopack-ecmascript/src/runtime_functions.rs b/turbopack/crates/turbopack-ecmascript/src/runtime_functions.rs index a4c565a4a2a..d152bdec738 100644 --- a/turbopack/crates/turbopack-ecmascript/src/runtime_functions.rs +++ b/turbopack/crates/turbopack-ecmascript/src/runtime_functions.rs @@ -78,7 +78,6 @@ pub const TURBOPACK_CACHE: &TurbopackRuntimeFunctionShortcut = make_shortcut!("c pub const TURBOPACK_MODULES: &TurbopackRuntimeFunctionShortcut = make_shortcut!("M"); pub const TURBOPACK_LOAD: &TurbopackRuntimeFunctionShortcut = make_shortcut!("l"); pub const TURBOPACK_LOAD_BY_URL: &TurbopackRuntimeFunctionShortcut = make_shortcut!("L"); -pub const TURBOPACK_LOAD_SCRIPT: &TurbopackRuntimeFunctionShortcut = make_shortcut!("S"); pub const TURBOPACK_CLEAR_CHUNK_CACHE: &TurbopackRuntimeFunctionShortcut = make_shortcut!("C"); pub const TURBOPACK_DYNAMIC: &TurbopackRuntimeFunctionShortcut = make_shortcut!("j"); pub const TURBOPACK_RESOLVE_ABSOLUTE_PATH: &TurbopackRuntimeFunctionShortcut = make_shortcut!("P"); @@ -94,11 +93,10 @@ pub const TURBOPACK_REQUIRE_REAL: &TurbopackRuntimeFunctionShortcut = make_short pub const TURBOPACK_WASM: &TurbopackRuntimeFunctionShortcut = make_shortcut!("w"); pub const TURBOPACK_WASM_MODULE: &TurbopackRuntimeFunctionShortcut = make_shortcut!("u"); pub const TURBOPACK_GLOBAL: &TurbopackRuntimeFunctionShortcut = make_shortcut!("g"); -pub const TURBOPACK_PUBLIC_PATH: &TurbopackRuntimeFunctionShortcut = make_shortcut!("p"); /// Adding an entry to this list will automatically ensure that `__turbopack_XXX__` can be called /// from user code (by inserting a replacement into free_var_references) -pub const TURBOPACK_RUNTIME_FUNCTION_SHORTCUTS: [(&str, &TurbopackRuntimeFunctionShortcut); 25] = [ +pub const TURBOPACK_RUNTIME_FUNCTION_SHORTCUTS: [(&str, &TurbopackRuntimeFunctionShortcut); 23] = [ ("__turbopack_require__", TURBOPACK_REQUIRE), ("__turbopack_module_context__", TURBOPACK_MODULE_CONTEXT), ("__turbopack_import__", TURBOPACK_IMPORT), @@ -109,7 +107,6 @@ pub const TURBOPACK_RUNTIME_FUNCTION_SHORTCUTS: [(&str, &TurbopackRuntimeFunctio ("__turbopack_modules__", TURBOPACK_MODULES), ("__turbopack_load__", TURBOPACK_LOAD), ("__turbopack_load_by_url__", TURBOPACK_LOAD_BY_URL), - ("__turbopack_load_script__", TURBOPACK_LOAD_SCRIPT), ("__turbopack_dynamic__", TURBOPACK_DYNAMIC), ( "__turbopack_resolve_absolute_path__", @@ -132,5 +129,4 @@ pub const TURBOPACK_RUNTIME_FUNCTION_SHORTCUTS: [(&str, &TurbopackRuntimeFunctio ), ("__turbopack_wasm__", TURBOPACK_WASM), ("__turbopack_wasm_module__", TURBOPACK_WASM_MODULE), - ("__turbopack_public_path__", TURBOPACK_PUBLIC_PATH), ]; diff --git a/turbopack/crates/turbopack-ecmascript/src/transform/mod.rs b/turbopack/crates/turbopack-ecmascript/src/transform/mod.rs index 144803fce6c..6c4ca8a238a 100644 --- a/turbopack/crates/turbopack-ecmascript/src/transform/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/transform/mod.rs @@ -7,7 +7,7 @@ use swc_core::{ base::SwcComments, common::{Mark, SourceMap, comments::Comments}, ecma::{ - ast::{ClassMember, ExprStmt, ModuleItem, Pass, Program, Stmt}, + ast::{ExprStmt, ModuleItem, Pass, Program, Stmt}, preset_env::{self, Feature, FeatureOrModule, Targets}, transforms::{ base::{ @@ -18,7 +18,6 @@ use swc_core::{ typescript::{Config, typescript}, }, utils::IsDirective, - visit::{VisitMut, VisitMutWith, noop_visit_mut_type}, }, quote, }; @@ -58,28 +57,6 @@ pub struct PresetEnvConfig { pub loose: Option, } -struct StripUninitializedClassFields; - -impl VisitMut for StripUninitializedClassFields { - noop_visit_mut_type!(); - - fn visit_mut_class_members(&mut self, members: &mut Vec) { - members.retain(|member| { - match member { - // Remove class properties without initializers (type-only declarations) - ClassMember::ClassProp(prop) => prop.value.is_some(), - // Remove private properties without initializers - ClassMember::PrivateProp(prop) => prop.value.is_some(), - // Keep all other members - _ => true, - } - }); - - // Continue visiting children - members.visit_mut_children_with(self); - } -} - #[turbo_tasks::value] #[derive(Debug, Clone, Hash)] pub enum EcmascriptInputTransform { @@ -336,7 +313,8 @@ impl EcmascriptInputTransform { ) } EcmascriptInputTransform::TypeScript { - use_define_for_class_fields, + // TODO(WEB-1213) + use_define_for_class_fields: _use_define_for_class_fields, verbatim_module_syntax, } => { let config = Config { @@ -347,28 +325,20 @@ impl EcmascriptInputTransform { program, helpers, typescript(config, unresolved_mark, top_level_mark), - ); - - // When useDefineForClassFields is false (TypeScript legacy behavior), - // class field declarations without initializers should be stripped - // as they are type-only declarations. - if !use_define_for_class_fields { - program.visit_mut_with(&mut StripUninitializedClassFields); - } - - helpers + ) } EcmascriptInputTransform::Decorators { is_legacy, is_ecma: _, emit_decorators_metadata, - use_define_for_class_fields, + // TODO(WEB-1213) + use_define_for_class_fields: _use_define_for_class_fields, } => { use swc_core::ecma::transforms::proposal::decorators::{Config, decorators}; let config = Config { legacy: *is_legacy, emit_metadata: *emit_decorators_metadata, - use_define_for_class_fields: *use_define_for_class_fields, + ..Default::default() }; apply_transform(program, helpers, decorators(config)) diff --git a/turbopack/crates/turbopack-ecmascript/src/tree_shake/graph.rs b/turbopack/crates/turbopack-ecmascript/src/tree_shake/graph.rs index 6aff7479b0b..68a20be94ba 100644 --- a/turbopack/crates/turbopack-ecmascript/src/tree_shake/graph.rs +++ b/turbopack/crates/turbopack-ecmascript/src/tree_shake/graph.rs @@ -1120,7 +1120,7 @@ impl DepGraph { content: ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new( VarDecl { span: DUMMY_SP, - kind: VarDeclKind::Var, + kind: VarDeclKind::Const, decls: vec![VarDeclarator { span: DUMMY_SP, name: default_var.clone().into(), diff --git a/turbopack/crates/turbopack-ecmascript/src/tree_shake/mod.rs b/turbopack/crates/turbopack-ecmascript/src/tree_shake/mod.rs index 25c648c7d1b..c5d4d677fc6 100644 --- a/turbopack/crates/turbopack-ecmascript/src/tree_shake/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/tree_shake/mod.rs @@ -564,13 +564,16 @@ pub(super) async fn split_module(asset: Vc) -> Result