turbopack_ecmascript/chunk/
placeable.rs1use 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 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 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 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 EsmExports(ResolvedVc<EsmExports>),
238 DynamicNamespace,
240 CommonJs,
242 EmptyCommonJs,
244 Value,
246 Unknown,
248 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}