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_tasks::{ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc, trace::TraceRawVcs};
9use turbo_tasks_fs::{
10 DirectoryContent, DirectoryEntry, File, FileContent, FileSystemPath, glob::Glob,
11};
12use turbo_tasks_hash::HashAlgorithm;
13use turbopack::externals_tracing_module_context;
14use turbopack_core::{
15 asset::{Asset, AssetContent},
16 module::{Module, Modules},
17 module_graph::{GraphEntries, ModuleGraph, SingleModuleGraph},
18 output::{OutputAsset, OutputAssets, OutputAssetsReference},
19 reference_type::CommonJsReferenceSubType,
20 resolve::{ResolveErrorMode, origin::PlainResolveOrigin, parse::Request},
21};
22use turbopack_resolve::ecmascript::cjs_resolve;
23
24use crate::{nft::traced_modules_for_entries, project::Project};
25
26#[turbo_tasks::task_input]
27#[derive(PartialEq, Eq, TraceRawVcs, Debug, Clone, Hash, Encode, Decode)]
28enum ServerNftType {
29 Minimal,
30 Full,
31}
32
33#[turbo_tasks::function]
34pub async fn next_server_nft_assets(project: Vc<Project>) -> Result<Vc<OutputAssets>> {
35 if *project.next_config().is_using_adapter().await? {
36 return Ok(Vc::cell(vec![]));
39 }
40
41 let has_next_support = *project.ci_has_next_support().await?;
42 let is_standalone = *project.next_config().is_standalone().await?;
43
44 let minimal = ResolvedVc::upcast(
45 ServerNftJsonAsset::new(project, ServerNftType::Minimal)
46 .to_resolved()
47 .await?,
48 );
49
50 if has_next_support && !is_standalone {
51 Ok(Vc::cell(vec![minimal]))
53 } else {
54 Ok(Vc::cell(vec![
55 minimal,
56 ResolvedVc::upcast(
57 ServerNftJsonAsset::new(project, ServerNftType::Full)
58 .to_resolved()
59 .await?,
60 ),
61 ]))
62 }
63}
64
65#[turbo_tasks::value]
66pub struct ServerNftJsonAsset {
67 project: ResolvedVc<Project>,
68 ty: ServerNftType,
69}
70
71#[turbo_tasks::value_impl]
72impl ServerNftJsonAsset {
73 #[turbo_tasks::function]
74 pub fn new(project: ResolvedVc<Project>, ty: ServerNftType) -> Vc<Self> {
75 ServerNftJsonAsset { project, ty }.cell()
76 }
77}
78
79#[turbo_tasks::value_impl]
80impl OutputAssetsReference for ServerNftJsonAsset {}
81
82#[turbo_tasks::value_impl]
83impl OutputAsset for ServerNftJsonAsset {
84 #[turbo_tasks::function]
85 async fn path(&self) -> Result<Vc<FileSystemPath>> {
86 let name = match self.ty {
87 ServerNftType::Minimal => "next-minimal-server.js.nft.json",
88 ServerNftType::Full => "next-server.js.nft.json",
89 };
90
91 Ok(self.project.node_root().await?.join(name)?.cell())
92 }
93}
94
95#[turbo_tasks::value_impl]
96impl Asset for ServerNftJsonAsset {
97 #[turbo_tasks::function]
98 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
99 let this = self.await?;
100
101 let base_dir = this
103 .project
104 .project_root_path()
105 .await?
106 .join(&this.project.node_root().await?.path)?;
107
108 let module_graph = ModuleGraph::from_graphs(
109 vec![SingleModuleGraph::new_with_entries(
110 GraphEntries::new(vec![], self.entries().owned().await?).resolved_cell(),
111 true,
112 false,
113 )],
114 None,
115 )
116 .connect();
117
118 let hash_salt = this.project.next_config().output_hash_salt();
119
120 let mut server_output_assets = traced_modules_for_entries(
121 module_graph,
122 Modules::empty(),
123 self.entries(),
124 Some(self.ignores()),
125 None,
126 hash_salt,
127 )
128 .await?
129 .iter()
130 .map(async |m| {
131 Ok((
132 base_dir
133 .get_relative_path_to(&m.ident().await?.path)
134 .context("failed to compute relative path for server NFT JSON")?,
135 m.source()
136 .await?
137 .context("NFT module has no content")?
138 .content()
139 .hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
140 .await?,
141 ))
142 })
143 .try_join()
144 .await?;
145
146 let next_dir = get_next_package(this.project.project_path().owned().await?).await?;
147 for ty in ["app-page", "pages"] {
148 let dir = next_dir.join(&format!("dist/server/route-modules/{ty}"))?;
149 let module_path = dir.join("module.compiled.js")?;
150 server_output_assets.push((
151 base_dir
152 .get_relative_path_to(&module_path)
153 .context("failed to compute relative path for server NFT JSON")?,
154 module_path
155 .read()
156 .hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
157 .await?,
158 ));
159
160 let contexts_dir = dir.join("vendored/contexts")?;
161 let DirectoryContent::Entries(contexts_files) = &*contexts_dir.read_dir().await? else {
162 bail!(
163 "Expected contexts directory to be a directory, found: {:?}",
164 contexts_dir
165 );
166 };
167 for (_, entry) in contexts_files {
168 let DirectoryEntry::File(file) = entry else {
169 continue;
170 };
171 if file.extension() == Some("js") {
172 server_output_assets.push((
173 base_dir
174 .get_relative_path_to(file)
175 .context("failed to compute relative path for server NFT JSON")?,
176 file.read()
177 .hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
178 .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 shared_entries = ["styled-jsx", "styled-jsx/style", "styled-jsx/style.js"];
223
224 let entries = match self.ty {
225 ServerNftType::Full => Either::Left(
226 if is_standalone {
227 Either::Left(
228 [
229 "next/dist/server/lib/start-server",
230 "next/dist/server/next",
231 "next/dist/server/require-hook",
232 ]
233 .into_iter(),
234 )
235 } else {
236 Either::Right(std::iter::empty())
237 }
238 .chain(std::iter::once("next/dist/server/next-server")),
239 ),
240 ServerNftType::Minimal => Either::Right(std::iter::once(
241 "next/dist/compiled/next-server/server.runtime.prod",
242 )),
243 };
244
245 Ok(Vc::cell(
246 shared_entries
247 .into_iter()
248 .chain(entries)
249 .map(async |path| {
250 Ok(cjs_resolve(
251 next_resolve_origin,
252 Request::parse_string(path.into()),
253 CommonJsReferenceSubType::Undefined,
254 None,
255 ResolveErrorMode::Error,
256 )
257 .await?
258 .primary_modules()
259 .await?
260 .into_iter())
261 })
262 .try_flat_join()
263 .await?,
264 ))
265 }
266
267 #[turbo_tasks::function]
268 async fn ignores(&self) -> Result<Vc<Glob>> {
269 let is_standalone = *self.project.next_config().is_standalone().await?;
270 let has_next_support = *self.project.ci_has_next_support().await?;
271 let project_path = self.project.project_path().owned().await?;
272
273 let output_file_tracing_excludes = self
274 .project
275 .next_config()
276 .output_file_tracing_excludes(project_path)
277 .await?;
278 let mut additional_ignores = BTreeSet::new();
279
280 for (route_glob, exclude_patterns) in output_file_tracing_excludes.iter() {
281 if route_glob.await?.matches("next-server") {
283 for (glob, root) in exclude_patterns {
284 additional_ignores.insert(if root.path.is_empty() {
285 glob.to_string()
286 } else {
287 format!("{root}/{glob}")
288 });
289 }
290 }
291 }
292
293 let server_ignores_glob = [
294 "**/node_modules/react{,-dom,-server-dom-turbopack}/**/*.development.js",
295 "**/*.d.ts",
296 "**/*.map",
297 "**/next/dist/pages/**/*",
298 "**/next/dist/compiled/next-server/**/*.dev.js",
299 "**/next/dist/compiled/webpack/*",
300 "**/node_modules/webpack5/**/*",
301 "**/next/dist/server/lib/route-resolver*",
302 "**/next/dist/compiled/semver/semver/**/*.js",
303 "**/next/dist/compiled/jest-worker/**/*",
304 "**/next/dist/next-devtools/userspace/use-app-dev-rendering-indicator.js",
307 "**/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js",
310 "**/next/dist/server/lib/router-utils/setup-dev-bundler.js",
312 "**/next/dist/server/dev/next-dev-server.js",
314 "**/next/dist/compiled/browserslist/**",
317 ]
318 .into_iter()
319 .chain(additional_ignores.iter().map(|s| s.as_str()))
320 .chain(if has_next_support {
323 Either::Left(
324 [
325 "**/node_modules/sharp/**/*",
326 "**/@img/sharp-libvips*/**/*",
327 "**/next/dist/server/image-optimizer.js",
328 ]
329 .into_iter(),
330 )
331 } else {
332 Either::Right(std::iter::empty())
333 })
334 .chain(if is_standalone {
335 Either::Left(std::iter::empty())
336 } else {
337 Either::Right(["**/*/next/dist/server/next.js", "**/*/next/dist/bin/next"].into_iter())
338 })
339 .map(|g| Glob::new(g.into(), Default::default()))
340 .collect::<Vec<_>>();
341
342 Ok(match self.ty {
343 ServerNftType::Full => Glob::alternatives(server_ignores_glob),
344 ServerNftType::Minimal => Glob::alternatives(
345 server_ignores_glob
346 .into_iter()
347 .chain(
348 [
349 "**/next/dist/compiled/edge-runtime/**/*",
350 "**/next/dist/server/web/sandbox/**/*",
351 "**/next/dist/server/post-process.js",
352 ]
353 .into_iter()
354 .map(|g| Glob::new(g.into(), Default::default())),
355 )
356 .collect(),
357 ),
358 })
359 }
360}