turbopack_ecmascript/chunk/
placeable.rs1use anyhow::Result;
2use async_trait::async_trait;
3use either::Either;
4use itertools::Itertools;
5use turbo_rcstr::rcstr;
6use turbo_tasks::{PrettyPrintError, ResolvedVc, TryJoinIterExt, Vc};
7use turbo_tasks_fs::{
8 FileJsonContent, FileSystemPath,
9 glob::{Glob, GlobOptions},
10};
11use turbopack_core::{
12 asset::Asset,
13 chunk::{AsyncModuleInfo, ChunkableModule, ChunkingContext},
14 file_source::FileSource,
15 ident::AssetIdent,
16 issue::{Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, StyledString},
17 module::Module,
18 module_graph::ModuleGraph,
19 output::{OutputAssets, OutputAssetsWithReferenced},
20 resolve::{FindContextFileResult, find_context_file, package_json},
21};
22
23use crate::{
24 chunk::EcmascriptChunkItemContent,
25 references::{
26 async_module::OptionAsyncModule,
27 esm::{EsmExport, EsmExports},
28 },
29};
30
31#[turbo_tasks::value_trait]
32pub trait EcmascriptChunkPlaceable: ChunkableModule + Module {
33 #[turbo_tasks::function]
34 fn get_exports(self: Vc<Self>) -> Vc<EcmascriptExports>;
35 #[turbo_tasks::function]
36 fn get_async_module(self: Vc<Self>) -> Vc<OptionAsyncModule> {
37 Vc::cell(None)
38 }
39
40 #[turbo_tasks::function]
45 fn chunk_item_content(
46 self: Vc<Self>,
47 _chunking_context: Vc<Box<dyn ChunkingContext>>,
48 _module_graph: Vc<ModuleGraph>,
49 _async_module_info: Option<Vc<AsyncModuleInfo>>,
50 _estimated: bool,
51 ) -> Vc<EcmascriptChunkItemContent>;
52
53 #[turbo_tasks::function]
57 fn chunk_item_content_ident(
58 self: Vc<Self>,
59 _chunking_context: Vc<Box<dyn ChunkingContext>>,
60 _module_graph: Vc<ModuleGraph>,
61 ) -> Vc<AssetIdent> {
62 self.ident()
63 }
64
65 #[turbo_tasks::function]
68 fn chunk_item_output_assets(
69 self: Vc<Self>,
70 _chunking_context: Vc<Box<dyn ChunkingContext>>,
71 _module_graph: Vc<ModuleGraph>,
72 ) -> Vc<OutputAssetsWithReferenced> {
73 OutputAssetsWithReferenced::from_assets(OutputAssets::empty())
74 }
75}
76
77#[turbo_tasks::value]
78enum SideEffectsValue {
79 None,
80 Constant(bool),
81 Glob(ResolvedVc<Glob>),
82}
83
84#[turbo_tasks::function]
85async fn side_effects_from_package_json(
86 package_json: FileSystemPath,
87) -> Result<Vc<SideEffectsValue>> {
88 let package_json_file = FileSource::new(package_json).to_resolved().await?;
89 let package_json = &*package_json_file.content().parse_json().await?;
90 if let FileJsonContent::Content(content) = package_json
91 && let Some(side_effects) = content.get("sideEffects")
92 {
93 if let Some(side_effects) = side_effects.as_bool() {
94 return Ok(SideEffectsValue::Constant(side_effects).cell());
95 } else if let Some(side_effects) = side_effects.as_array() {
96 let (globs, issues): (Vec<_>, Vec<_>) = side_effects
97 .iter()
98 .map(|side_effect| {
99 if let Some(side_effect) = side_effect.as_str() {
100 if side_effect.contains('/') {
101 Either::Left(Glob::new(
102 side_effect.strip_prefix("./").unwrap_or(side_effect).into(),
103 GlobOptions::default(),
104 ))
105 } else {
106 Either::Left(Glob::new(
107 format!("**/{side_effect}").into(),
108 GlobOptions::default(),
109 ))
110 }
111 } else {
112 Either::Right(SideEffectsInPackageJsonIssue {
113 source: IssueSource::from_source_only(ResolvedVc::upcast(
115 package_json_file,
116 )),
117 description: Some(StyledString::Text(
118 format!(
119 "Each element in sideEffects must be a string, but found \
120 {side_effect:?}"
121 )
122 .into(),
123 )),
124 })
125 }
126 })
127 .map(|glob| async move {
128 Ok(match glob {
129 Either::Left(glob) => {
130 match glob.to_resolved().await {
131 Ok(glob) => Either::Left(*glob),
132 Err(err) => {
133 Either::Right(SideEffectsInPackageJsonIssue {
134 source: IssueSource::from_source_only(ResolvedVc::upcast(
136 package_json_file,
137 )),
138 description: Some(StyledString::Text(
139 format!(
140 "Invalid glob in sideEffects: {}",
141 PrettyPrintError(&err)
142 )
143 .into(),
144 )),
145 })
146 }
147 }
148 }
149 Either::Right(_) => glob,
150 })
151 })
152 .try_join()
153 .await?
154 .into_iter()
155 .partition_map(|either| either);
156
157 for issue in issues {
158 issue.resolved_cell().emit();
159 }
160
161 return Ok(
162 SideEffectsValue::Glob(Glob::alternatives(globs).to_resolved().await?).cell(),
163 );
164 } else {
165 SideEffectsInPackageJsonIssue {
166 source: IssueSource::from_source_only(ResolvedVc::upcast(package_json_file)),
168 description: Some(StyledString::Text(
169 format!(
170 "sideEffects must be a boolean or an array, but found {side_effects:?}"
171 )
172 .into(),
173 )),
174 }
175 .resolved_cell()
176 .emit();
177 }
178 }
179 Ok(SideEffectsValue::None.cell())
180}
181
182#[turbo_tasks::value]
183struct SideEffectsInPackageJsonIssue {
184 source: IssueSource,
185 description: Option<StyledString>,
186}
187
188#[async_trait]
189#[turbo_tasks::value_impl]
190impl Issue for SideEffectsInPackageJsonIssue {
191 fn stage(&self) -> IssueStage {
192 IssueStage::Parse
193 }
194
195 fn severity(&self) -> IssueSeverity {
196 IssueSeverity::Warning
197 }
198
199 async fn file_path(&self) -> Result<FileSystemPath> {
200 self.source.file_path().await
201 }
202
203 async fn title(&self) -> Result<StyledString> {
204 Ok(StyledString::Text(rcstr!(
205 "Invalid value for sideEffects in package.json"
206 )))
207 }
208
209 async fn description(&self) -> Result<Option<StyledString>> {
210 Ok(self.description.clone())
211 }
212
213 fn source(&self) -> Option<IssueSource> {
214 Some(self.source)
215 }
216}
217
218#[turbo_tasks::value(shared)]
219#[derive(Copy, Clone)]
220pub enum SideEffectsDeclaration {
221 SideEffectFree,
222 SideEffectful,
223 None,
224}
225
226#[turbo_tasks::function]
227pub async fn get_side_effect_free_declaration(
228 path: FileSystemPath,
229 side_effect_free_packages: Option<Vc<Glob>>,
230) -> Result<Vc<SideEffectsDeclaration>> {
231 if let Some(side_effect_free_packages) = side_effect_free_packages
232 && side_effect_free_packages.await?.matches(&path.path)
233 {
234 return Ok(SideEffectsDeclaration::SideEffectFree.cell());
235 }
236
237 let find_package_json = find_context_file(path.parent(), package_json(), false).await?;
238
239 if let FindContextFileResult::Found(package_json, _) = &*find_package_json {
240 match *side_effects_from_package_json(package_json.clone()).await? {
241 SideEffectsValue::None => {}
242 SideEffectsValue::Constant(side_effects) => {
243 return Ok(if side_effects {
244 SideEffectsDeclaration::SideEffectful
245 } else {
246 SideEffectsDeclaration::SideEffectFree
247 }
248 .cell());
249 }
250 SideEffectsValue::Glob(glob) => {
251 if let Some(rel_path) = package_json.parent().get_relative_path_to(&path) {
252 let rel_path = rel_path.strip_prefix("./").unwrap_or(&rel_path);
253 return Ok(if glob.await?.matches(rel_path) {
254 SideEffectsDeclaration::SideEffectful
255 } else {
256 SideEffectsDeclaration::SideEffectFree
257 }
258 .cell());
259 }
260 }
261 }
262 }
263
264 Ok(SideEffectsDeclaration::None.cell())
265}
266
267#[turbo_tasks::value(shared)]
268pub enum EcmascriptExports {
269 EsmExports(ResolvedVc<EsmExports>),
271 DynamicNamespace,
273 CommonJs,
275 EmptyCommonJs,
277 Value,
279 Unknown,
281 None,
283}
284
285#[turbo_tasks::value_impl]
286impl EcmascriptExports {
287 #[turbo_tasks::function]
292 pub async fn split_locals_and_reexports(&self) -> Result<Vc<bool>> {
293 Ok(match self {
294 EcmascriptExports::EsmExports(exports) => {
295 let exports = exports.await?;
296 let has_reexports = !exports.star_exports.is_empty()
297 || exports.exports.iter().any(|(_, export)| {
298 matches!(
299 export,
300 EsmExport::ImportedBinding(..) | EsmExport::ImportedNamespace(_)
301 )
302 });
303 Vc::cell(has_reexports)
304 }
305 _ => Vc::cell(false),
306 })
307 }
308}