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