1use std::collections::BTreeSet;
2
3use anyhow::{Context, Result, bail};
4use either::Either;
5use next_core::{get_next_package, next_server::get_tracing_compile_time_info};
6use serde::{Deserialize, Serialize};
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::{DirectoryContent, DirectoryEntry, File, FileSystemPath, glob::Glob};
14use turbopack::externals_tracing_module_context;
15use turbopack_core::{
16 asset::{Asset, AssetContent},
17 context::AssetContext,
18 file_source::FileSource,
19 output::{OutputAsset, OutputAssets},
20 reference_type::{CommonJsReferenceSubType, ReferenceType},
21 resolve::{origin::PlainResolveOrigin, parse::Request},
22 traced_asset::TracedAsset,
23};
24use turbopack_ecmascript::resolve::cjs_resolve;
25
26use 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)]
34enum ServerNftType {
35 Minimal,
36 Full,
37}
38
39#[turbo_tasks::function]
40pub async fn next_server_nft_assets(project: Vc<Project>) -> Result<Vc<OutputAssets>> {
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 OutputAsset for ServerNftJsonAsset {
81 #[turbo_tasks::function]
82 async fn path(&self) -> Result<Vc<FileSystemPath>> {
83 let name = match self.ty {
84 ServerNftType::Minimal => "next-minimal-server.js.nft.json",
85 ServerNftType::Full => "next-server.js.nft.json",
86 };
87
88 Ok(self.project.node_root().await?.join(name)?.cell())
89 }
90}
91
92#[turbo_tasks::value_impl]
93impl Asset for ServerNftJsonAsset {
94 #[turbo_tasks::function]
95 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
96 let this = self.await?;
97 let base_dir = this
99 .project
100 .project_root_path()
101 .await?
102 .join(&this.project.node_root().await?.path)?;
103
104 let mut server_output_assets =
105 all_assets_from_entries_filtered(self.entries(), None, Some(self.ignores()))
106 .await?
107 .iter()
108 .map(async |m| {
109 base_dir
110 .get_relative_path_to(&*m.path().await?)
111 .context("failed to compute relative path for server NFT JSON")
112 })
113 .try_join()
114 .await?;
115
116 server_output_assets.push("./package.json".into());
118
119 let next_dir = get_next_package(this.project.project_path().owned().await?).await?;
120 for ty in ["app-page", "pages"] {
121 let dir = next_dir.join(&format!("dist/server/route-modules/{ty}"))?;
122 let module_path = dir.join("module.compiled.js")?;
123 server_output_assets.push(
124 base_dir
125 .get_relative_path_to(&module_path)
126 .context("failed to compute relative path for server NFT JSON")?,
127 );
128
129 let contexts_dir = dir.join("vendored/contexts")?;
130 let DirectoryContent::Entries(contexts_files) = &*contexts_dir.read_dir().await? else {
131 bail!(
132 "Expected contexts directory to be a directory, found: {:?}",
133 contexts_dir
134 );
135 };
136 for (_, entry) in contexts_files {
137 let DirectoryEntry::File(file) = entry else {
138 continue;
139 };
140 if file.extension() == "js" {
141 server_output_assets.push(
142 base_dir
143 .get_relative_path_to(file)
144 .context("failed to compute relative path for server NFT JSON")?,
145 )
146 }
147 }
148 }
149
150 server_output_assets.sort();
151 server_output_assets.dedup();
155
156 let json = json!({
157 "version": 1,
158 "files": server_output_assets
159 });
160
161 Ok(AssetContent::file(File::from(json.to_string()).into()))
162 }
163}
164
165#[turbo_tasks::value_impl]
166impl ServerNftJsonAsset {
167 #[turbo_tasks::function]
168 async fn entries(&self) -> Result<Vc<OutputAssets>> {
169 let is_standalone = *self.project.next_config().is_standalone().await?;
170
171 let asset_context = Vc::upcast(externals_tracing_module_context(
172 get_tracing_compile_time_info(),
173 ));
174
175 let project_path = self.project.project_path().owned().await?;
176
177 let next_resolve_origin = Vc::upcast(PlainResolveOrigin::new(
178 asset_context,
179 get_next_package(project_path.clone()).await?.join("_")?,
180 ));
181
182 let cache_handler = self
183 .project
184 .next_config()
185 .cache_handler(project_path.clone())
186 .await?;
187 let cache_handlers = self
188 .project
189 .next_config()
190 .experimental_cache_handlers(project_path.clone())
191 .await?;
192
193 let shared_entries = ["styled-jsx", "styled-jsx/style", "styled-jsx/style.js"];
195
196 let cache_handler_entries = cache_handler
197 .into_iter()
198 .chain(cache_handlers.into_iter())
199 .map(|f| {
200 asset_context
201 .process(
202 Vc::upcast(FileSource::new(f.clone())),
203 ReferenceType::CommonJs(CommonJsReferenceSubType::Undefined),
204 )
205 .module()
206 });
207
208 let entries = match self.ty {
209 ServerNftType::Full => Either::Left(
210 if is_standalone {
211 Either::Left(
212 [
213 "next/dist/server/lib/start-server",
214 "next/dist/server/next",
215 "next/dist/server/require-hook",
216 ]
217 .into_iter(),
218 )
219 } else {
220 Either::Right(std::iter::empty())
221 }
222 .chain(std::iter::once("next/dist/server/next-server")),
223 ),
224 ServerNftType::Minimal => Either::Right(std::iter::once(
225 "next/dist/compiled/next-server/server.runtime.prod",
226 )),
227 };
228
229 Ok(Vc::cell(
230 cache_handler_entries
231 .chain(
232 shared_entries
233 .into_iter()
234 .chain(entries)
235 .map(async |path| {
236 Ok(cjs_resolve(
237 next_resolve_origin,
238 Request::parse_string(path.into()),
239 CommonJsReferenceSubType::Undefined,
240 None,
241 false,
242 )
243 .primary_modules()
244 .await?
245 .into_iter()
246 .map(|m| **m))
247 })
248 .try_flat_join()
249 .await?,
250 )
251 .map(|m| Vc::upcast::<Box<dyn OutputAsset>>(TracedAsset::new(m)).to_resolved())
252 .try_join()
253 .await?,
254 ))
255 }
256
257 #[turbo_tasks::function]
258 async fn ignores(&self) -> Result<Vc<Glob>> {
259 let is_standalone = *self.project.next_config().is_standalone().await?;
260 let has_next_support = *self.project.ci_has_next_support().await?;
261 let project_path = self.project.project_path().owned().await?;
262
263 let output_file_tracing_excludes = self
264 .project
265 .next_config()
266 .output_file_tracing_excludes()
267 .await?;
268 let mut additional_ignores = BTreeSet::new();
269 if let Some(output_file_tracing_excludes) = output_file_tracing_excludes
270 .as_ref()
271 .and_then(Value::as_object)
272 {
273 for (glob_pattern, exclude_patterns) in output_file_tracing_excludes {
274 let glob = Glob::new(RcStr::from(glob_pattern.clone()), Default::default()).await?;
276 if glob.matches("next-server")
277 && let Some(patterns) = exclude_patterns.as_array()
278 {
279 for pattern in patterns {
280 if let Some(pattern_str) = pattern.as_str() {
281 let (glob, root) = relativize_glob(pattern_str, project_path.clone())?;
282 let glob = if root.path.is_empty() {
283 glob.to_string()
284 } else {
285 format!("{root}/{glob}")
286 };
287 additional_ignores.insert(glob);
288 }
289 }
290 }
291 }
292 }
293
294 let server_ignores_glob = [
295 "**/node_modules/react{,-dom,-server-dom-turbopack}/**/*.development.js",
296 "**/*.d.ts",
297 "**/*.map",
298 "**/next/dist/pages/**/*",
299 "**/next/dist/compiled/next-server/**/*.dev.js",
300 "**/next/dist/compiled/webpack/*",
301 "**/node_modules/webpack5/**/*",
302 "**/next/dist/server/lib/route-resolver*",
303 "**/next/dist/compiled/semver/semver/**/*.js",
304 "**/next/dist/compiled/jest-worker/**/*",
305 "**/next/dist/next-devtools/userspace/use-app-dev-rendering-indicator.js",
308 "**/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js",
311 "**/next/dist/server/lib/router-utils/setup-dev-bundler.js",
313 "**/next/dist/server/dev/next-dev-server.js",
315 "**/next/dist/compiled/browserslist/**",
318 ]
319 .into_iter()
320 .chain(additional_ignores.iter().map(|s| s.as_str()))
321 .chain(if has_next_support {
324 Either::Left(
325 [
326 "**/node_modules/sharp/**/*",
327 "**/@img/sharp-libvips*/**/*",
328 "**/next/dist/server/image-optimizer.js",
329 ]
330 .into_iter(),
331 )
332 } else {
333 Either::Right(std::iter::empty())
334 })
335 .chain(if is_standalone {
336 Either::Left(std::iter::empty())
337 } else {
338 Either::Right(["**/*/next/dist/server/next.js", "**/*/next/dist/bin/next"].into_iter())
339 })
340 .map(|g| Glob::new(g.into(), Default::default()))
341 .collect::<Vec<_>>();
342
343 Ok(match self.ty {
344 ServerNftType::Full => Glob::alternatives(server_ignores_glob),
345 ServerNftType::Minimal => Glob::alternatives(
346 server_ignores_glob
347 .into_iter()
348 .chain(
349 [
350 "**/next/dist/compiled/edge-runtime/**/*",
351 "**/next/dist/server/web/sandbox/**/*",
352 "**/next/dist/server/post-process.js",
353 ]
354 .into_iter()
355 .map(|g| Glob::new(g.into(), Default::default())),
356 )
357 .collect(),
358 ),
359 })
360 }
361}