turbopack_ecmascript/chunk/
placeable.rs

1use anyhow::Result;
2use turbo_rcstr::rcstr;
3use turbo_tasks::{ResolvedVc, TryFlatJoinIterExt, Vc};
4use turbo_tasks_fs::{FileJsonContent, FileSystemPath, glob::Glob};
5use turbopack_core::{
6    asset::Asset,
7    chunk::ChunkableModule,
8    error::PrettyPrintError,
9    file_source::FileSource,
10    issue::{
11        Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, OptionIssueSource,
12        OptionStyledString, StyledString,
13    },
14    module::Module,
15    resolve::{FindContextFileResult, find_context_file, package_json},
16};
17
18use crate::references::{
19    async_module::OptionAsyncModule,
20    esm::{EsmExport, EsmExports},
21};
22
23#[turbo_tasks::value_trait]
24pub trait EcmascriptChunkPlaceable: ChunkableModule + Module + Asset {
25    #[turbo_tasks::function]
26    fn get_exports(self: Vc<Self>) -> Vc<EcmascriptExports>;
27    #[turbo_tasks::function]
28    fn get_async_module(self: Vc<Self>) -> Vc<OptionAsyncModule> {
29        Vc::cell(None)
30    }
31    #[turbo_tasks::function]
32    async fn is_marked_as_side_effect_free(
33        self: Vc<Self>,
34        side_effect_free_packages: Vc<Glob>,
35    ) -> Result<Vc<bool>> {
36        Ok(is_marked_as_side_effect_free(
37            self.ident().path().await?.clone_value(),
38            side_effect_free_packages,
39        ))
40    }
41}
42
43#[turbo_tasks::value]
44enum SideEffectsValue {
45    None,
46    Constant(bool),
47    Glob(ResolvedVc<Glob>),
48}
49
50#[turbo_tasks::function]
51async fn side_effects_from_package_json(
52    package_json: FileSystemPath,
53) -> Result<Vc<SideEffectsValue>> {
54    let package_json_file = FileSource::new(package_json).to_resolved().await?;
55    let package_json = &*package_json_file.content().parse_json().await?;
56    if let FileJsonContent::Content(content) = package_json
57        && let Some(side_effects) = content.get("sideEffects")
58    {
59        if let Some(side_effects) = side_effects.as_bool() {
60            return Ok(SideEffectsValue::Constant(side_effects).cell());
61        } else if let Some(side_effects) = side_effects.as_array() {
62            let globs = side_effects
63                .iter()
64                .filter_map(|side_effect| {
65                    if let Some(side_effect) = side_effect.as_str() {
66                        if side_effect.contains('/') {
67                            Some(Glob::new(
68                                side_effect.strip_prefix("./").unwrap_or(side_effect).into(),
69                            ))
70                        } else {
71                            Some(Glob::new(format!("**/{side_effect}").into()))
72                        }
73                    } else {
74                        SideEffectsInPackageJsonIssue {
75                            // TODO(PACK-4879): This should point at the buggy element
76                            source: IssueSource::from_source_only(ResolvedVc::upcast(
77                                package_json_file,
78                            )),
79                            description: Some(
80                                StyledString::Text(
81                                    format!(
82                                        "Each element in sideEffects must be a string, but found \
83                                         {side_effect:?}"
84                                    )
85                                    .into(),
86                                )
87                                .resolved_cell(),
88                            ),
89                        }
90                        .resolved_cell()
91                        .emit();
92                        None
93                    }
94                })
95                .map(|glob| async move {
96                    match glob.resolve().await {
97                        Ok(glob) => Ok(Some(glob)),
98                        Err(err) => {
99                            SideEffectsInPackageJsonIssue {
100                                // TODO(PACK-4879): This should point at the buggy glob
101                                source: IssueSource::from_source_only(ResolvedVc::upcast(
102                                    package_json_file,
103                                )),
104                                description: Some(
105                                    StyledString::Text(
106                                        format!(
107                                            "Invalid glob in sideEffects: {}",
108                                            PrettyPrintError(&err)
109                                        )
110                                        .into(),
111                                    )
112                                    .resolved_cell(),
113                                ),
114                            }
115                            .resolved_cell()
116                            .emit();
117                            Ok(None)
118                        }
119                    }
120                })
121                .try_flat_join()
122                .await?;
123            return Ok(
124                SideEffectsValue::Glob(Glob::alternatives(globs).to_resolved().await?).cell(),
125            );
126        } else {
127            SideEffectsInPackageJsonIssue {
128                // TODO(PACK-4879): This should point at the buggy value
129                source: IssueSource::from_source_only(ResolvedVc::upcast(package_json_file)),
130                description: Some(
131                    StyledString::Text(
132                        format!(
133                            "sideEffects must be a boolean or an array, but found {side_effects:?}"
134                        )
135                        .into(),
136                    )
137                    .resolved_cell(),
138                ),
139            }
140            .resolved_cell()
141            .emit();
142        }
143    }
144    Ok(SideEffectsValue::None.cell())
145}
146
147#[turbo_tasks::value]
148struct SideEffectsInPackageJsonIssue {
149    source: IssueSource,
150    description: Option<ResolvedVc<StyledString>>,
151}
152
153#[turbo_tasks::value_impl]
154impl Issue for SideEffectsInPackageJsonIssue {
155    #[turbo_tasks::function]
156    fn stage(&self) -> Vc<IssueStage> {
157        IssueStage::Parse.into()
158    }
159
160    fn severity(&self) -> IssueSeverity {
161        IssueSeverity::Warning
162    }
163
164    #[turbo_tasks::function]
165    fn file_path(&self) -> Vc<FileSystemPath> {
166        self.source.file_path()
167    }
168
169    #[turbo_tasks::function]
170    fn title(&self) -> Vc<StyledString> {
171        StyledString::Text(rcstr!("Invalid value for sideEffects in package.json")).cell()
172    }
173
174    #[turbo_tasks::function]
175    fn description(&self) -> Vc<OptionStyledString> {
176        Vc::cell(self.description)
177    }
178
179    #[turbo_tasks::function]
180    fn source(&self) -> Vc<OptionIssueSource> {
181        Vc::cell(Some(self.source))
182    }
183}
184
185#[turbo_tasks::function]
186pub async fn is_marked_as_side_effect_free(
187    path: FileSystemPath,
188    side_effect_free_packages: Vc<Glob>,
189) -> Result<Vc<bool>> {
190    if side_effect_free_packages.await?.matches(&path.path) {
191        return Ok(Vc::cell(true));
192    }
193
194    let find_package_json = find_context_file(path.parent(), package_json()).await?;
195
196    if let FindContextFileResult::Found(package_json, _) = &*find_package_json {
197        match *side_effects_from_package_json(package_json.clone()).await? {
198            SideEffectsValue::None => {}
199            SideEffectsValue::Constant(side_effects) => return Ok(Vc::cell(!side_effects)),
200            SideEffectsValue::Glob(glob) => {
201                if let Some(rel_path) = package_json.parent().get_relative_path_to(&path) {
202                    let rel_path = rel_path.strip_prefix("./").unwrap_or(&rel_path);
203                    return Ok(Vc::cell(!glob.await?.matches(rel_path)));
204                }
205            }
206        }
207    }
208
209    Ok(Vc::cell(false))
210}
211
212#[turbo_tasks::value(shared)]
213pub enum EcmascriptExports {
214    /// A module using ESM exports.
215    EsmExports(ResolvedVc<EsmExports>),
216    /// A module using `__turbopack_export_namespace__`, used by custom module types.
217    DynamicNamespace,
218    /// A module using CommonJS exports.
219    CommonJs,
220    /// No exports at all, and falling back to CommonJS semantics.
221    EmptyCommonJs,
222    /// A value that is made available as both the CommonJS `exports` and the ESM default export.
223    Value,
224    /// Some error occurred while determining exports.
225    Unknown,
226    /// No exports, used by custom module types.
227    None,
228}
229
230#[turbo_tasks::value_impl]
231impl EcmascriptExports {
232    #[turbo_tasks::function]
233    pub async fn needs_facade(&self) -> Result<Vc<bool>> {
234        Ok(match self {
235            EcmascriptExports::EsmExports(exports) => {
236                let exports = exports.await?;
237                let has_reexports = !exports.star_exports.is_empty()
238                    || exports.exports.iter().any(|(_, export)| {
239                        matches!(
240                            export,
241                            EsmExport::ImportedBinding(..) | EsmExport::ImportedNamespace(_)
242                        )
243                    });
244                Vc::cell(has_reexports)
245            }
246            _ => Vc::cell(false),
247        })
248    }
249}