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},
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(file.into()),
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    }
154}
155
156#[turbo_tasks::value_impl]
157impl OutputAsset for PageLoaderAsset {
158    #[turbo_tasks::function]
159    async fn path(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
160        let this = self.await?;
161        let ident = this.ident_for_path().await?;
162        if this.use_fixed_path {
163            // In development mode, don't include a content hash and put the chunk at e.g.
164            // `static/chunks/pages/page2.js`, so that the dev runtime can request it at a known
165            // path.
166            // https://github.com/vercel/next.js/blob/84873e00874e096e6c4951dcf070e8219ed414e5/packages/next/src/client/route-loader.ts#L256-L271
167            Ok(ident.path())
168        } else {
169            Ok(this
170                .chunking_context
171                .chunk_path(Some(Vc::upcast(self)), ident, None, rcstr!(".js")))
172        }
173    }
174
175    #[turbo_tasks::function]
176    async fn references(self: Vc<Self>) -> Result<Vc<OutputAssets>> {
177        let chunks = self.await?.page_chunks.await?;
178
179        let mut references = Vec::with_capacity(chunks.len());
180        for &chunk in chunks.iter() {
181            references.push(chunk);
182        }
183
184        // We don't need to strip the client relative prefix, because we won't be using
185        // these reference paths with `__turbopack_load__`.
186        for chunk_data in &*self.chunks_data(FileSystemPathOption::none()).await? {
187            references.extend(chunk_data.references().await?.iter().copied());
188        }
189
190        Ok(Vc::cell(references))
191    }
192}
193
194#[turbo_tasks::value_impl]
195impl Asset for PageLoaderAsset {
196    #[turbo_tasks::function]
197    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
198        let this = &*self.await?;
199
200        let chunks_data = self.chunks_data(*this.rebase_prefix_path).await?;
201        let chunks_data = chunks_data.iter().try_join().await?;
202        let chunks_data: Vec<_> = chunks_data
203            .iter()
204            .map(|chunk_data| EcmascriptChunkData::new(chunk_data))
205            .collect();
206
207        let content = format!(
208            "__turbopack_load_page_chunks__({}, {:#})\n",
209            StringifyJs(&this.pathname),
210            StringifyJs(&chunks_data)
211        );
212
213        Ok(AssetContent::file(File::from(content).into()))
214    }
215}