turbopack_ecmascript/manifest/
loader_item.rs

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