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