Skip to content

Commit 79c16cb

Browse files
authored
Turbopack: port remaining NFT step (#82278)
Port the remainder of `Collect build traces` from JS to Turbopack (when using Turbopack, anyway), which was the generation of the `.next/next-[minimal-]server.js.nft.json` files Depends on #82340 Closes PACK-4166
1 parent 95ee107 commit 79c16cb

File tree

22 files changed

+889
-69
lines changed

22 files changed

+889
-69
lines changed

crates/napi/src/next_api/project.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use napi::{
99
};
1010
use next_api::{
1111
entrypoints::Entrypoints,
12+
next_server_nft::next_server_nft_assets,
1213
operation::{
1314
EntrypointsOperation, InstrumentationOperation, MiddlewareOperation, OptionEndpoint,
1415
RouteOperation,
@@ -988,7 +989,14 @@ async fn output_assets_operation(
988989
.flat_map(|assets| assets.iter().copied())
989990
.collect();
990991

991-
Ok(Vc::cell(output_assets.into_iter().collect()))
992+
let nft = next_server_nft_assets(container.project()).await?;
993+
994+
Ok(Vc::cell(
995+
output_assets
996+
.into_iter()
997+
.chain(nft.iter().copied())
998+
.collect(),
999+
))
9921000
}
9931001

9941002
#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]

crates/next-api/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod instrumentation;
1313
mod loadable_manifest;
1414
mod middleware;
1515
mod module_graph;
16+
pub mod next_server_nft;
1617
mod nft_json;
1718
pub mod operation;
1819
mod pages;
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
use std::collections::BTreeSet;
2+
3+
use anyhow::{Context, Result, bail};
4+
use either::Either;
5+
use next_core::{get_next_package, next_server::get_tracing_compile_time_info};
6+
use serde::{Deserialize, Serialize};
7+
use serde_json::{Value, json};
8+
use turbo_rcstr::RcStr;
9+
use turbo_tasks::{
10+
NonLocalValue, ResolvedVc, TaskInput, TryFlatJoinIterExt, TryJoinIterExt, Vc,
11+
trace::TraceRawVcs,
12+
};
13+
use turbo_tasks_fs::{DirectoryContent, DirectoryEntry, File, FileSystemPath, glob::Glob};
14+
use turbopack::externals_tracing_module_context;
15+
use turbopack_core::{
16+
asset::{Asset, AssetContent},
17+
context::AssetContext,
18+
file_source::FileSource,
19+
output::{OutputAsset, OutputAssets},
20+
reference_type::{CommonJsReferenceSubType, ReferenceType},
21+
resolve::{ExternalType, origin::PlainResolveOrigin, parse::Request},
22+
traced_asset::TracedAsset,
23+
};
24+
use turbopack_ecmascript::resolve::cjs_resolve;
25+
26+
use crate::{
27+
nft_json::{all_assets_from_entries_filtered, relativize_glob},
28+
project::Project,
29+
};
30+
31+
#[derive(
32+
PartialEq, Eq, TraceRawVcs, NonLocalValue, Deserialize, Serialize, Debug, Clone, Hash, TaskInput,
33+
)]
34+
enum ServerNftType {
35+
Minimal,
36+
Full,
37+
}
38+
39+
#[turbo_tasks::function]
40+
pub async fn next_server_nft_assets(project: Vc<Project>) -> Result<Vc<OutputAssets>> {
41+
Ok(Vc::cell(vec![
42+
ResolvedVc::upcast(
43+
ServerNftJsonAsset::new(project, ServerNftType::Full)
44+
.to_resolved()
45+
.await?,
46+
),
47+
ResolvedVc::upcast(
48+
ServerNftJsonAsset::new(project, ServerNftType::Minimal)
49+
.to_resolved()
50+
.await?,
51+
),
52+
]))
53+
}
54+
55+
#[turbo_tasks::value]
56+
pub struct ServerNftJsonAsset {
57+
project: ResolvedVc<Project>,
58+
ty: ServerNftType,
59+
}
60+
61+
#[turbo_tasks::value_impl]
62+
impl ServerNftJsonAsset {
63+
#[turbo_tasks::function]
64+
pub fn new(project: ResolvedVc<Project>, ty: ServerNftType) -> Vc<Self> {
65+
ServerNftJsonAsset { project, ty }.cell()
66+
}
67+
}
68+
69+
#[turbo_tasks::value_impl]
70+
impl OutputAsset for ServerNftJsonAsset {
71+
#[turbo_tasks::function]
72+
async fn path(&self) -> Result<Vc<FileSystemPath>> {
73+
let name = match self.ty {
74+
ServerNftType::Minimal => "next-minimal-server.js.nft.json",
75+
ServerNftType::Full => "next-server.js.nft.json",
76+
};
77+
78+
Ok(self.project.node_root().await?.join(name)?.cell())
79+
}
80+
}
81+
82+
#[turbo_tasks::value_impl]
83+
impl Asset for ServerNftJsonAsset {
84+
#[turbo_tasks::function]
85+
async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
86+
let this = self.await?;
87+
// Example: [project]/apps/my-website/.next/
88+
let base_dir = this
89+
.project
90+
.project_root_path()
91+
.await?
92+
.join(&this.project.node_root().await?.path)?;
93+
94+
let mut server_output_assets =
95+
all_assets_from_entries_filtered(self.entries(), None, Some(self.ignores()))
96+
.await?
97+
.iter()
98+
.map(async |m| {
99+
base_dir
100+
.get_relative_path_to(&*m.path().await?)
101+
.context("failed to compute relative path for server NFT JSON")
102+
})
103+
.try_join()
104+
.await?;
105+
106+
// A few hardcoded files (not recursive)
107+
server_output_assets.push("./package.json".into());
108+
109+
let next_dir = get_next_package(this.project.project_path().owned().await?).await?;
110+
for ty in ["app-page", "pages"] {
111+
let dir = next_dir.join(&format!("dist/server/route-modules/{ty}"))?;
112+
let module_path = dir.join("module.compiled.js")?;
113+
server_output_assets.push(
114+
base_dir
115+
.get_relative_path_to(&module_path)
116+
.context("failed to compute relative path for server NFT JSON")?,
117+
);
118+
119+
let contexts_dir = dir.join("vendored/contexts")?;
120+
let DirectoryContent::Entries(contexts_files) = &*contexts_dir.read_dir().await? else {
121+
bail!(
122+
"Expected contexts directory to be a directory, found: {:?}",
123+
contexts_dir
124+
);
125+
};
126+
for (_, entry) in contexts_files {
127+
let DirectoryEntry::File(file) = entry else {
128+
continue;
129+
};
130+
if file.extension() == "js" {
131+
server_output_assets.push(
132+
base_dir
133+
.get_relative_path_to(file)
134+
.context("failed to compute relative path for server NFT JSON")?,
135+
)
136+
}
137+
}
138+
}
139+
140+
server_output_assets.sort();
141+
// Dedupe as some entries may be duplicates: a file might be referenced multiple times,
142+
// e.g. as a RawModule (from an FS operation) and as an EcmascriptModuleAsset because it
143+
// was required.
144+
server_output_assets.dedup();
145+
146+
let json = json!({
147+
"version": 1,
148+
"files": server_output_assets
149+
});
150+
151+
Ok(AssetContent::file(File::from(json.to_string()).into()))
152+
}
153+
}
154+
155+
#[turbo_tasks::value_impl]
156+
impl ServerNftJsonAsset {
157+
#[turbo_tasks::function]
158+
async fn entries(&self) -> Result<Vc<OutputAssets>> {
159+
let is_standalone = *self.project.next_config().is_standalone().await?;
160+
161+
let asset_context = Vc::upcast(externals_tracing_module_context(
162+
ExternalType::CommonJs,
163+
get_tracing_compile_time_info(),
164+
));
165+
166+
let project_path = self.project.project_path().owned().await?;
167+
168+
let next_resolve_origin = Vc::upcast(PlainResolveOrigin::new(
169+
asset_context,
170+
get_next_package(project_path.clone()).await?.join("_")?,
171+
));
172+
173+
let cache_handler = self
174+
.project
175+
.next_config()
176+
.cache_handler(project_path.clone())
177+
.await?;
178+
let cache_handlers = self
179+
.project
180+
.next_config()
181+
.experimental_cache_handlers(project_path.clone())
182+
.await?;
183+
184+
// These are used by packages/next/src/server/require-hook.ts
185+
let shared_entries = ["styled-jsx", "styled-jsx/style", "styled-jsx/style.js"];
186+
187+
let cache_handler_entries = cache_handler
188+
.into_iter()
189+
.chain(cache_handlers.into_iter())
190+
.map(|f| {
191+
asset_context
192+
.process(
193+
Vc::upcast(FileSource::new(f.clone())),
194+
ReferenceType::CommonJs(CommonJsReferenceSubType::Undefined),
195+
)
196+
.module()
197+
});
198+
199+
let entries = match self.ty {
200+
ServerNftType::Full => Either::Left(
201+
if is_standalone {
202+
Either::Left(
203+
[
204+
"next/dist/server/lib/start-server",
205+
"next/dist/server/next",
206+
"next/dist/server/require-hook",
207+
]
208+
.into_iter(),
209+
)
210+
} else {
211+
Either::Right(std::iter::empty())
212+
}
213+
.chain(std::iter::once("next/dist/server/next-server")),
214+
),
215+
ServerNftType::Minimal => Either::Right(std::iter::once(
216+
"next/dist/compiled/next-server/server.runtime.prod",
217+
)),
218+
};
219+
220+
Ok(Vc::cell(
221+
cache_handler_entries
222+
.chain(
223+
shared_entries
224+
.into_iter()
225+
.chain(entries)
226+
.map(async |path| {
227+
Ok(cjs_resolve(
228+
next_resolve_origin,
229+
Request::parse_string(path.into()),
230+
CommonJsReferenceSubType::Undefined,
231+
None,
232+
false,
233+
)
234+
.primary_modules()
235+
.await?
236+
.into_iter()
237+
.map(|m| **m))
238+
})
239+
.try_flat_join()
240+
.await?,
241+
)
242+
.map(|m| Vc::upcast::<Box<dyn OutputAsset>>(TracedAsset::new(m)).to_resolved())
243+
.try_join()
244+
.await?,
245+
))
246+
}
247+
248+
#[turbo_tasks::function]
249+
async fn ignores(&self) -> Result<Vc<Glob>> {
250+
let is_standalone = *self.project.next_config().is_standalone().await?;
251+
let has_next_support = *self.project.next_config().ci_has_next_support().await?;
252+
let project_path = self.project.project_path().owned().await?;
253+
254+
let output_file_tracing_excludes = self
255+
.project
256+
.next_config()
257+
.output_file_tracing_excludes()
258+
.await?;
259+
let mut additional_ignores = BTreeSet::new();
260+
if let Some(output_file_tracing_excludes) = output_file_tracing_excludes
261+
.as_ref()
262+
.and_then(Value::as_object)
263+
{
264+
for (glob_pattern, exclude_patterns) in output_file_tracing_excludes {
265+
// Check if the route matches the glob pattern
266+
let glob = Glob::new(RcStr::from(glob_pattern.clone()), Default::default()).await?;
267+
if glob.matches("next-server")
268+
&& let Some(patterns) = exclude_patterns.as_array()
269+
{
270+
for pattern in patterns {
271+
if let Some(pattern_str) = pattern.as_str() {
272+
let (glob, root) = relativize_glob(pattern_str, project_path.clone())?;
273+
let glob = if root.path.is_empty() {
274+
glob.to_string()
275+
} else {
276+
format!("{root}/{glob}")
277+
};
278+
additional_ignores.insert(glob);
279+
}
280+
}
281+
}
282+
}
283+
}
284+
285+
let server_ignores_glob = [
286+
"**/node_modules/react{,-dom,-server-dom-turbopack}/**/*.development.js",
287+
"**/*.d.ts",
288+
"**/*.map",
289+
"**/next/dist/pages/**/*",
290+
"**/next/dist/compiled/next-server/**/*.dev.js",
291+
"**/next/dist/compiled/webpack/*",
292+
"**/node_modules/webpack5/**/*",
293+
"**/next/dist/server/lib/route-resolver*",
294+
"**/next/dist/compiled/semver/semver/**/*.js",
295+
"**/next/dist/compiled/jest-worker/**/*",
296+
// Turbopack doesn't support AMP
297+
"**/next/dist/compiled/@ampproject/toolbox-optimizer/**/*",
298+
// -- The following were added for Turbopack specifically --
299+
// client/components/use-action-queue.ts has a process.env.NODE_ENV guard, but we can't set that due to React: https://github.com/vercel/next.js/pull/75254
300+
"**/next/dist/next-devtools/userspace/use-app-dev-rendering-indicator.js",
301+
// client/components/app-router.js has a process.env.NODE_ENV guard, but we
302+
// can't set that.
303+
"**/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js",
304+
// server/lib/router-server.js doesn't guard this require:
305+
"**/next/dist/server/lib/router-utils/setup-dev-bundler.js",
306+
// server/next.js doesn't guard this require
307+
"**/next/dist/server/dev/next-dev-server.js",
308+
// next/dist/compiled/babel* pulls in this, but we never actually transpile at
309+
// deploy-time
310+
"**/next/dist/compiled/browserslist/**",
311+
]
312+
.into_iter()
313+
.chain(additional_ignores.iter().map(|s| s.as_str()))
314+
// only ignore image-optimizer code when
315+
// this is being handled outside of next-server
316+
.chain(if has_next_support {
317+
Either::Left(
318+
[
319+
"**/node_modules/sharp/**/*",
320+
"**/@img/sharp-libvips*/**/*",
321+
"**/next/dist/server/image-optimizer.js",
322+
]
323+
.into_iter(),
324+
)
325+
} else {
326+
Either::Right(std::iter::empty())
327+
})
328+
.chain(if is_standalone {
329+
Either::Left(std::iter::empty())
330+
} else {
331+
Either::Right(["**/*/next/dist/server/next.js", "**/*/next/dist/bin/next"].into_iter())
332+
})
333+
.map(|g| Glob::new(g.into(), Default::default()))
334+
.collect::<Vec<_>>();
335+
336+
Ok(match self.ty {
337+
ServerNftType::Full => Glob::alternatives(server_ignores_glob),
338+
ServerNftType::Minimal => Glob::alternatives(
339+
server_ignores_glob
340+
.into_iter()
341+
.chain(
342+
[
343+
"**/next/dist/compiled/edge-runtime/**/*",
344+
"**/next/dist/server/web/sandbox/**/*",
345+
"**/next/dist/server/post-process.js",
346+
]
347+
.into_iter()
348+
.map(|g| Glob::new(g.into(), Default::default())),
349+
)
350+
.collect(),
351+
),
352+
})
353+
}
354+
}

crates/next-api/src/nft_json.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,10 @@ impl Asset for NftJsonAsset {
350350
/// traversal doesn't need to consider them and can just traverse 'down' the tree.
351351
/// The main alternative is to merge glob evaluation with directory traversal which is what the npm
352352
/// `glob` package does, but this would be a substantial rewrite.`
353-
fn relativize_glob(glob: &str, relative_to: FileSystemPath) -> Result<(&str, FileSystemPath)> {
353+
pub(crate) fn relativize_glob(
354+
glob: &str,
355+
relative_to: FileSystemPath,
356+
) -> Result<(&str, FileSystemPath)> {
354357
let mut relative_to = relative_to;
355358
let mut processed_glob = glob;
356359
loop {

0 commit comments

Comments
 (0)