Skip to main content

turbopack_ecmascript/chunk/
item.rs

1use std::io::Write;
2
3use anyhow::{Result, bail};
4use async_trait::async_trait;
5use bincode::{Decode, Encode};
6use smallvec::SmallVec;
7use turbo_rcstr::{RcStr, rcstr};
8use turbo_tasks::{
9    NonLocalValue, PrettyPrintError, ResolvedVc, TaskInput, Upcast, ValueToString, Vc,
10    trace::TraceRawVcs,
11};
12use turbo_tasks_fs::{FileSystemPath, rope::Rope};
13use turbopack_core::{
14    chunk::{
15        AsyncModuleInfo, ChunkItem, ChunkItemWithAsyncModuleInfo, ChunkType, ChunkingContext,
16        ChunkingContextExt, ModuleId, SourceMapSourceType,
17    },
18    code_builder::{Code, CodeBuilder, PersistedCode},
19    ident::AssetIdent,
20    issue::{IssueExt, IssueSeverity, StyledString, code_gen::CodeGenerationIssue},
21    module::Module,
22    module_graph::ModuleGraph,
23    output::OutputAssetsReference,
24    source_map::utils::{absolute_fileify_source_map, relative_fileify_source_map},
25};
26
27use crate::{
28    EcmascriptModuleContent,
29    chunk::{chunk_type::EcmascriptChunkType, placeable::EcmascriptChunkPlaceable},
30    references::async_module::{AsyncModuleOptions, OptionAsyncModuleOptions},
31    runtime_functions::TURBOPACK_ASYNC_MODULE,
32    utils::StringifyJs,
33};
34
35#[derive(
36    Debug,
37    Clone,
38    PartialEq,
39    Eq,
40    Hash,
41    TraceRawVcs,
42    TaskInput,
43    NonLocalValue,
44    Default,
45    Encode,
46    Decode,
47)]
48pub enum RewriteSourcePath {
49    AbsoluteFilePath(FileSystemPath),
50    RelativeFilePath(FileSystemPath, RcStr),
51    #[default]
52    None,
53}
54
55// Note we don't want to persist this as `module_factory_with_code_generation_issue` is already
56// persisted and we want to avoid duplicating it.
57#[turbo_tasks::value(shared, serialization = "skip")]
58#[derive(Default, Clone)]
59pub struct EcmascriptChunkItemContent {
60    pub inner_code: Rope,
61    pub source_map: Option<Rope>,
62    pub additional_ids: SmallVec<[ModuleId; 1]>,
63    pub options: EcmascriptChunkItemOptions,
64    pub rewrite_source_path: RewriteSourcePath,
65    pub placeholder_for_future_extensions: (),
66}
67
68#[turbo_tasks::value_impl]
69impl EcmascriptChunkItemContent {
70    #[turbo_tasks::function]
71    pub async fn new(
72        content: Vc<EcmascriptModuleContent>,
73        chunking_context: Vc<Box<dyn ChunkingContext>>,
74        async_module_options: Vc<OptionAsyncModuleOptions>,
75    ) -> Result<Vc<Self>> {
76        let supports_arrow_functions = *chunking_context
77            .environment()
78            .runtime_versions()
79            .supports_arrow_functions()
80            .await?;
81        let externals = *chunking_context
82            .environment()
83            .supports_commonjs_externals()
84            .await?;
85
86        let content = content.await?;
87        let async_module = async_module_options.owned().await?;
88        let strict = content.strict;
89
90        Ok(EcmascriptChunkItemContent {
91            rewrite_source_path: match *chunking_context.source_map_source_type().await? {
92                SourceMapSourceType::AbsoluteFileUri => {
93                    RewriteSourcePath::AbsoluteFilePath(chunking_context.root_path().owned().await?)
94                }
95                SourceMapSourceType::RelativeUri => RewriteSourcePath::RelativeFilePath(
96                    chunking_context.root_path().owned().await?,
97                    chunking_context
98                        .relative_path_from_chunk_root_to_project_root()
99                        .owned()
100                        .await?,
101                ),
102                SourceMapSourceType::TurbopackUri => RewriteSourcePath::None,
103            },
104            inner_code: content.inner_code.clone(),
105            source_map: content.source_map.clone(),
106            additional_ids: content.additional_ids.clone(),
107            options: if content.is_esm {
108                EcmascriptChunkItemOptions {
109                    strict: true,
110                    externals,
111                    async_module,
112                    supports_arrow_functions,
113                    ..Default::default()
114                }
115            } else {
116                if async_module.is_some() {
117                    bail!("CJS module can't be async.");
118                }
119
120                EcmascriptChunkItemOptions {
121                    strict,
122                    externals,
123                    supports_arrow_functions,
124                    // These things are not available in ESM
125                    module_and_exports: true,
126                    ..Default::default()
127                }
128            },
129            ..Default::default()
130        }
131        .cell())
132    }
133}
134
135impl EcmascriptChunkItemContent {
136    async fn module_factory(&self) -> Result<ResolvedVc<PersistedCode>> {
137        let mut code = CodeBuilder::default();
138        for additional_id in self.additional_ids.iter() {
139            writeln!(code, "{}, ", StringifyJs(&additional_id))?;
140        }
141
142        if self.options.supports_arrow_functions {
143            code += "((";
144        } else {
145            code += "(function(";
146        }
147        if self.options.module_and_exports {
148            code += "__turbopack_context__, module, exports";
149        } else {
150            code += "__turbopack_context__";
151        }
152        if self.options.supports_arrow_functions {
153            code += ") => {\n";
154        } else {
155            code += "){\n";
156        }
157
158        if self.options.strict {
159            code += "\"use strict\";\n\n";
160        } else {
161            code += "\n";
162        }
163
164        if self.options.async_module.is_some() {
165            write!(code, "return {TURBOPACK_ASYNC_MODULE}")?;
166            if self.options.supports_arrow_functions {
167                code += "(async (";
168            } else {
169                code += "(async function(";
170            }
171            code += "__turbopack_handle_async_dependencies__, __turbopack_async_result__";
172            if self.options.supports_arrow_functions {
173                code += ") => {";
174            } else {
175                code += "){";
176            }
177            code += " try {\n";
178        }
179
180        let source_map = match &self.rewrite_source_path {
181            RewriteSourcePath::AbsoluteFilePath(path) => {
182                absolute_fileify_source_map(self.source_map.as_ref(), path.clone()).await?
183            }
184            RewriteSourcePath::RelativeFilePath(path, relative_path) => {
185                relative_fileify_source_map(
186                    self.source_map.as_ref(),
187                    path.clone(),
188                    relative_path.clone(),
189                )
190                .await?
191            }
192            RewriteSourcePath::None => self.source_map.clone(),
193        };
194
195        code.push_source(&self.inner_code, source_map);
196
197        if let Some(opts) = &self.options.async_module {
198            write!(
199                code,
200                "__turbopack_async_result__();\n}} catch(e) {{ __turbopack_async_result__(e); }} \
201                 }}, {});",
202                opts.has_top_level_await
203            )?;
204        }
205
206        code += "})";
207
208        Ok(code.build().cell_persisted())
209    }
210}
211
212#[derive(PartialEq, Eq, Default, Debug, Clone, TraceRawVcs, NonLocalValue, Encode, Decode)]
213pub struct EcmascriptChunkItemOptions {
214    /// Whether this chunk item should be in "use strict" mode.
215    pub strict: bool,
216    /// Whether this chunk item's module factory should include a `module` and
217    /// `exports` argument.
218    pub module_and_exports: bool,
219    /// Whether this chunk item's module factory should include a
220    /// `__turbopack_external_require__` argument.
221    pub externals: bool,
222    /// Whether this chunk item's module is async (either has a top level await
223    /// or is importing async modules).
224    pub async_module: Option<AsyncModuleOptions>,
225    /// Whether the environment supports arrow functions (e.g. when targeting modern browsers).
226    pub supports_arrow_functions: bool,
227    pub placeholder_for_future_extensions: (),
228}
229
230#[derive(
231    Debug, Clone, PartialEq, Eq, Hash, TraceRawVcs, TaskInput, NonLocalValue, Encode, Decode,
232)]
233pub struct EcmascriptChunkItemWithAsyncInfo {
234    pub chunk_item: ResolvedVc<Box<dyn EcmascriptChunkItem>>,
235    pub async_info: Option<ResolvedVc<AsyncModuleInfo>>,
236}
237
238impl EcmascriptChunkItemWithAsyncInfo {
239    pub fn from_chunk_item(
240        chunk_item: &ChunkItemWithAsyncModuleInfo,
241    ) -> Result<EcmascriptChunkItemWithAsyncInfo> {
242        let ChunkItemWithAsyncModuleInfo {
243            chunk_item,
244            chunk_type: _,
245            module: _,
246            async_info,
247        } = chunk_item;
248        let Some(chunk_item) =
249            ResolvedVc::try_downcast::<Box<dyn EcmascriptChunkItem>>(*chunk_item)
250        else {
251            bail!("Chunk item is not an ecmascript chunk item but reporting chunk type ecmascript");
252        };
253        Ok(EcmascriptChunkItemWithAsyncInfo {
254            chunk_item,
255            async_info: *async_info,
256        })
257    }
258}
259
260#[async_trait]
261#[turbo_tasks::value_trait]
262pub trait EcmascriptChunkItem: ChunkItem + OutputAssetsReference {
263    /// Fetches the content of the chunk item with async module info.
264    /// When `estimated` is true, it's ok to provide an estimated content, since it's only used for
265    /// compute the chunking. When `estimated` is true, this function should not invoke other
266    /// chunking operations that would cause cycles.
267    async fn content_with_async_module_info(
268        &self,
269        async_module_info: Option<Vc<AsyncModuleInfo>>,
270        estimated: bool,
271    ) -> Result<Vc<EcmascriptChunkItemContent>>;
272}
273
274pub trait EcmascriptChunkItemExt {
275    /// Generates the module factory for this chunk item.
276    fn code(self: Vc<Self>, async_module_info: Option<Vc<AsyncModuleInfo>>) -> Vc<Code>;
277}
278
279impl<T> EcmascriptChunkItemExt for T
280where
281    T: Upcast<Box<dyn EcmascriptChunkItem>>,
282{
283    /// Generates the module factory for this chunk item.
284    fn code(self: Vc<Self>, async_module_info: Option<Vc<AsyncModuleInfo>>) -> Vc<Code> {
285        module_factory_with_code_generation_issue(Vc::upcast_non_strict(self), async_module_info)
286            .to_code()
287    }
288}
289
290#[turbo_tasks::function]
291async fn module_factory_with_code_generation_issue(
292    chunk_item: Vc<Box<dyn EcmascriptChunkItem>>,
293    async_module_info: Option<Vc<AsyncModuleInfo>>,
294) -> Result<Vc<PersistedCode>> {
295    async fn get_content(
296        chunk_item: Vc<Box<dyn EcmascriptChunkItem>>,
297        async_module_info: Option<Vc<AsyncModuleInfo>>,
298    ) -> Result<ResolvedVc<PersistedCode>> {
299        let chunk_item_ref = chunk_item.into_trait_ref().await?;
300        let content = chunk_item_ref
301            .content_with_async_module_info(async_module_info, false)
302            .await?
303            .await?;
304        content.module_factory().await
305    }
306    let content = get_content(chunk_item, async_module_info).await;
307    Ok(match content {
308        Ok(factory) => *factory,
309        Err(error) => {
310            let id = chunk_item.asset_ident().to_string().await;
311            let id = id.as_ref().map_or_else(|_| "unknown", |id| &**id);
312
313            // ast-grep-ignore: no-context-format
314            let error = error.context(format!(
315                "An error occurred while generating the chunk item {id}"
316            ));
317            let error_message = format!("{}", PrettyPrintError(&error)).into();
318            let js_error_message = serde_json::to_string(&error_message)?;
319            CodeGenerationIssue {
320                severity: IssueSeverity::Error,
321                path: chunk_item.asset_ident().await?.path.clone(),
322                title: StyledString::Text(rcstr!("Code generation for chunk item errored"))
323                    .resolved_cell(),
324                message: StyledString::Text(error_message).resolved_cell(),
325                source: None,
326            }
327            .resolved_cell()
328            .emit();
329            let mut code = CodeBuilder::default();
330            code += "(() => {{\n\n";
331            writeln!(code, "throw new Error({error});", error = &js_error_message)?;
332            code += "\n}})";
333            *code.build().cell_persisted()
334        }
335    })
336}
337
338/// Generic chunk item that wraps any EcmascriptChunkPlaceable module.
339/// This replaces the need for individual per-module ChunkItem wrapper structs.
340#[turbo_tasks::value]
341pub struct EcmascriptModuleChunkItem {
342    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
343    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
344    module_graph: ResolvedVc<ModuleGraph>,
345}
346
347/// Factory function to create an EcmascriptModuleChunkItem.
348/// Use this instead of implementing ChunkableModule::as_chunk_item() on each module.
349pub fn ecmascript_chunk_item(
350    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
351    module_graph: ResolvedVc<ModuleGraph>,
352    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
353) -> Vc<Box<dyn ChunkItem>> {
354    Vc::upcast(
355        EcmascriptModuleChunkItem {
356            module,
357            chunking_context,
358            module_graph,
359        }
360        .cell(),
361    )
362}
363
364#[turbo_tasks::value_impl]
365impl ChunkItem for EcmascriptModuleChunkItem {
366    #[turbo_tasks::function]
367    fn asset_ident(&self) -> Vc<AssetIdent> {
368        self.module.ident()
369    }
370
371    #[turbo_tasks::function]
372    fn content_ident(&self) -> Vc<AssetIdent> {
373        self.module
374            .chunk_item_content_ident(*self.chunking_context, *self.module_graph)
375    }
376
377    fn ty(&self) -> Vc<Box<dyn ChunkType>> {
378        Vc::upcast(Vc::<EcmascriptChunkType>::default())
379    }
380
381    #[turbo_tasks::function]
382    fn module(&self) -> Vc<Box<dyn Module>> {
383        Vc::upcast(*self.module)
384    }
385
386    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
387        *self.chunking_context
388    }
389}
390
391#[turbo_tasks::value_impl]
392impl OutputAssetsReference for EcmascriptModuleChunkItem {
393    #[turbo_tasks::function]
394    fn references(&self) -> Vc<turbopack_core::output::OutputAssetsWithReferenced> {
395        self.module
396            .chunk_item_output_assets(*self.chunking_context, *self.module_graph)
397    }
398}
399
400#[async_trait]
401#[turbo_tasks::value_impl]
402impl EcmascriptChunkItem for EcmascriptModuleChunkItem {
403    async fn content_with_async_module_info(
404        &self,
405        async_module_info: Option<Vc<AsyncModuleInfo>>,
406        estimated: bool,
407    ) -> Result<Vc<EcmascriptChunkItemContent>> {
408        Ok(self.module.chunk_item_content(
409            *self.chunking_context,
410            *self.module_graph,
411            async_module_info,
412            estimated,
413        ))
414    }
415}