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