Skip to content

Commit 322c0ee

Browse files
authored
add nft bundle to SsrChunk (#72305)
A small PR to start inserting nft on the critical path. This is only enabled in production builds and implements the bare minimum to get NFT outputs by inserting it into the server assets. Followed up by another PR and inserts more hooks / flags elsewhere in the APIs.
1 parent e0ce7da commit 322c0ee

File tree

5 files changed

+225
-4
lines changed

5 files changed

+225
-4
lines changed

crates/next-api/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod global_module_id_strategy;
1212
mod instrumentation;
1313
mod loadable_manifest;
1414
mod middleware;
15+
mod nft_json;
1516
mod pages;
1617
pub mod paths;
1718
pub mod project;

crates/next-api/src/nft_json.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
use anyhow::{bail, Result};
2+
use serde_json::json;
3+
use turbo_tasks::{RcStr, ResolvedVc, ValueToString, Vc};
4+
use turbo_tasks_fs::{DiskFileSystem, File, FileSystem, FileSystemPath, VirtualFileSystem};
5+
use turbopack_core::{
6+
asset::{Asset, AssetContent},
7+
ident::AssetIdent,
8+
output::OutputAsset,
9+
reference::all_assets_from_entries,
10+
};
11+
12+
/// A json file that produces references to all files that are needed by the given module
13+
/// at runtime. This will include, for example, node native modules, unanalyzable packages,
14+
/// client side chunks, etc.
15+
///
16+
/// With this file, users can determine the minimum set of files that are needed alongside
17+
/// their bundle.
18+
#[turbo_tasks::value(shared)]
19+
pub struct NftJsonAsset {
20+
/// The chunk for which the asset is being generated
21+
chunk: Vc<Box<dyn OutputAsset>>,
22+
output_fs: Vc<DiskFileSystem>,
23+
project_fs: Vc<DiskFileSystem>,
24+
client_fs: Vc<Box<dyn FileSystem>>,
25+
/// Additional assets to include in the nft json. This can be used to manually collect assets
26+
/// that are known to be required but are not in the graph yet, for whatever reason.
27+
///
28+
/// An example of this is the two-phase approach used by the `ClientReferenceManifest` in
29+
/// next.js.
30+
additional_assets: Vec<ResolvedVc<Box<dyn OutputAsset>>>,
31+
}
32+
33+
#[turbo_tasks::value_impl]
34+
impl NftJsonAsset {
35+
#[turbo_tasks::function]
36+
pub fn new(
37+
chunk: Vc<Box<dyn OutputAsset>>,
38+
output_fs: Vc<DiskFileSystem>,
39+
project_fs: Vc<DiskFileSystem>,
40+
client_fs: Vc<Box<dyn FileSystem>>,
41+
additional_assets: Vec<ResolvedVc<Box<dyn OutputAsset>>>,
42+
) -> Vc<Self> {
43+
NftJsonAsset {
44+
chunk,
45+
output_fs,
46+
project_fs,
47+
client_fs,
48+
additional_assets,
49+
}
50+
.cell()
51+
}
52+
}
53+
54+
#[turbo_tasks::value(transparent)]
55+
pub struct OutputSpecifier(Option<RcStr>);
56+
57+
#[turbo_tasks::value_impl]
58+
impl NftJsonAsset {
59+
#[turbo_tasks::function]
60+
async fn ident_in_project_fs(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
61+
let this = self.await?;
62+
let project_fs = this.project_fs.await?;
63+
let output_fs = this.output_fs.await?;
64+
let nft_folder = self.ident().path().parent().await?;
65+
66+
if let Some(subdir) = output_fs.root.strip_prefix(&*project_fs.root) {
67+
Ok(this
68+
.project_fs
69+
.root()
70+
.join(subdir.into())
71+
.join(nft_folder.path.clone()))
72+
} else {
73+
// TODO: what are the implications of this?
74+
bail!("output fs not inside project fs");
75+
}
76+
}
77+
78+
#[turbo_tasks::function]
79+
async fn ident_in_client_fs(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
80+
Ok(self
81+
.await?
82+
.client_fs
83+
.root()
84+
.join(self.ident().path().parent().await?.path.clone()))
85+
}
86+
87+
#[turbo_tasks::function]
88+
async fn get_output_specifier(
89+
self: Vc<Self>,
90+
path: Vc<FileSystemPath>,
91+
) -> Result<Vc<OutputSpecifier>> {
92+
let this = self.await?;
93+
let path_fs = path.fs().resolve().await?;
94+
let path_ref = path.await?;
95+
let nft_folder = self.ident().path().parent().await?;
96+
97+
if path_fs == Vc::upcast(this.output_fs.resolve().await?) {
98+
// e.g. a referenced chunk
99+
return Ok(Vc::cell(Some(
100+
nft_folder.get_relative_path_to(&path_ref).unwrap(),
101+
)));
102+
} else if path_fs == Vc::upcast(this.project_fs.resolve().await?) {
103+
return Ok(Vc::cell(Some(
104+
self.ident_in_project_fs()
105+
.await?
106+
.get_relative_path_to(&path_ref)
107+
.unwrap(),
108+
)));
109+
} else if path_fs == Vc::upcast(this.client_fs.resolve().await?) {
110+
return Ok(Vc::cell(Some(
111+
self.ident_in_client_fs()
112+
.await?
113+
.get_relative_path_to(&path_ref)
114+
.unwrap()
115+
.replace("/_next/", "/.next/")
116+
.into(),
117+
)));
118+
}
119+
120+
if let Some(path_fs) = Vc::try_resolve_downcast_type::<VirtualFileSystem>(path_fs).await? {
121+
if path_fs.await?.name == "externals" || path_fs.await?.name == "traced" {
122+
return Ok(Vc::cell(Some(
123+
self.ident_in_project_fs()
124+
.await?
125+
.get_relative_path_to(
126+
&*this.project_fs.root().join(path_ref.path.clone()).await?,
127+
)
128+
.unwrap(),
129+
)));
130+
}
131+
}
132+
133+
println!("Unknown filesystem for {}", path.to_string().await?);
134+
Ok(Vc::cell(None))
135+
}
136+
}
137+
138+
#[turbo_tasks::value_impl]
139+
impl OutputAsset for NftJsonAsset {
140+
#[turbo_tasks::function]
141+
async fn ident(&self) -> Result<Vc<AssetIdent>> {
142+
let path = self.chunk.ident().path().await?;
143+
Ok(AssetIdent::from_path(
144+
path.fs
145+
.root()
146+
.join(format!("{}.nft.json", path.path).into()),
147+
))
148+
}
149+
}
150+
151+
#[turbo_tasks::value_impl]
152+
impl Asset for NftJsonAsset {
153+
#[turbo_tasks::function]
154+
async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
155+
let this = &*self.await?;
156+
let mut result = Vec::new();
157+
158+
let chunk = this.chunk.to_resolved().await?;
159+
let entries = this
160+
.additional_assets
161+
.iter()
162+
.copied()
163+
.chain(std::iter::once(chunk))
164+
.collect();
165+
for referenced_chunk in all_assets_from_entries(Vc::cell(entries)).await? {
166+
if referenced_chunk.ident().path().await?.extension_ref() == Some("map") {
167+
continue;
168+
}
169+
170+
if chunk == referenced_chunk.to_resolved().await? {
171+
continue;
172+
}
173+
174+
let specifier = self
175+
.get_output_specifier(referenced_chunk.ident().path())
176+
.await?;
177+
if let Some(specifier) = &*specifier {
178+
result.push(specifier.clone());
179+
}
180+
}
181+
182+
result.sort();
183+
result.dedup();
184+
let json = json!({
185+
"version": 1,
186+
"files": result
187+
});
188+
189+
Ok(AssetContent::file(File::from(json.to_string()).into()))
190+
}
191+
}

crates/next-api/src/pages.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ use turbopack_core::{
5050
file_source::FileSource,
5151
ident::AssetIdent,
5252
module::{Module, Modules},
53-
output::{OutputAsset, OutputAssets},
53+
output::{OptionOutputAsset, OutputAsset, OutputAssets},
5454
reference_type::{EcmaScriptModulesReferenceSubType, EntryReferenceSubType, ReferenceType},
5555
resolve::{origin::PlainResolveOrigin, parse::Request, pattern::Pattern},
5656
source::Source,
@@ -66,6 +66,7 @@ use crate::{
6666
},
6767
font::create_font_manifest,
6868
loadable_manifest::create_react_loadable_manifest,
69+
nft_json::NftJsonAsset,
6970
paths::{
7071
all_paths_in_root, all_server_paths, get_js_paths_from_root, get_paths_from_root,
7172
get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings,
@@ -879,9 +880,32 @@ impl PageEndpoint {
879880
)
880881
.await?;
881882

883+
let nft = if this
884+
.pages_project
885+
.project()
886+
.next_mode()
887+
.await?
888+
.is_production()
889+
{
890+
ResolvedVc::cell(Some(ResolvedVc::upcast(
891+
NftJsonAsset::new(
892+
*ssr_entry_chunk,
893+
this.pages_project.project().output_fs(),
894+
this.pages_project.project().project_fs(),
895+
this.pages_project.project().client_fs(),
896+
vec![],
897+
)
898+
.to_resolved()
899+
.await?,
900+
)))
901+
} else {
902+
ResolvedVc::cell(None)
903+
};
904+
882905
Ok(SsrChunk::NodeJs {
883906
entry: ssr_entry_chunk,
884907
dynamic_import_entries,
908+
nft,
885909
}
886910
.cell())
887911
}
@@ -1097,10 +1121,14 @@ impl PageEndpoint {
10971121
SsrChunk::NodeJs {
10981122
entry,
10991123
dynamic_import_entries,
1124+
nft,
11001125
} => {
11011126
let pages_manifest = self.pages_manifest(*entry).to_resolved().await?;
11021127
server_assets.push(pages_manifest);
11031128
server_assets.push(entry);
1129+
if let Some(nft) = &*nft.await? {
1130+
server_assets.push(*nft);
1131+
}
11041132

11051133
let loadable_manifest_output = self.react_loadable_manifest(dynamic_import_entries);
11061134
server_assets.extend(loadable_manifest_output.await?.iter().copied());
@@ -1373,6 +1401,7 @@ pub enum SsrChunk {
13731401
NodeJs {
13741402
entry: ResolvedVc<Box<dyn OutputAsset>>,
13751403
dynamic_import_entries: Vc<DynamicImportedChunks>,
1404+
nft: ResolvedVc<OptionOutputAsset>,
13761405
},
13771406
Edge {
13781407
files: Vc<OutputAssets>,

crates/next-api/src/project.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ impl Project {
539539
}
540540

541541
#[turbo_tasks::function]
542-
fn project_fs(&self) -> Vc<DiskFileSystem> {
542+
pub fn project_fs(&self) -> Vc<DiskFileSystem> {
543543
DiskFileSystem::new(
544544
PROJECT_FILESYSTEM_NAME.into(),
545545
self.root_path.clone(),
@@ -548,7 +548,7 @@ impl Project {
548548
}
549549

550550
#[turbo_tasks::function]
551-
fn client_fs(self: Vc<Self>) -> Vc<Box<dyn FileSystem>> {
551+
pub fn client_fs(self: Vc<Self>) -> Vc<Box<dyn FileSystem>> {
552552
let virtual_fs = VirtualFileSystem::new();
553553
Vc::upcast(virtual_fs)
554554
}

turbopack/crates/turbo-tasks-fs/src/virtual_fs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use super::{DirectoryContent, FileContent, FileMeta, FileSystem, FileSystemPath,
55

66
#[turbo_tasks::value]
77
pub struct VirtualFileSystem {
8-
name: RcStr,
8+
pub name: RcStr,
99
}
1010

1111
impl VirtualFileSystem {

0 commit comments

Comments
 (0)