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 turbo_rcstr::{RcStr, rcstr};
8use turbo_tasks::{ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc};
9use turbo_tasks_fs::{
10 DirectoryContent, DirectoryEntry, File, FileContent, FileSystemPath,
11 glob::{Glob, GlobOptions},
12};
13use turbo_tasks_hash::HashAlgorithm;
14use turbopack::externals_tracing_module_context;
15use turbopack_core::{
16 asset::{Asset, AssetContent},
17 context::AssetContext,
18 file_source::FileSource,
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, nft_json_builder::NftJsonBuilder, 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(
77 project_path: FileSystemPath,
78 asset_context: Vc<Box<dyn AssetContext>>,
79) -> Result<Vc<Modules>> {
80 let next_resolve_origin = Vc::upcast(PlainResolveOrigin::new(
81 asset_context,
82 get_next_package(project_path).await?.join("_")?,
83 ));
84
85 Ok(Vc::cell(
86 cjs_resolve(
87 next_resolve_origin,
88 Request::parse_string(
89 "next/dist/compiled/next-server/pages-turbo.runtime.prod.js".into(),
90 ),
91 CommonJsReferenceSubType::Undefined,
92 None,
93 ResolveErrorMode::Error,
94 )
95 .await?
96 .primary_modules()
97 .await?
98 .to_vec(),
99 ))
100}
101
102#[turbo_tasks::task_input]
103#[derive(PartialEq, Eq, Debug, Clone, Hash, Encode, Decode)]
104enum ServerNftType {
105 Minimal,
106 Full,
107}
108
109#[turbo_tasks::function]
110pub async fn next_server_nft_assets(project: Vc<Project>) -> Result<Vc<OutputAssets>> {
111 let is_standalone = *project.next_config().is_standalone().await?;
112
113 if *project.next_config().is_using_adapter().await? && !is_standalone {
114 return Ok(Vc::cell(vec![]));
126 }
127
128 let has_next_support = *project.ci_has_next_support().await?;
129
130 let minimal = ResolvedVc::upcast(
131 ServerNftJsonAsset::new(project, ServerNftType::Minimal)
132 .to_resolved()
133 .await?,
134 );
135
136 if has_next_support && !is_standalone {
137 Ok(Vc::cell(vec![minimal]))
139 } else {
140 Ok(Vc::cell(vec![
141 minimal,
142 ResolvedVc::upcast(
143 ServerNftJsonAsset::new(project, ServerNftType::Full)
144 .to_resolved()
145 .await?,
146 ),
147 ]))
148 }
149}
150
151#[turbo_tasks::value]
152pub struct ServerNftJsonAsset {
153 project: ResolvedVc<Project>,
154 ty: ServerNftType,
155}
156
157#[turbo_tasks::value_impl]
158impl ServerNftJsonAsset {
159 #[turbo_tasks::function]
160 pub fn new(project: ResolvedVc<Project>, ty: ServerNftType) -> Vc<Self> {
161 ServerNftJsonAsset { project, ty }.cell()
162 }
163}
164
165#[turbo_tasks::value_impl]
166impl OutputAssetsReference for ServerNftJsonAsset {}
167
168#[turbo_tasks::value_impl]
169impl OutputAsset for ServerNftJsonAsset {
170 #[turbo_tasks::function]
171 async fn path(&self) -> Result<Vc<FileSystemPath>> {
172 let name = match self.ty {
173 ServerNftType::Minimal => "next-minimal-server.js.nft.json",
174 ServerNftType::Full => "next-server.js.nft.json",
175 };
176
177 Ok(self.project.node_root().await?.join(name)?.cell())
178 }
179}
180
181#[turbo_tasks::value_impl]
182impl Asset for ServerNftJsonAsset {
183 #[turbo_tasks::function]
184 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
185 let this = self.await?;
186
187 let nft_path = self.path().owned().await?;
189 let mut nft_json = NftJsonBuilder::new(this.project, &nft_path).await?;
190
191 let module_graph = ModuleGraph::from_graphs(
192 vec![SingleModuleGraph::new_with_entries(
193 GraphEntries::new(vec![], self.entries().owned().await?).resolved_cell(),
194 true,
195 false,
196 )],
197 None,
198 )
199 .connect();
200
201 let hash_salt = this.project.next_config().output_hash_salt();
202
203 let server_output_assets = traced_modules_for_entries(
204 module_graph,
205 Modules::empty(),
206 self.entries(),
207 Some(self.ignores()),
208 None,
209 )
210 .await?
211 .iter()
212 .map(async |m| {
213 let path = m.ident().await?.path.clone();
214 let source = m.source().await?.context("NFT module has no content")?;
215 let content = source.content();
216 Ok((
217 path,
218 content
219 .hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
220 .owned()
221 .await?,
222 content.await?,
223 ))
224 })
225 .try_join()
226 .await?;
227
228 for (path, hash, content) in server_output_assets {
229 nft_json.add(path, hash, &content)?;
230 }
231
232 let next_dir = get_next_package(this.project.project_path().owned().await?).await?;
233 for ty in ["app-page", "pages"] {
234 let dir = next_dir.join(&format!("dist/server/route-modules/{ty}"))?;
235 let module_path = dir.join("module.compiled.js")?;
236 let content = FileSource::new(module_path.clone()).content();
237 let hash = content
238 .hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
239 .owned()
240 .await?;
241 let content = content.await?;
242 nft_json.add(module_path, hash, &content)?;
243
244 let contexts_dir = dir.join("vendored/contexts")?;
245 let DirectoryContent::Entries(contexts_files) = &*contexts_dir.read_dir().await? else {
246 bail!(
247 "Expected contexts directory to be a directory, found: {:?}",
248 contexts_dir
249 );
250 };
251 for (_, entry) in contexts_files {
252 let DirectoryEntry::File(file) = entry else {
253 continue;
254 };
255 if file.extension() == Some("js") {
256 let content = FileSource::new(file.clone()).content();
257 let hash = content
258 .hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
259 .owned()
260 .await?;
261 let content = content.await?;
262 nft_json.add(file.clone(), hash, &content)?;
263 }
264 }
265 }
266
267 let json = serde_json::to_string(&nft_json.into_json(None))?;
268
269 Ok(AssetContent::file(
270 FileContent::Content(File::from(json)).cell(),
271 ))
272 }
273}
274
275fn next_owned_ignores(
278 ty: &ServerNftType,
279 has_next_support: bool,
280 is_standalone: bool,
281) -> Vec<RcStr> {
282 let mut globs = vec![
283 rcstr!("**/node_modules/react{,-dom,-server-dom-turbopack}/**/*.development.js"),
284 rcstr!("**/*.d.ts"),
285 rcstr!("**/*.map"),
286 rcstr!("**/next/dist/pages/**/*"),
287 rcstr!("**/next/dist/compiled/next-server/**/*.dev.js"),
288 rcstr!("**/next/dist/compiled/webpack/*"),
289 rcstr!("**/node_modules/webpack5/**/*"),
290 rcstr!("**/next/dist/server/lib/route-resolver*"),
291 rcstr!("**/next/dist/lib/upgrade/**/*"),
293 rcstr!("**/next/dist/compiled/@mswjs/interceptors/ClientRequest/LICENSE"),
299 rcstr!("**/next/dist/compiled/@mswjs/interceptors/ClientRequest/llhttp/**"),
300 rcstr!("**/next/dist/compiled/semver/semver/**/*.js"),
301 rcstr!("**/next/dist/compiled/jest-worker/**/*"),
302 rcstr!("**/next/dist/next-devtools/userspace/use-app-dev-rendering-indicator.js"),
305 rcstr!("**/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js"),
308 rcstr!("**/next/dist/server/lib/router-utils/setup-dev-bundler.js"),
310 rcstr!("**/next/dist/server/dev/next-dev-server.js"),
312 rcstr!("**/next/dist/compiled/browserslist/**"),
315 ];
316
317 if has_next_support {
320 globs.extend([
321 rcstr!("**/node_modules/sharp/**/*"),
322 rcstr!("**/@img/sharp-libvips*/**/*"),
323 rcstr!("**/next/dist/server/image-optimizer.js"),
324 ]);
325 }
326
327 if !is_standalone {
328 globs.extend([
329 rcstr!("**/*/next/dist/server/next.js"),
330 rcstr!("**/*/next/dist/bin/next"),
331 ]);
332 }
333
334 if matches!(ty, ServerNftType::Minimal) {
335 globs.extend([
336 rcstr!("**/next/dist/compiled/edge-runtime/**/*"),
337 rcstr!("**/next/dist/server/web/sandbox/**/*"),
338 rcstr!("**/next/dist/server/post-process.js"),
339 ]);
340 }
341
342 globs
343}
344
345#[turbo_tasks::value_impl]
346impl ServerNftJsonAsset {
347 #[turbo_tasks::function]
348 async fn entries(&self) -> Result<Vc<Modules>> {
349 let is_standalone = *self.project.next_config().is_standalone().await?;
350
351 let prune = Glob::alternatives(
352 next_owned_ignores(
353 &self.ty,
354 *self.project.ci_has_next_support().await?,
355 is_standalone,
356 )
357 .into_iter()
358 .map(|g| Glob::new(g, GlobOptions::default()))
359 .collect(),
360 )
361 .to_resolved()
362 .await?;
363
364 let asset_context = Vc::upcast(externals_tracing_module_context(
365 get_tracing_compile_time_info(),
366 false,
367 Some((self.project.project_root_path().owned().await?, prune)),
368 true,
369 ));
370
371 let project_path = self.project.project_path().owned().await?;
372
373 let next_resolve_origin = Vc::upcast(PlainResolveOrigin::new(
374 asset_context,
375 get_next_package(project_path.clone()).await?.join("_")?,
376 ));
377
378 let entries = match self.ty {
379 ServerNftType::Full => Either::Left(
380 if is_standalone {
381 Either::Left(
382 [
383 "next/dist/server/lib/start-server",
384 "next/dist/server/next",
385 "next/dist/server/require-hook",
386 ]
387 .into_iter(),
388 )
389 } else {
390 Either::Right(std::iter::empty())
391 }
392 .chain(std::iter::once("next/dist/server/next-server")),
393 ),
394 ServerNftType::Minimal => Either::Right(std::iter::once(
395 "next/dist/compiled/next-server/server.runtime.prod",
396 )),
397 };
398
399 let hook_modules = require_hook_modules(project_path, asset_context)
403 .owned()
404 .await?;
405
406 Ok(Vc::cell(
407 hook_modules
408 .into_iter()
409 .chain(
410 entries
411 .map(async |path| {
412 Ok(cjs_resolve(
413 next_resolve_origin,
414 Request::parse_string(path.into()),
415 CommonJsReferenceSubType::Undefined,
416 None,
417 ResolveErrorMode::Error,
418 )
419 .await?
420 .primary_modules()
421 .await?
422 .into_iter())
423 })
424 .try_flat_join()
425 .await?,
426 )
427 .collect(),
428 ))
429 }
430
431 #[turbo_tasks::function]
432 async fn ignores(&self) -> Result<Vc<Glob>> {
433 let is_standalone = *self.project.next_config().is_standalone().await?;
434 let has_next_support = *self.project.ci_has_next_support().await?;
435 let project_path = self.project.project_path().owned().await?;
436
437 let output_file_tracing_excludes = self
438 .project
439 .next_config()
440 .output_file_tracing_excludes(project_path)
441 .await?;
442 let mut additional_ignores = BTreeSet::new();
443
444 for (route_glob, exclude_patterns) in output_file_tracing_excludes.iter() {
445 if route_glob.await?.matches("next-server") {
447 for (glob, root) in exclude_patterns {
448 additional_ignores.insert(if root.path.is_empty() {
449 glob.clone()
450 } else {
451 format!("{root}/{glob}").into()
452 });
453 }
454 }
455 }
456
457 let server_ignores_glob = next_owned_ignores(&self.ty, has_next_support, is_standalone)
463 .into_iter()
464 .chain(additional_ignores)
465 .map(|g| Glob::new(g, Default::default()))
466 .collect::<Vec<_>>();
467
468 Ok(Glob::alternatives(server_ignores_glob))
469 }
470}