turbopack_ecmascript/chunk/
placeable.rs1use anyhow::Result;
2use turbo_rcstr::rcstr;
3use turbo_tasks::{ResolvedVc, TryFlatJoinIterExt, Vc};
4use turbo_tasks_fs::{FileJsonContent, FileSystemPath, glob::Glob};
5use turbopack_core::{
6 asset::Asset,
7 chunk::ChunkableModule,
8 error::PrettyPrintError,
9 file_source::FileSource,
10 issue::{
11 Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, OptionIssueSource,
12 OptionStyledString, StyledString,
13 },
14 module::Module,
15 resolve::{FindContextFileResult, find_context_file, package_json},
16};
17
18use crate::references::{
19 async_module::OptionAsyncModule,
20 esm::{EsmExport, EsmExports},
21};
22
23#[turbo_tasks::value_trait]
24pub trait EcmascriptChunkPlaceable: ChunkableModule + Module + Asset {
25 #[turbo_tasks::function]
26 fn get_exports(self: Vc<Self>) -> Vc<EcmascriptExports>;
27 #[turbo_tasks::function]
28 fn get_async_module(self: Vc<Self>) -> Vc<OptionAsyncModule> {
29 Vc::cell(None)
30 }
31 #[turbo_tasks::function]
32 async fn is_marked_as_side_effect_free(
33 self: Vc<Self>,
34 side_effect_free_packages: Vc<Glob>,
35 ) -> Result<Vc<bool>> {
36 Ok(is_marked_as_side_effect_free(
37 self.ident().path().await?.clone_value(),
38 side_effect_free_packages,
39 ))
40 }
41}
42
43#[turbo_tasks::value]
44enum SideEffectsValue {
45 None,
46 Constant(bool),
47 Glob(ResolvedVc<Glob>),
48}
49
50#[turbo_tasks::function]
51async fn side_effects_from_package_json(
52 package_json: FileSystemPath,
53) -> Result<Vc<SideEffectsValue>> {
54 let package_json_file = FileSource::new(package_json).to_resolved().await?;
55 let package_json = &*package_json_file.content().parse_json().await?;
56 if let FileJsonContent::Content(content) = package_json
57 && let Some(side_effects) = content.get("sideEffects")
58 {
59 if let Some(side_effects) = side_effects.as_bool() {
60 return Ok(SideEffectsValue::Constant(side_effects).cell());
61 } else if let Some(side_effects) = side_effects.as_array() {
62 let globs = side_effects
63 .iter()
64 .filter_map(|side_effect| {
65 if let Some(side_effect) = side_effect.as_str() {
66 if side_effect.contains('/') {
67 Some(Glob::new(
68 side_effect.strip_prefix("./").unwrap_or(side_effect).into(),
69 ))
70 } else {
71 Some(Glob::new(format!("**/{side_effect}").into()))
72 }
73 } else {
74 SideEffectsInPackageJsonIssue {
75 source: IssueSource::from_source_only(ResolvedVc::upcast(
77 package_json_file,
78 )),
79 description: Some(
80 StyledString::Text(
81 format!(
82 "Each element in sideEffects must be a string, but found \
83 {side_effect:?}"
84 )
85 .into(),
86 )
87 .resolved_cell(),
88 ),
89 }
90 .resolved_cell()
91 .emit();
92 None
93 }
94 })
95 .map(|glob| async move {
96 match glob.resolve().await {
97 Ok(glob) => Ok(Some(glob)),
98 Err(err) => {
99 SideEffectsInPackageJsonIssue {
100 source: IssueSource::from_source_only(ResolvedVc::upcast(
102 package_json_file,
103 )),
104 description: Some(
105 StyledString::Text(
106 format!(
107 "Invalid glob in sideEffects: {}",
108 PrettyPrintError(&err)
109 )
110 .into(),
111 )
112 .resolved_cell(),
113 ),
114 }
115 .resolved_cell()
116 .emit();
117 Ok(None)
118 }
119 }
120 })
121 .try_flat_join()
122 .await?;
123 return Ok(
124 SideEffectsValue::Glob(Glob::alternatives(globs).to_resolved().await?).cell(),
125 );
126 } else {
127 SideEffectsInPackageJsonIssue {
128 source: IssueSource::from_source_only(ResolvedVc::upcast(package_json_file)),
130 description: Some(
131 StyledString::Text(
132 format!(
133 "sideEffects must be a boolean or an array, but found {side_effects:?}"
134 )
135 .into(),
136 )
137 .resolved_cell(),
138 ),
139 }
140 .resolved_cell()
141 .emit();
142 }
143 }
144 Ok(SideEffectsValue::None.cell())
145}
146
147#[turbo_tasks::value]
148struct SideEffectsInPackageJsonIssue {
149 source: IssueSource,
150 description: Option<ResolvedVc<StyledString>>,
151}
152
153#[turbo_tasks::value_impl]
154impl Issue for SideEffectsInPackageJsonIssue {
155 #[turbo_tasks::function]
156 fn stage(&self) -> Vc<IssueStage> {
157 IssueStage::Parse.into()
158 }
159
160 fn severity(&self) -> IssueSeverity {
161 IssueSeverity::Warning
162 }
163
164 #[turbo_tasks::function]
165 fn file_path(&self) -> Vc<FileSystemPath> {
166 self.source.file_path()
167 }
168
169 #[turbo_tasks::function]
170 fn title(&self) -> Vc<StyledString> {
171 StyledString::Text(rcstr!("Invalid value for sideEffects in package.json")).cell()
172 }
173
174 #[turbo_tasks::function]
175 fn description(&self) -> Vc<OptionStyledString> {
176 Vc::cell(self.description)
177 }
178
179 #[turbo_tasks::function]
180 fn source(&self) -> Vc<OptionIssueSource> {
181 Vc::cell(Some(self.source))
182 }
183}
184
185#[turbo_tasks::function]
186pub async fn is_marked_as_side_effect_free(
187 path: FileSystemPath,
188 side_effect_free_packages: Vc<Glob>,
189) -> Result<Vc<bool>> {
190 if side_effect_free_packages.await?.matches(&path.path) {
191 return Ok(Vc::cell(true));
192 }
193
194 let find_package_json = find_context_file(path.parent(), package_json()).await?;
195
196 if let FindContextFileResult::Found(package_json, _) = &*find_package_json {
197 match *side_effects_from_package_json(package_json.clone()).await? {
198 SideEffectsValue::None => {}
199 SideEffectsValue::Constant(side_effects) => return Ok(Vc::cell(!side_effects)),
200 SideEffectsValue::Glob(glob) => {
201 if let Some(rel_path) = package_json.parent().get_relative_path_to(&path) {
202 let rel_path = rel_path.strip_prefix("./").unwrap_or(&rel_path);
203 return Ok(Vc::cell(!glob.await?.matches(rel_path)));
204 }
205 }
206 }
207 }
208
209 Ok(Vc::cell(false))
210}
211
212#[turbo_tasks::value(shared)]
213pub enum EcmascriptExports {
214 EsmExports(ResolvedVc<EsmExports>),
216 DynamicNamespace,
218 CommonJs,
220 EmptyCommonJs,
222 Value,
224 Unknown,
226 None,
228}
229
230#[turbo_tasks::value_impl]
231impl EcmascriptExports {
232 #[turbo_tasks::function]
233 pub async fn needs_facade(&self) -> Result<Vc<bool>> {
234 Ok(match self {
235 EcmascriptExports::EsmExports(exports) => {
236 let exports = exports.await?;
237 let has_reexports = !exports.star_exports.is_empty()
238 || exports.exports.iter().any(|(_, export)| {
239 matches!(
240 export,
241 EsmExport::ImportedBinding(..) | EsmExport::ImportedNamespace(_)
242 )
243 });
244 Vc::cell(has_reexports)
245 }
246 _ => Vc::cell(false),
247 })
248 }
249}