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