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::{Value, json};
8use turbo_rcstr::RcStr;
9use turbo_tasks::{
10 NonLocalValue, ResolvedVc, TaskInput, TryFlatJoinIterExt, TryJoinIterExt, Vc,
11 trace::TraceRawVcs,
12};
13use turbo_tasks_fs::{
14 DirectoryContent, DirectoryEntry, File, FileContent, FileSystemPath, glob::Glob,
15};
16use turbo_tasks_hash::HashAlgorithm;
17use turbopack::externals_tracing_module_context;
18use turbopack_core::{
19 asset::{Asset, AssetContent},
20 context::AssetContext,
21 file_source::FileSource,
22 module::{Module, Modules},
23 module_graph::{ModuleGraph, SingleModuleGraph, chunk_group_info::ChunkGroupEntry},
24 output::{OutputAsset, OutputAssets, OutputAssetsReference},
25 reference_type::{CommonJsReferenceSubType, ReferenceType},
26 resolve::{ResolveErrorMode, origin::PlainResolveOrigin, parse::Request},
27};
28use turbopack_resolve::ecmascript::cjs_resolve;
29
30use crate::{
31 nft_json::{relativize_glob, traced_modules_for_entries},
32 project::Project,
33};
34
35#[derive(
36 PartialEq, Eq, TraceRawVcs, NonLocalValue, Debug, Clone, Hash, TaskInput, Encode, Decode,
37)]
38enum ServerNftType {
39 Minimal,
40 Full,
41}
42
43#[turbo_tasks::function]
44pub async fn next_server_nft_assets(project: Vc<Project>) -> Result<Vc<OutputAssets>> {
45 if *project.next_config().is_using_adapter().await? {
46 return Ok(Vc::cell(vec![]));
49 }
50
51 let has_next_support = *project.ci_has_next_support().await?;
52 let is_standalone = *project.next_config().is_standalone().await?;
53
54 let minimal = ResolvedVc::upcast(
55 ServerNftJsonAsset::new(project, ServerNftType::Minimal)
56 .to_resolved()
57 .await?,
58 );
59
60 if has_next_support && !is_standalone {
61 Ok(Vc::cell(vec![minimal]))
63 } else {
64 Ok(Vc::cell(vec![
65 minimal,
66 ResolvedVc::upcast(
67 ServerNftJsonAsset::new(project, ServerNftType::Full)
68 .to_resolved()
69 .await?,
70 ),
71 ]))
72 }
73}
74
75#[turbo_tasks::value]
76pub struct ServerNftJsonAsset {
77 project: ResolvedVc<Project>,
78 ty: ServerNftType,
79}
80
81#[turbo_tasks::value_impl]
82impl ServerNftJsonAsset {
83 #[turbo_tasks::function]
84 pub fn new(project: ResolvedVc<Project>, ty: ServerNftType) -> Vc<Self> {
85 ServerNftJsonAsset { project, ty }.cell()
86 }
87}
88
89#[turbo_tasks::value_impl]
90impl OutputAssetsReference for ServerNftJsonAsset {}
91
92#[turbo_tasks::value_impl]
93impl OutputAsset for ServerNftJsonAsset {
94 #[turbo_tasks::function]
95 async fn path(&self) -> Result<Vc<FileSystemPath>> {
96 let name = match self.ty {
97 ServerNftType::Minimal => "next-minimal-server.js.nft.json",
98 ServerNftType::Full => "next-server.js.nft.json",
99 };
100
101 Ok(self.project.node_root().await?.join(name)?.cell())
102 }
103}
104
105#[turbo_tasks::value_impl]
106impl Asset for ServerNftJsonAsset {
107 #[turbo_tasks::function]
108 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
109 let this = self.await?;
110
111 let base_dir = this
113 .project
114 .project_root_path()
115 .await?
116 .join(&this.project.node_root().await?.path)?;
117
118 let module_graph = ModuleGraph::from_graphs(
119 vec![SingleModuleGraph::new_with_traced_entries(
120 ResolvedVc::cell(vec![ChunkGroupEntry::Entry(self.entries().owned().await?)]),
121 true,
122 false,
123 )],
124 None,
125 )
126 .connect();
127
128 let mut server_output_assets =
129 traced_modules_for_entries(module_graph, self.entries(), Some(self.ignores()), true)
130 .await?
131 .iter()
132 .map(async |m| {
133 Ok((
134 base_dir
135 .get_relative_path_to(&m.ident().await?.path)
136 .context("failed to compute relative path for server NFT JSON")?,
137 m.source()
138 .await?
139 .context("NFT module has no content")?
140 .content()
141 .hash(HashAlgorithm::Xxh3Hash128Hex)
142 .await?,
143 ))
144 })
145 .try_join()
146 .await?;
147
148 let next_dir = get_next_package(this.project.project_path().owned().await?).await?;
149 for ty in ["app-page", "pages"] {
150 let dir = next_dir.join(&format!("dist/server/route-modules/{ty}"))?;
151 let module_path = dir.join("module.compiled.js")?;
152 server_output_assets.push((
153 base_dir
154 .get_relative_path_to(&module_path)
155 .context("failed to compute relative path for server NFT JSON")?,
156 module_path
157 .read()
158 .hash(HashAlgorithm::Xxh3Hash128Hex)
159 .await?,
160 ));
161
162 let contexts_dir = dir.join("vendored/contexts")?;
163 let DirectoryContent::Entries(contexts_files) = &*contexts_dir.read_dir().await? else {
164 bail!(
165 "Expected contexts directory to be a directory, found: {:?}",
166 contexts_dir
167 );
168 };
169 for (_, entry) in contexts_files {
170 let DirectoryEntry::File(file) = entry else {
171 continue;
172 };
173 if file.extension() == Some("js") {
174 server_output_assets.push((
175 base_dir
176 .get_relative_path_to(file)
177 .context("failed to compute relative path for server NFT JSON")?,
178 file.read().hash(HashAlgorithm::Xxh3Hash128Hex).await?,
179 ))
180 }
181 }
182 }
183
184 server_output_assets.sort_unstable();
185 server_output_assets.dedup();
189
190 let (files, file_hashes): (Vec<_>, Vec<_>) = server_output_assets.into_iter().unzip();
191 let json = json!({
192 "version": 1,
193 "files": files,
194 "fileHashes": file_hashes
195 });
196
197 Ok(AssetContent::file(
198 FileContent::Content(File::from(json.to_string())).cell(),
199 ))
200 }
201}
202
203#[turbo_tasks::value_impl]
204impl ServerNftJsonAsset {
205 #[turbo_tasks::function]
206 async fn entries(&self) -> Result<Vc<Modules>> {
207 let is_standalone = *self.project.next_config().is_standalone().await?;
208
209 let asset_context = Vc::upcast(externals_tracing_module_context(
210 get_tracing_compile_time_info(),
211 false,
212 ));
213
214 let project_path = self.project.project_path().owned().await?;
215
216 let next_resolve_origin = Vc::upcast(PlainResolveOrigin::new(
217 asset_context,
218 get_next_package(project_path.clone()).await?.join("_")?,
219 ));
220
221 let cache_handler = self
222 .project
223 .next_config()
224 .cache_handler(project_path.clone())
225 .await?;
226 let cache_handlers = self
227 .project
228 .next_config()
229 .cache_handlers(project_path.clone())
230 .await?;
231
232 let shared_entries = ["styled-jsx", "styled-jsx/style", "styled-jsx/style.js"];
234
235 let cache_handler_entries = cache_handler
236 .iter()
237 .chain(cache_handlers.iter())
238 .map(|f| {
239 asset_context
240 .process(
241 Vc::upcast(FileSource::new(f.clone())),
242 ReferenceType::CommonJs(CommonJsReferenceSubType::Undefined),
243 )
244 .module()
245 })
246 .map(|m| m.to_resolved())
247 .try_join()
248 .await?;
249
250 let entries = match self.ty {
251 ServerNftType::Full => Either::Left(
252 if is_standalone {
253 Either::Left(
254 [
255 "next/dist/server/lib/start-server",
256 "next/dist/server/next",
257 "next/dist/server/require-hook",
258 ]
259 .into_iter(),
260 )
261 } else {
262 Either::Right(std::iter::empty())
263 }
264 .chain(std::iter::once("next/dist/server/next-server")),
265 ),
266 ServerNftType::Minimal => Either::Right(std::iter::once(
267 "next/dist/compiled/next-server/server.runtime.prod",
268 )),
269 };
270
271 Ok(Vc::cell(
272 cache_handler_entries
273 .into_iter()
274 .chain(
275 shared_entries
276 .into_iter()
277 .chain(entries)
278 .map(async |path| {
279 Ok(cjs_resolve(
280 next_resolve_origin,
281 Request::parse_string(path.into()),
282 CommonJsReferenceSubType::Undefined,
283 None,
284 ResolveErrorMode::Error,
285 )
286 .await?
287 .primary_modules()
288 .await?
289 .into_iter())
290 })
291 .try_flat_join()
292 .await?,
293 )
294 .collect(),
295 ))
296 }
297
298 #[turbo_tasks::function]
299 async fn ignores(&self) -> Result<Vc<Glob>> {
300 let is_standalone = *self.project.next_config().is_standalone().await?;
301 let has_next_support = *self.project.ci_has_next_support().await?;
302 let project_path = self.project.project_path().owned().await?;
303
304 let output_file_tracing_excludes = self
305 .project
306 .next_config()
307 .output_file_tracing_excludes()
308 .await?;
309 let mut additional_ignores = BTreeSet::new();
310 if let Some(output_file_tracing_excludes) = output_file_tracing_excludes
311 .as_ref()
312 .and_then(Value::as_object)
313 {
314 for (glob_pattern, exclude_patterns) in output_file_tracing_excludes {
315 let glob = Glob::new(RcStr::from(glob_pattern.clone()), Default::default()).await?;
317 if glob.matches("next-server")
318 && let Some(patterns) = exclude_patterns.as_array()
319 {
320 for pattern in patterns {
321 if let Some(pattern_str) = pattern.as_str() {
322 let (glob, root) = relativize_glob(pattern_str, project_path.clone())?;
323 let glob = if root.path.is_empty() {
324 glob.to_string()
325 } else {
326 format!("{root}/{glob}")
327 };
328 additional_ignores.insert(glob);
329 }
330 }
331 }
332 }
333 }
334
335 let server_ignores_glob = [
336 "**/node_modules/react{,-dom,-server-dom-turbopack}/**/*.development.js",
337 "**/*.d.ts",
338 "**/*.map",
339 "**/next/dist/pages/**/*",
340 "**/next/dist/compiled/next-server/**/*.dev.js",
341 "**/next/dist/compiled/webpack/*",
342 "**/node_modules/webpack5/**/*",
343 "**/next/dist/server/lib/route-resolver*",
344 "**/next/dist/compiled/semver/semver/**/*.js",
345 "**/next/dist/compiled/jest-worker/**/*",
346 "**/next/dist/next-devtools/userspace/use-app-dev-rendering-indicator.js",
349 "**/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js",
352 "**/next/dist/server/lib/router-utils/setup-dev-bundler.js",
354 "**/next/dist/server/dev/next-dev-server.js",
356 "**/next/dist/compiled/browserslist/**",
359 ]
360 .into_iter()
361 .chain(additional_ignores.iter().map(|s| s.as_str()))
362 .chain(if has_next_support {
365 Either::Left(
366 [
367 "**/node_modules/sharp/**/*",
368 "**/@img/sharp-libvips*/**/*",
369 "**/next/dist/server/image-optimizer.js",
370 ]
371 .into_iter(),
372 )
373 } else {
374 Either::Right(std::iter::empty())
375 })
376 .chain(if is_standalone {
377 Either::Left(std::iter::empty())
378 } else {
379 Either::Right(["**/*/next/dist/server/next.js", "**/*/next/dist/bin/next"].into_iter())
380 })
381 .map(|g| Glob::new(g.into(), Default::default()))
382 .collect::<Vec<_>>();
383
384 Ok(match self.ty {
385 ServerNftType::Full => Glob::alternatives(server_ignores_glob),
386 ServerNftType::Minimal => Glob::alternatives(
387 server_ignores_glob
388 .into_iter()
389 .chain(
390 [
391 "**/next/dist/compiled/edge-runtime/**/*",
392 "**/next/dist/server/web/sandbox/**/*",
393 "**/next/dist/server/post-process.js",
394 ]
395 .into_iter()
396 .map(|g| Glob::new(g.into(), Default::default())),
397 )
398 .collect(),
399 ),
400 })
401 }
402}