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