turbopack_ecmascript/manifest/
loader_item.rs

1use std::io::Write as _;
2
3use anyhow::{Result, anyhow};
4use indoc::writedoc;
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{ResolvedVc, TryJoinIterExt, Vc};
7use turbopack_core::{
8    chunk::{
9        ChunkData, ChunkItem, ChunkType, ChunkableModule, ChunkingContext, ChunksData,
10        ModuleChunkItemIdExt,
11    },
12    ident::AssetIdent,
13    module::Module,
14    module_graph::ModuleGraph,
15    output::OutputAssets,
16};
17
18use super::chunk_asset::ManifestAsyncModule;
19use crate::{
20    chunk::{
21        EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkPlaceable,
22        EcmascriptChunkType, data::EcmascriptChunkData,
23    },
24    runtime_functions::{TURBOPACK_EXPORT_VALUE, TURBOPACK_LOAD, TURBOPACK_REQUIRE},
25    utils::{StringifyJs, StringifyModuleId},
26};
27
28fn modifier() -> RcStr {
29    rcstr!("loader")
30}
31
32/// The manifest loader item is shipped in the same chunk that uses the dynamic
33/// `import()` expression.
34///
35/// Its responsibility is to load the manifest chunk from the server. The
36/// dynamic import has been rewritten to import this manifest loader item,
37/// which will load the manifest chunk from the server, which will load all
38/// the chunks needed by the dynamic import. Finally, we'll be able to import
39/// the module we're trying to dynamically import.
40///
41/// Splitting the dynamic import into a quickly generate-able manifest loader
42/// item and a slow-to-generate manifest chunk allows for faster incremental
43/// compilation. The traversal won't be performed until the dynamic import is
44/// actually reached, instead of eagerly as part of the chunk that the dynamic
45/// import appears in.
46#[turbo_tasks::value]
47pub struct ManifestLoaderChunkItem {
48    manifest: ResolvedVc<ManifestAsyncModule>,
49    module_graph: ResolvedVc<ModuleGraph>,
50    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
51}
52
53#[turbo_tasks::value_impl]
54impl ManifestLoaderChunkItem {
55    #[turbo_tasks::function]
56    pub fn new(
57        manifest: ResolvedVc<ManifestAsyncModule>,
58        module_graph: ResolvedVc<ModuleGraph>,
59        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
60    ) -> Vc<Self> {
61        Self::cell(ManifestLoaderChunkItem {
62            manifest,
63            module_graph,
64            chunking_context,
65        })
66    }
67
68    #[turbo_tasks::function]
69    pub async fn chunks_data(&self) -> Result<Vc<ChunksData>> {
70        let chunks = self.manifest.manifest_chunks();
71        Ok(ChunkData::from_assets(
72            self.chunking_context.output_root().await?.clone_value(),
73            chunks,
74        ))
75    }
76
77    #[turbo_tasks::function]
78    pub fn asset_ident_for(module: Vc<Box<dyn ChunkableModule>>) -> Vc<AssetIdent> {
79        module.ident().with_modifier(modifier())
80    }
81}
82
83#[turbo_tasks::value_impl]
84impl ChunkItem for ManifestLoaderChunkItem {
85    #[turbo_tasks::function]
86    fn asset_ident(&self) -> Vc<AssetIdent> {
87        self.manifest.module_ident().with_modifier(modifier())
88    }
89
90    #[turbo_tasks::function]
91    fn content_ident(&self) -> Vc<AssetIdent> {
92        self.manifest.content_ident().with_modifier(modifier())
93    }
94
95    #[turbo_tasks::function]
96    async fn references(self: Vc<Self>) -> Result<Vc<OutputAssets>> {
97        let this = self.await?;
98        let mut references = (*this.manifest.manifest_chunks().await?).clone();
99        for chunk_data in &*self.chunks_data().await? {
100            references.extend(chunk_data.references().await?);
101        }
102
103        Ok(Vc::cell(references))
104    }
105
106    #[turbo_tasks::function]
107    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
108        *ResolvedVc::upcast(self.chunking_context)
109    }
110
111    #[turbo_tasks::function]
112    async fn ty(&self) -> Result<Vc<Box<dyn ChunkType>>> {
113        Ok(Vc::upcast(
114            Vc::<EcmascriptChunkType>::default().resolve().await?,
115        ))
116    }
117
118    #[turbo_tasks::function]
119    fn module(&self) -> Vc<Box<dyn Module>> {
120        *ResolvedVc::upcast(self.manifest)
121    }
122}
123
124#[turbo_tasks::value_impl]
125impl EcmascriptChunkItem for ManifestLoaderChunkItem {
126    #[turbo_tasks::function]
127    async fn content(self: Vc<Self>) -> Result<Vc<EcmascriptChunkItemContent>> {
128        let this = &*self.await?;
129        let mut code = Vec::new();
130
131        let manifest = this.manifest.await?;
132
133        // We need several items in order for a dynamic import to fully load. First, we
134        // need the chunk path of the manifest chunk, relative from the output root. The
135        // chunk is a servable file, which will contain the manifest chunk item, which
136        // will perform the actual chunk traversal and generate load statements.
137        let chunks_server_data = &*self.chunks_data().await?.iter().try_join().await?;
138
139        // We also need the manifest chunk item's id, which points to a CJS module that
140        // exports a promise for all of the necessary chunk loads.
141        let item_id = &*this
142            .manifest
143            .chunk_item_id(*ResolvedVc::upcast(manifest.chunking_context))
144            .await?;
145
146        // Finally, we need the id of the module that we're actually trying to
147        // dynamically import.
148        let placeable =
149            ResolvedVc::try_downcast::<Box<dyn EcmascriptChunkPlaceable>>(manifest.inner)
150                .ok_or_else(|| anyhow!("asset is not placeable in ecmascript chunk"))?;
151        let dynamic_id = &*placeable
152            .chunk_item_id(*ResolvedVc::upcast(manifest.chunking_context))
153            .await?;
154
155        // This is the code that will be executed when the dynamic import is reached.
156        // It will load the manifest chunk, which will load all the chunks needed by
157        // the dynamic import, and finally we'll be able to import the module we're
158        // trying to dynamically import.
159        // This is similar to what happens when the first evaluated chunk is executed
160        // on first page load, but it's happening on-demand instead of eagerly.
161        writedoc!(
162            code,
163            r#"
164                {TURBOPACK_EXPORT_VALUE}((parentImport) => {{
165                    return Promise.all({chunks_server_data}.map((chunk) => {TURBOPACK_LOAD}(chunk))).then(() => {{
166                        return {TURBOPACK_REQUIRE}({item_id});
167                    }}).then((chunks) => {{
168                        return Promise.all(chunks.map((chunk) => {TURBOPACK_LOAD}(chunk)));
169                    }}).then(() => {{
170                        return parentImport({dynamic_id});
171                    }});
172                }});
173            "#,
174            chunks_server_data = StringifyJs(
175                &chunks_server_data
176                    .iter()
177                    .map(|chunk_data| EcmascriptChunkData::new(chunk_data))
178                    .collect::<Vec<_>>()
179            ),
180            item_id = StringifyModuleId(item_id),
181            dynamic_id = StringifyModuleId(dynamic_id),
182        )?;
183
184        Ok(EcmascriptChunkItemContent {
185            inner_code: code.into(),
186            ..Default::default()
187        }
188        .into())
189    }
190}