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