Skip to main content

turbopack_ecmascript/chunk/
item.rs

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