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;
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    /// Returns the content identity for cache invalidation.
54    /// Override this for modules whose content depends on more than just the module source
55    /// (e.g., async loaders that depend on available modules).
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(|glob| async move {
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    if let Some(side_effect_free_packages) = side_effect_free_packages
232        && side_effect_free_packages.await?.matches(&path.path)
233    {
234        return Ok(SideEffectsDeclaration::SideEffectFree.cell());
235    }
236
237    let find_package_json = find_context_file(path.parent(), package_json(), false).await?;
238
239    if let FindContextFileResult::Found(package_json, _) = &*find_package_json {
240        match *side_effects_from_package_json(package_json.clone()).await? {
241            SideEffectsValue::None => {}
242            SideEffectsValue::Constant(side_effects) => {
243                return Ok(if side_effects {
244                    SideEffectsDeclaration::SideEffectful
245                } else {
246                    SideEffectsDeclaration::SideEffectFree
247                }
248                .cell());
249            }
250            SideEffectsValue::Glob(glob) => {
251                if let Some(rel_path) = package_json.parent().get_relative_path_to(&path) {
252                    let rel_path = rel_path.strip_prefix("./").unwrap_or(&rel_path);
253                    return Ok(if glob.await?.matches(rel_path) {
254                        SideEffectsDeclaration::SideEffectful
255                    } else {
256                        SideEffectsDeclaration::SideEffectFree
257                    }
258                    .cell());
259                }
260            }
261        }
262    }
263
264    Ok(SideEffectsDeclaration::None.cell())
265}
266
267#[turbo_tasks::value(shared)]
268pub enum EcmascriptExports {
269    /// A module using ESM exports.
270    EsmExports(ResolvedVc<EsmExports>),
271    /// A module using `__turbopack_export_namespace__`, used by custom module types.
272    DynamicNamespace,
273    /// A module using CommonJS exports.
274    CommonJs,
275    /// No exports at all, and falling back to CommonJS semantics.
276    EmptyCommonJs,
277    /// A value that is made available as both the CommonJS `exports` and the ESM default export.
278    Value,
279    /// Some error occurred while determining exports.
280    Unknown,
281    /// No exports, used by custom module types.
282    None,
283}
284
285#[turbo_tasks::value_impl]
286impl EcmascriptExports {
287    /// Returns whether this module should be split into separate locals and facade modules.
288    ///
289    /// Splitting is enabled when the module has re-exports (star exports or imported bindings),
290    /// which allows the tree-shaking optimization to separate local definitions from re-exports.
291    #[turbo_tasks::function]
292    pub async fn split_locals_and_reexports(&self) -> Result<Vc<bool>> {
293        Ok(match self {
294            EcmascriptExports::EsmExports(exports) => {
295                let exports = exports.await?;
296                let has_reexports = !exports.star_exports.is_empty()
297                    || exports.exports.iter().any(|(_, export)| {
298                        matches!(
299                            export,
300                            EsmExport::ImportedBinding(..) | EsmExport::ImportedNamespace(_)
301                        )
302                    });
303                Vc::cell(has_reexports)
304            }
305            _ => Vc::cell(false),
306        })
307    }
308}