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