Skip to main content

turbopack_ecmascript/chunk/
placeable.rs

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