Skip to main content

next_core/
page_loader.rs

1use std::io::Write;
2
3use anyhow::{Result, bail};
4use turbo_rcstr::{RcStr, rcstr};
5use turbo_tasks::{ResolvedVc, TryJoinIterExt, Vc, fxindexmap};
6use turbo_tasks_fs::{
7    self, File, FileContent, FileSystemPath, FileSystemPathOption, rope::RopeBuilder,
8};
9use turbopack_core::{
10    asset::{Asset, AssetContent},
11    chunk::{ChunkData, ChunkingContext, ChunksData},
12    context::AssetContext,
13    ident::AssetIdent,
14    module::Module,
15    output::{OutputAsset, OutputAssets, OutputAssetsReference, OutputAssetsWithReferenced},
16    proxied_asset::ProxiedAsset,
17    reference_type::{EntryReferenceSubType, ReferenceType},
18    source::Source,
19    virtual_source::VirtualSource,
20};
21use turbopack_ecmascript::{chunk::EcmascriptChunkData, utils::StringifyJs};
22
23use crate::{embed_js::next_js_file_path, util::get_asset_path_from_pathname};
24
25#[turbo_tasks::function]
26pub async fn create_page_loader_entry_module(
27    client_context: Vc<Box<dyn AssetContext>>,
28    entry_asset: Vc<Box<dyn Source>>,
29    pathname: RcStr,
30) -> Result<Vc<Box<dyn Module>>> {
31    let mut result = RopeBuilder::default();
32    writeln!(result, "const PAGE_PATH = {};\n", StringifyJs(&pathname))?;
33
34    let page_loader_path = next_js_file_path(rcstr!("entry/page-loader.ts"))
35        .owned()
36        .await?;
37    let base_code = page_loader_path.read();
38    if let FileContent::Content(base_file) = &*base_code.await? {
39        result += base_file.content()
40    } else {
41        bail!("required file `entry/page-loader.ts` not found");
42    }
43
44    let file = File::from(result.build());
45
46    let virtual_source = Vc::upcast(VirtualSource::new(
47        page_loader_path,
48        AssetContent::file(FileContent::Content(file).cell()),
49    ));
50
51    let module = client_context
52        .process(
53            entry_asset,
54            ReferenceType::Entry(EntryReferenceSubType::Page),
55        )
56        .module()
57        .to_resolved()
58        .await?;
59
60    let module = client_context
61        .process(
62            virtual_source,
63            ReferenceType::Internal(ResolvedVc::cell(fxindexmap! {
64                rcstr!("PAGE") => module,
65            })),
66        )
67        .module();
68    Ok(module)
69}
70
71#[turbo_tasks::value(shared)]
72pub struct PageLoaderAsset {
73    pub server_root: FileSystemPath,
74    pub pathname: RcStr,
75    pub rebase_prefix_path: ResolvedVc<FileSystemPathOption>,
76    pub page_chunks: ResolvedVc<OutputAssets>,
77    pub chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
78    pub use_fixed_path: bool,
79}
80
81#[turbo_tasks::value_impl]
82impl PageLoaderAsset {
83    #[turbo_tasks::function]
84    pub fn new(
85        server_root: FileSystemPath,
86        pathname: RcStr,
87        rebase_prefix_path: ResolvedVc<FileSystemPathOption>,
88        page_chunks: ResolvedVc<OutputAssets>,
89        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
90        use_fixed_path: bool,
91    ) -> Vc<Self> {
92        Self {
93            server_root,
94            pathname,
95            rebase_prefix_path,
96            page_chunks,
97            chunking_context,
98            use_fixed_path,
99        }
100        .cell()
101    }
102
103    #[turbo_tasks::function]
104    async fn chunks_data(
105        &self,
106        rebase_prefix_path: Vc<FileSystemPathOption>,
107    ) -> Result<Vc<ChunksData>> {
108        let mut chunks = self.page_chunks;
109
110        // If we are provided a prefix path, we need to rewrite our chunk paths to
111        // remove that prefix.
112        if let Some(rebase_path) = &*rebase_prefix_path.await? {
113            let root_path = rebase_path.root().owned().await?;
114            let rebased = chunks
115                .await?
116                .iter()
117                .map(|&chunk| {
118                    let root_path = root_path.clone();
119
120                    async move {
121                        Vc::upcast::<Box<dyn OutputAsset>>(ProxiedAsset::new(
122                            *chunk,
123                            FileSystemPath::rebase(
124                                chunk.path().owned().await?,
125                                rebase_path.clone(),
126                                root_path.clone(),
127                            )
128                            .owned()
129                            .await?,
130                        ))
131                        .to_resolved()
132                        .await
133                    }
134                })
135                .try_join()
136                .await?;
137            chunks = ResolvedVc::cell(rebased);
138        };
139
140        Ok(ChunkData::from_assets(self.server_root.clone(), *chunks))
141    }
142}
143
144impl PageLoaderAsset {
145    async fn ident_for_path(&self) -> Result<Vc<AssetIdent>> {
146        let rebase_prefix_path = self.rebase_prefix_path.await?;
147        let root = rebase_prefix_path.as_ref().unwrap_or(&self.server_root);
148        Ok(AssetIdent::from_path(root.join(&format!(
149            "static/chunks/pages{}",
150            get_asset_path_from_pathname(&self.pathname, ".js")
151        ))?)
152        .with_modifier(rcstr!("page loader asset"))
153        .into_vc())
154    }
155}
156
157#[turbo_tasks::value_impl]
158impl OutputAssetsReference for PageLoaderAsset {
159    #[turbo_tasks::function]
160    async fn references(self: Vc<Self>) -> Result<Vc<OutputAssetsWithReferenced>> {
161        Ok(OutputAssetsWithReferenced::from_assets(
162            *self.await?.page_chunks,
163        ))
164    }
165}
166
167#[turbo_tasks::value_impl]
168impl OutputAsset for PageLoaderAsset {
169    #[turbo_tasks::function]
170    async fn path(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
171        let this = self.await?;
172        let ident = this.ident_for_path().await?;
173        if this.use_fixed_path {
174            // In development mode, don't include a content hash and put the chunk at e.g.
175            // `static/chunks/pages/page2.js`, so that the dev runtime can request it at a known
176            // path.
177            // https://github.com/vercel/next.js/blob/84873e00874e096e6c4951dcf070e8219ed414e5/packages/next/src/client/route-loader.ts#L256-L271
178            Ok(ident.await?.path.clone().cell())
179        } else {
180            Ok(this
181                .chunking_context
182                .chunk_path(Some(Vc::upcast(self)), ident, None, rcstr!(".js")))
183        }
184    }
185}
186
187#[turbo_tasks::value_impl]
188impl Asset for PageLoaderAsset {
189    #[turbo_tasks::function]
190    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
191        let this = &*self.await?;
192
193        let chunks_data = self.chunks_data(*this.rebase_prefix_path).await?;
194        let chunks_data = chunks_data.iter().try_join().await?;
195        let chunks_data: Vec<_> = chunks_data
196            .iter()
197            .map(|chunk_data| EcmascriptChunkData::new(chunk_data))
198            .collect();
199
200        let content = format!(
201            "__turbopack_load_page_chunks__({}, {:#})\n",
202            StringifyJs(&this.pathname),
203            StringifyJs(&chunks_data)
204        );
205
206        Ok(AssetContent::file(
207            FileContent::Content(File::from(content)).cell(),
208        ))
209    }
210}