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