1use std::collections::BTreeSet;
2
3use anyhow::{Context, Result, bail};
4use bincode::{Decode, Encode};
5use either::Either;
6use next_core::{get_next_package, next_server::get_tracing_compile_time_info};
7use serde_json::json;
8use turbo_rcstr::{RcStr, rcstr};
9use turbo_tasks::{ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc, trace::TraceRawVcs};
10use turbo_tasks_fs::{
11 DirectoryContent, DirectoryEntry, File, FileContent, FileSystemPath,
12 glob::{Glob, GlobOptions},
13};
14use turbo_tasks_hash::HashAlgorithm;
15use turbopack::externals_tracing_module_context;
16use turbopack_core::{
17 asset::{Asset, AssetContent},
18 context::AssetContext,
19 module::{Module, Modules},
20 module_graph::{GraphEntries, ModuleGraph, SingleModuleGraph},
21 output::{OutputAsset, OutputAssets, OutputAssetsReference},
22 reference_type::CommonJsReferenceSubType,
23 resolve::{ResolveErrorMode, origin::PlainResolveOrigin, parse::Request},
24};
25use turbopack_resolve::ecmascript::cjs_resolve;
26
27use crate::{nft::traced_modules_for_entries, project::Project};
28
29#[turbo_tasks::function]
40pub(crate) async fn require_hook_modules(
41 project_path: FileSystemPath,
42 asset_context: Vc<Box<dyn AssetContext>>,
43) -> Result<Vc<Modules>> {
44 let next_resolve_origin = Vc::upcast(PlainResolveOrigin::new(
45 asset_context,
46 get_next_package(project_path).await?.join("_")?,
47 ));
48
49 Ok(Vc::cell(
50 ["styled-jsx", "styled-jsx/style", "styled-jsx/style.js"]
51 .into_iter()
52 .map(async |request| {
53 Ok(cjs_resolve(
54 next_resolve_origin,
55 Request::parse_string(request.into()),
56 CommonJsReferenceSubType::Undefined,
57 None,
58 ResolveErrorMode::Error,
59 )
60 .await?
61 .primary_modules()
62 .await?
63 .into_iter())
64 })
65 .try_flat_join()
66 .await?,
67 ))
68}
69
70#[turbo_tasks::function]
76pub(crate) async fn pages_renderer_modules(project_path: FileSystemPath) -> Result<Vc<Modules>> {
77 let asset_context = Vc::upcast(externals_tracing_module_context(
78 get_tracing_compile_time_info(),
79 false,
80 None,
81 ));
82 let next_resolve_origin = Vc::upcast(PlainResolveOrigin::new(
83 asset_context,
84 get_next_package(project_path).await?.join("_")?,
85 ));
86
87 Ok(Vc::cell(
88 cjs_resolve(
89 next_resolve_origin,
90 Request::parse_string(
91 "next/dist/compiled/next-server/pages-turbo.runtime.prod.js".into(),
92 ),
93 CommonJsReferenceSubType::Undefined,
94 None,
95 ResolveErrorMode::Error,
96 )
97 .await?
98 .primary_modules()
99 .await?
100 .to_vec(),
101 ))
102}
103
104#[turbo_tasks::task_input]
105#[derive(PartialEq, Eq, TraceRawVcs, Debug, Clone, Hash, Encode, Decode)]
106enum ServerNftType {
107 Minimal,
108 Full,
109}
110
111#[turbo_tasks::function]
112pub async fn next_server_nft_assets(project: Vc<Project>) -> Result<Vc<OutputAssets>> {
113 let is_standalone = *project.next_config().is_standalone().await?;
114
115 if *project.next_config().is_using_adapter().await? && !is_standalone {
116 return Ok(Vc::cell(vec![]));
128 }
129
130 let has_next_support = *project.ci_has_next_support().await?;
131
132 let minimal = ResolvedVc::upcast(
133 ServerNftJsonAsset::new(project, ServerNftType::Minimal)
134 .to_resolved()
135 .await?,
136 );
137
138 if has_next_support && !is_standalone {
139 Ok(Vc::cell(vec![minimal]))
141 } else {
142 Ok(Vc::cell(vec![
143 minimal,
144 ResolvedVc::upcast(
145 ServerNftJsonAsset::new(project, ServerNftType::Full)
146 .to_resolved()
147 .await?,
148 ),
149 ]))
150 }
151}
152
153#[turbo_tasks::value]
154pub struct ServerNftJsonAsset {
155 project: ResolvedVc<Project>,
156 ty: ServerNftType,
157}
158
159#[turbo_tasks::value_impl]
160impl ServerNftJsonAsset {
161 #[turbo_tasks::function]
162 pub fn new(project: ResolvedVc<Project>, ty: ServerNftType) -> Vc<Self> {
163 ServerNftJsonAsset { project, ty }.cell()
164 }
165}
166
167#[turbo_tasks::value_impl]
168impl OutputAssetsReference for ServerNftJsonAsset {}
169
170#[turbo_tasks::value_impl]
171impl OutputAsset for ServerNftJsonAsset {
172 #[turbo_tasks::function]
173 async fn path(&self) -> Result<Vc<FileSystemPath>> {
174 let name = match self.ty {
175 ServerNftType::Minimal => "next-minimal-server.js.nft.json",
176 ServerNftType::Full => "next-server.js.nft.json",
177 };
178
179 Ok(self.project.node_root().await?.join(name)?.cell())
180 }
181}
182
183#[turbo_tasks::value_impl]
184impl Asset for ServerNftJsonAsset {
185 #[turbo_tasks::function]
186 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
187 let this = self.await?;
188
189 let base_dir = this
191 .project
192 .project_root_path()
193 .await?
194 .join(&this.project.node_root().await?.path)?;
195
196 let module_graph = ModuleGraph::from_graphs(
197 vec![SingleModuleGraph::new_with_entries(
198 GraphEntries::new(vec![], self.entries().owned().await?).resolved_cell(),
199 true,
200 false,
201 )],
202 None,
203 )
204 .connect();
205
206 let hash_salt = this.project.next_config().output_hash_salt();
207
208 let mut server_output_assets = traced_modules_for_entries(
209 module_graph,
210 Modules::empty(),
211 self.entries(),
212 Some(self.ignores()),
213 None,
214 )
215 .await?
216 .iter()
217 .map(async |m| {
218 Ok((
219 base_dir
220 .get_relative_path_to(&m.ident().await?.path)
221 .context("failed to compute relative path for server NFT JSON")?,
222 m.source()
223 .await?
224 .context("NFT module has no content")?
225 .content()
226 .hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
227 .await?,
228 ))
229 })
230 .try_join()
231 .await?;
232
233 let next_dir = get_next_package(this.project.project_path().owned().await?).await?;
234 for ty in ["app-page", "pages"] {
235 let dir = next_dir.join(&format!("dist/server/route-modules/{ty}"))?;
236 let module_path = dir.join("module.compiled.js")?;
237 server_output_assets.push((
238 base_dir
239 .get_relative_path_to(&module_path)
240 .context("failed to compute relative path for server NFT JSON")?,
241 module_path
242 .hash_file(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
243 .await?,
244 ));
245
246 let contexts_dir = dir.join("vendored/contexts")?;
247 let DirectoryContent::Entries(contexts_files) = &*contexts_dir.read_dir().await? else {
248 bail!(
249 "Expected contexts directory to be a directory, found: {:?}",
250 contexts_dir
251 );
252 };
253 for (_, entry) in contexts_files {
254 let DirectoryEntry::File(file) = entry else {
255 continue;
256 };
257 if file.extension() == Some("js") {
258 server_output_assets.push((
259 base_dir
260 .get_relative_path_to(file)
261 .context("failed to compute relative path for server NFT JSON")?,
262 file.hash_file(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
263 .await?,
264 ))
265 }
266 }
267 }
268
269 server_output_assets.sort_unstable();
270 server_output_assets.dedup();
274
275 let (files, file_hashes): (Vec<_>, Vec<_>) = server_output_assets.into_iter().unzip();
276 let json = json!({
277 "version": 1,
278 "files": files,
279 "fileHashes": file_hashes
280 });
281
282 Ok(AssetContent::file(
283 FileContent::Content(File::from(json.to_string())).cell(),
284 ))
285 }
286}
287
288fn next_owned_ignores(
291 ty: &ServerNftType,
292 has_next_support: bool,
293 is_standalone: bool,
294) -> Vec<RcStr> {
295 let mut globs = vec![
296 rcstr!("**/node_modules/react{,-dom,-server-dom-turbopack}/**/*.development.js"),
297 rcstr!("**/*.d.ts"),
298 rcstr!("**/*.map"),
299 rcstr!("**/next/dist/pages/**/*"),
300 rcstr!("**/next/dist/compiled/next-server/**/*.dev.js"),
301 rcstr!("**/next/dist/compiled/webpack/*"),
302 rcstr!("**/node_modules/webpack5/**/*"),
303 rcstr!("**/next/dist/server/lib/route-resolver*"),
304 rcstr!("**/next/dist/compiled/@mswjs/interceptors/ClientRequest/LICENSE"),
310 rcstr!("**/next/dist/compiled/@mswjs/interceptors/ClientRequest/llhttp/**"),
311 rcstr!("**/next/dist/compiled/semver/semver/**/*.js"),
312 rcstr!("**/next/dist/compiled/jest-worker/**/*"),
313 rcstr!("**/next/dist/next-devtools/userspace/use-app-dev-rendering-indicator.js"),
316 rcstr!("**/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js"),
319 rcstr!("**/next/dist/server/lib/router-utils/setup-dev-bundler.js"),
321 rcstr!("**/next/dist/server/dev/next-dev-server.js"),
323 rcstr!("**/next/dist/compiled/browserslist/**"),
326 ];
327
328 if has_next_support {
331 globs.extend([
332 rcstr!("**/node_modules/sharp/**/*"),
333 rcstr!("**/@img/sharp-libvips*/**/*"),
334 rcstr!("**/next/dist/server/image-optimizer.js"),
335 ]);
336 }
337
338 if !is_standalone {
339 globs.extend([
340 rcstr!("**/*/next/dist/server/next.js"),
341 rcstr!("**/*/next/dist/bin/next"),
342 ]);
343 }
344
345 if matches!(ty, ServerNftType::Minimal) {
346 globs.extend([
347 rcstr!("**/next/dist/compiled/edge-runtime/**/*"),
348 rcstr!("**/next/dist/server/web/sandbox/**/*"),
349 rcstr!("**/next/dist/server/post-process.js"),
350 ]);
351 }
352
353 globs
354}
355
356#[turbo_tasks::value_impl]
357impl ServerNftJsonAsset {
358 #[turbo_tasks::function]
359 async fn entries(&self) -> Result<Vc<Modules>> {
360 let is_standalone = *self.project.next_config().is_standalone().await?;
361
362 let prune = Glob::alternatives(
363 next_owned_ignores(
364 &self.ty,
365 *self.project.ci_has_next_support().await?,
366 is_standalone,
367 )
368 .into_iter()
369 .map(|g| Glob::new(g, GlobOptions::default()))
370 .collect(),
371 )
372 .to_resolved()
373 .await?;
374
375 let asset_context = Vc::upcast(externals_tracing_module_context(
376 get_tracing_compile_time_info(),
377 false,
378 Some((self.project.project_root_path().owned().await?, prune)),
379 ));
380
381 let project_path = self.project.project_path().owned().await?;
382
383 let next_resolve_origin = Vc::upcast(PlainResolveOrigin::new(
384 asset_context,
385 get_next_package(project_path.clone()).await?.join("_")?,
386 ));
387
388 let entries = match self.ty {
389 ServerNftType::Full => Either::Left(
390 if is_standalone {
391 Either::Left(
392 [
393 "next/dist/server/lib/start-server",
394 "next/dist/server/next",
395 "next/dist/server/require-hook",
396 ]
397 .into_iter(),
398 )
399 } else {
400 Either::Right(std::iter::empty())
401 }
402 .chain(std::iter::once("next/dist/server/next-server")),
403 ),
404 ServerNftType::Minimal => Either::Right(std::iter::once(
405 "next/dist/compiled/next-server/server.runtime.prod",
406 )),
407 };
408
409 let hook_modules = require_hook_modules(project_path, asset_context)
413 .owned()
414 .await?;
415
416 Ok(Vc::cell(
417 hook_modules
418 .into_iter()
419 .chain(
420 entries
421 .map(async |path| {
422 Ok(cjs_resolve(
423 next_resolve_origin,
424 Request::parse_string(path.into()),
425 CommonJsReferenceSubType::Undefined,
426 None,
427 ResolveErrorMode::Error,
428 )
429 .await?
430 .primary_modules()
431 .await?
432 .into_iter())
433 })
434 .try_flat_join()
435 .await?,
436 )
437 .collect(),
438 ))
439 }
440
441 #[turbo_tasks::function]
442 async fn ignores(&self) -> Result<Vc<Glob>> {
443 let is_standalone = *self.project.next_config().is_standalone().await?;
444 let has_next_support = *self.project.ci_has_next_support().await?;
445 let project_path = self.project.project_path().owned().await?;
446
447 let output_file_tracing_excludes = self
448 .project
449 .next_config()
450 .output_file_tracing_excludes(project_path)
451 .await?;
452 let mut additional_ignores = BTreeSet::new();
453
454 for (route_glob, exclude_patterns) in output_file_tracing_excludes.iter() {
455 if route_glob.await?.matches("next-server") {
457 for (glob, root) in exclude_patterns {
458 additional_ignores.insert(if root.path.is_empty() {
459 glob.clone()
460 } else {
461 format!("{root}/{glob}").into()
462 });
463 }
464 }
465 }
466
467 let server_ignores_glob = next_owned_ignores(&self.ty, has_next_support, is_standalone)
473 .into_iter()
474 .chain(additional_ignores)
475 .map(|g| Glob::new(g, Default::default()))
476 .collect::<Vec<_>>();
477
478 Ok(Glob::alternatives(server_ignores_glob))
479 }
480}