Skip to main content

turbopack_ecmascript/chunk/
placeable.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use either::Either;
4use itertools::Itertools;
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{PrettyPrintError, ResolvedVc, TryJoinIterExt, Vc};
7use turbo_tasks_fs::{
8    FileJsonContent, FileSystemPath,
9    glob::{Glob, GlobOptions},
10};
11use turbopack_core::{
12    asset::Asset,
13    chunk::{AsyncModuleInfo, ChunkableModule, ChunkingContext},
14    file_source::FileSource,
15    ident::AssetIdent,
16    issue::{Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, StyledString},
17    module::Module,
18    module_graph::ModuleGraph,
19    output::{OutputAssets, OutputAssetsWithReferenced},
20    resolve::{FindContextFileResult, find_context_file, package_json},
21};
22
23use crate::{
24    chunk::EcmascriptChunkItemContent,
25    references::{
26        async_module::OptionAsyncModule,
27        esm::{EsmExport, EsmExports},
28    },
29};
30
31#[turbo_tasks::value_trait]
32pub trait EcmascriptChunkPlaceable: ChunkableModule + Module {
33    #[turbo_tasks::function]
34    fn get_exports(self: Vc<Self>) -> Vc<EcmascriptExports>;
35    #[turbo_tasks::function]
36    fn get_async_module(self: Vc<Self>) -> Vc<OptionAsyncModule> {
37        Vc::cell(None)
38    }
39
40    /// Generate chunk item content directly on the module.
41    /// This replaces the need for separate ChunkItem wrapper structs.
42    /// The `estimated` parameter is used during size estimation - when true, implementations
43    /// should avoid calling chunking context APIs that would cause cycles.
44    #[turbo_tasks::function]
45    fn chunk_item_content(
46        self: Vc<Self>,
47        _chunking_context: Vc<Box<dyn ChunkingContext>>,
48        _module_graph: Vc<ModuleGraph>,
49        _async_module_info: Option<Vc<AsyncModuleInfo>>,
50        _estimated: bool,
51    ) -> Vc<EcmascriptChunkItemContent>;
52
53    /// See [`ChunkItem::content_ident`]
54    ///
55    /// [`ChunkItem::content_ident`]: turbopack_core::chunk::ChunkItem::content_ident
56    #[turbo_tasks::function]
57    fn chunk_item_content_ident(
58        self: Vc<Self>,
59        _chunking_context: Vc<Box<dyn ChunkingContext>>,
60        _module_graph: Vc<ModuleGraph>,
61    ) -> Vc<AssetIdent> {
62        self.ident()
63    }
64
65    /// Returns output assets that this chunk item depends on.
66    /// Override this for modules that reference static assets, manifests, etc.
67    #[turbo_tasks::function]
68    fn chunk_item_output_assets(
69        self: Vc<Self>,
70        _chunking_context: Vc<Box<dyn ChunkingContext>>,
71        _module_graph: Vc<ModuleGraph>,
72    ) -> Vc<OutputAssetsWithReferenced> {
73        OutputAssetsWithReferenced::from_assets(OutputAssets::empty())
74    }
75}
76
77#[turbo_tasks::value]
78enum SideEffectsValue {
79    None,
80    Constant(bool),
81    Glob(ResolvedVc<Glob>),
82}
83
84#[turbo_tasks::function]
85async fn side_effects_from_package_json(
86    package_json: FileSystemPath,
87) -> Result<Vc<SideEffectsValue>> {
88    let package_json_file = FileSource::new(package_json).to_resolved().await?;
89    let package_json = &*package_json_file.content().parse_json().await?;
90    if let FileJsonContent::Content(content) = package_json
91        && let Some(side_effects) = content.get("sideEffects")
92    {
93        if let Some(side_effects) = side_effects.as_bool() {
94            return Ok(SideEffectsValue::Constant(side_effects).cell());
95        } else if let Some(side_effects) = side_effects.as_array() {
96            let (globs, issues): (Vec<_>, Vec<_>) = side_effects
97                .iter()
98                .map(|side_effect| {
99                    if let Some(side_effect) = side_effect.as_str() {
100                        if side_effect.contains('/') {
101                            Either::Left(Glob::new(
102                                side_effect.strip_prefix("./").unwrap_or(side_effect).into(),
103                                GlobOptions::default(),
104                            ))
105                        } else {
106                            Either::Left(Glob::new(
107                                format!("**/{side_effect}").into(),
108                                GlobOptions::default(),
109                            ))
110                        }
111                    } else {
112                        Either::Right(SideEffectsInPackageJsonIssue {
113                            // TODO(PACK-4879): This should point at the buggy element
114                            source: IssueSource::from_source_only(ResolvedVc::upcast(
115                                package_json_file,
116                            )),
117                            description: Some(StyledString::Text(
118                                format!(
119                                    "Each element in sideEffects must be a string, but found \
120                                     {side_effect:?}"
121                                )
122                                .into(),
123                            )),
124                        })
125                    }
126                })
127                .map(async |glob| {
128                    Ok(match glob {
129                        Either::Left(glob) => {
130                            match glob.to_resolved().await {
131                                Ok(glob) => Either::Left(*glob),
132                                Err(err) => {
133                                    Either::Right(SideEffectsInPackageJsonIssue {
134                                        // TODO(PACK-4879): This should point at the buggy glob
135                                        source: IssueSource::from_source_only(ResolvedVc::upcast(
136                                            package_json_file,
137                                        )),
138                                        description: Some(StyledString::Text(
139                                            format!(
140                                                "Invalid glob in sideEffects: {}",
141                                                PrettyPrintError(&err)
142                                            )
143                                            .into(),
144                                        )),
145                                    })
146                                }
147                            }
148                        }
149                        Either::Right(_) => glob,
150                    })
151                })
152                .try_join()
153                .await?
154                .into_iter()
155                .partition_map(|either| either);
156
157            for issue in issues {
158                issue.resolved_cell().emit();
159            }
160
161            return Ok(
162                SideEffectsValue::Glob(Glob::alternatives(globs).to_resolved().await?).cell(),
163            );
164        } else {
165            SideEffectsInPackageJsonIssue {
166                // TODO(PACK-4879): This should point at the buggy value
167                source: IssueSource::from_source_only(ResolvedVc::upcast(package_json_file)),
168                description: Some(StyledString::Text(
169                    format!(
170                        "sideEffects must be a boolean or an array, but found {side_effects:?}"
171                    )
172                    .into(),
173                )),
174            }
175            .resolved_cell()
176            .emit();
177        }
178    }
179    Ok(SideEffectsValue::None.cell())
180}
181
182#[turbo_tasks::value]
183struct SideEffectsInPackageJsonIssue {
184    source: IssueSource,
185    description: Option<StyledString>,
186}
187
188#[async_trait]
189#[turbo_tasks::value_impl]
190impl Issue for SideEffectsInPackageJsonIssue {
191    fn stage(&self) -> IssueStage {
192        IssueStage::Parse
193    }
194
195    fn severity(&self) -> IssueSeverity {
196        IssueSeverity::Warning
197    }
198
199    async fn file_path(&self) -> Result<FileSystemPath> {
200        self.source.file_path().await
201    }
202
203    async fn title(&self) -> Result<StyledString> {
204        Ok(StyledString::Text(rcstr!(
205            "Invalid value for sideEffects in package.json"
206        )))
207    }
208
209    async fn description(&self) -> Result<Option<StyledString>> {
210        Ok(self.description.clone())
211    }
212
213    fn source(&self) -> Option<IssueSource> {
214        Some(self.source)
215    }
216}
217
218#[turbo_tasks::value(shared)]
219#[derive(Copy, Clone)]
220pub enum SideEffectsDeclaration {
221    SideEffectFree,
222    SideEffectful,
223    None,
224}
225
226#[turbo_tasks::function]
227pub async fn get_side_effect_free_declaration(
228    path: FileSystemPath,
229    side_effect_free_packages: Option<Vc<Glob>>,
230) -> Result<Vc<SideEffectsDeclaration>> {
231    let find_package_json = find_context_file(path.parent(), package_json(), false).await?;
232    // Always respect the package.json over the global side_effect_free_packages by checking it
233    // first See #96333
234    if let FindContextFileResult::Found(package_json, _) = &*find_package_json {
235        match *side_effects_from_package_json(package_json.clone()).await? {
236            SideEffectsValue::None => {}
237            SideEffectsValue::Constant(side_effects) => {
238                return Ok(if side_effects {
239                    SideEffectsDeclaration::SideEffectful
240                } else {
241                    SideEffectsDeclaration::SideEffectFree
242                }
243                .cell());
244            }
245            SideEffectsValue::Glob(glob) => {
246                if let Some(rel_path) = package_json.parent().get_relative_path_to(&path) {
247                    let rel_path = rel_path.strip_prefix("./").unwrap_or(&rel_path);
248                    return Ok(if glob.await?.matches(rel_path) {
249                        SideEffectsDeclaration::SideEffectful
250                    } else {
251                        SideEffectsDeclaration::SideEffectFree
252                    }
253                    .cell());
254                }
255            }
256        }
257    }
258
259    if let Some(side_effect_free_packages) = side_effect_free_packages
260        && side_effect_free_packages.await?.matches(&path.path)
261    {
262        return Ok(SideEffectsDeclaration::SideEffectFree.cell());
263    }
264
265    Ok(SideEffectsDeclaration::None.cell())
266}
267
268#[turbo_tasks::value(shared)]
269pub enum EcmascriptExports {
270    /// A module using ESM exports.
271    EsmExports(ResolvedVc<EsmExports>),
272    /// A module using `__turbopack_export_namespace__`, used by custom module types.
273    DynamicNamespace,
274    /// A module using CommonJS exports.
275    ///
276    /// Carries the static export names when statically analyzable, for scope hoisting.
277    /// `None` means that the exports were not analyzable by us.
278    CommonJs(Option<CjsStaticExports>),
279    /// No exports at all, and falling back to CommonJS semantics.
280    EmptyCommonJs,
281    /// A value that is made available as both the CommonJS `exports` and the ESM default export.
282    Value,
283    /// Some error occurred while determining exports.
284    Unknown,
285    /// No exports, used by custom module types.
286    None,
287}
288
289#[turbo_tasks::value_impl]
290impl EcmascriptExports {
291    /// A view of these exports for a module that *borrows* them from another module, i.e. whose
292    /// `get_exports` hands out the exports value of some other module verbatim.
293    ///
294    /// Export mangling is decided per module: the producing side keys on the module whose code
295    /// generation emits the export object, and the consuming side on the module it imports from.
296    /// An exports value shared by two module identities would let those two sides compute
297    /// different keys for the same export, so a borrowed view is always unmangled — which both
298    /// sides agree on.
299    #[turbo_tasks::function]
300    pub async fn borrowed(self: Vc<Self>) -> Result<Vc<EcmascriptExports>> {
301        let this = self.await?;
302        Ok(match &*this {
303            EcmascriptExports::EsmExports(exports) => {
304                let exports = exports.await?;
305                if !exports.mangle_export_names {
306                    return Ok(self);
307                }
308                EcmascriptExports::EsmExports(
309                    EsmExports {
310                        exports: exports.exports.clone(),
311                        star_exports: exports.star_exports.clone(),
312                        mangle_export_names: false,
313                    }
314                    .resolved_cell(),
315                )
316                .cell()
317            }
318            // Nothing else carries a mangling decision.
319            _ => self,
320        })
321    }
322
323    /// Returns whether this module should be split into separate locals and facade modules.
324    ///
325    /// Splitting is enabled when the module has re-exports (star exports or imported bindings),
326    /// which allows the tree-shaking optimization to separate local definitions from re-exports.
327    #[turbo_tasks::function]
328    pub async fn split_locals_and_reexports(&self) -> Result<Vc<bool>> {
329        Ok(match self {
330            EcmascriptExports::EsmExports(exports) => {
331                let exports = exports.await?;
332                let has_reexports = !exports.star_exports.is_empty()
333                    || exports.exports.iter().any(|(_, export)| {
334                        matches!(
335                            export,
336                            EsmExport::ImportedBinding(..) | EsmExport::ImportedNamespace(_)
337                        )
338                    });
339                Vc::cell(has_reexports)
340            }
341            _ => Vc::cell(false),
342        })
343    }
344}
345
346/// A statically-analyzable CommonJS module's named exports, for scope hoisting.
347/// See the analyzer's `CjsExportsCollector`.
348#[derive(Clone, Debug, Hash)]
349#[turbo_tasks::value(shared)]
350pub struct CjsStaticExports {
351    /// Recognized `exports.NAME` / `module.exports.NAME` names, in source order.
352    pub export_names: Vec<RcStr>,
353    /// Whether `exports.__esModule = true` is set.
354    pub has_es_module: bool,
355}