turbopack_ecmascript/chunk/
placeable.rs

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