Skip to main content

next_api/
next_server_nft.rs

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/// The modules `next/dist/server/require-hook` resolves its aliased requests to at runtime
30/// (currently all of styled-jsx), so that the Pages Router renderer and user code share a single
31/// `styled-jsx` module instance (see that file for why they otherwise wouldn't). Nothing in any
32/// module graph references these - user code only ever imports its own copy of `styled-jsx` - so
33/// whatever assembles a deployment's file list has to include them explicitly, or the hook fails
34/// to register its aliases and all styled-jsx styles silently disappear from the server-rendered
35/// HTML.
36///
37/// Used by the server NFTs below and, so that they are part of every endpoint's trace regardless
38/// of how the output is assembled, by [`Project::additional_traced_modules`].
39#[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/// The Pages renderer selected dynamically by `pages/module.compiled` in Turbopack production
71/// builds. A Pages API endpoint can load the compiled module through a vendored context when an
72/// external dependency imports `next/head`, but neither dynamic edge is visible in its module
73/// graph. Include the renderer as an explicit Pages trace entry so that its runtime closure is
74/// available when the endpoint initializes.
75#[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        // When using an adapter, `next-server.js.nft.json` / `next-minimal-server.js.nft.json` are
115        // not needed: they exist for `output: 'standalone'` (see `copyTracedFiles`), while an
116        // adapter assembles the deployment from the per-endpoint NFTs in build-complete.ts. What
117        // those two files trace on top of the endpoints - the `styled-jsx` modules the require hook
118        // needs at runtime - is part of every endpoint's trace via
119        // `Project::additional_traced_modules`, so nothing is lost here.
120        //
121        // The exception is `output: 'standalone'` configured alongside an adapter:
122        // `copyTracedFiles` reads `next-server.js.nft.json` unconditionally whenever standalone
123        // output is requested (adapter or not), so suppressing the pair crashes the build
124        // (see #96646).
125        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        // When deploying to Vercel, we only need next-minimal-server.js.nft.json
138        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        // Example: [project]/apps/my-website/.next/
188        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
275/// These globs are used to prune the module graph for the server NFT JSON assets. They are always
276/// ignored for every page, so we can completely skip even parsing/walking these modules.
277fn 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        // Upgrade workflows are CLI-only and are not needed by production servers.
292        rcstr!("**/next/dist/lib/upgrade/**/*"),
293        // The testmode interceptors bundle reads its HTTP parser WASM with a
294        // dynamic path, making the tracer include the bundle's whole
295        // directory. Test proxying is not supported in standalone output, so
296        // keep the parser asset (and the license file picked up by the
297        // directory glob) out of production traces.
298        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        // -- The following were added for Turbopack specifically --
303        // client/components/use-action-queue.ts has a process.env.NODE_ENV guard, but we can't set that due to React: https://github.com/vercel/next.js/pull/75254
304        rcstr!("**/next/dist/next-devtools/userspace/use-app-dev-rendering-indicator.js"),
305        // client/components/app-router.js has a process.env.NODE_ENV guard, but we
306        // can't set that.
307        rcstr!("**/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js"),
308        // server/lib/router-server.js doesn't guard this require:
309        rcstr!("**/next/dist/server/lib/router-utils/setup-dev-bundler.js"),
310        // server/next.js doesn't guard this require
311        rcstr!("**/next/dist/server/dev/next-dev-server.js"),
312        // next/dist/compiled/babel* pulls in this, but we never actually transpile at
313        // deploy-time
314        rcstr!("**/next/dist/compiled/browserslist/**"),
315    ];
316
317    // only ignore image-optimizer code when
318    // this is being handled outside of next-server
319    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            /* resolve_typescript */ false,
367            /* prune */ Some((self.project.project_root_path().owned().await?, prune)),
368            /* trace_file_references */ 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        // The modules the require hook needs are part of every endpoint's trace (see
400        // `Project::additional_traced_modules`), but `next-server.js` / `next-minimal-server.js`
401        // are traced on their own for `output: 'standalone'`, so they have to be added here too.
402        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            // Check if the route matches the glob pattern
446            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        // The project-provided ignores can match one of the entry requests `entries()` resolves,
458        // so they can only be applied to the finished graph:
459        // `traced_modules_for_entries` inserts entries without consulting the glob (the
460        // `parent == None` arm in `nft.rs`), whereas pruning one would delete it and everything
461        // reachable only through it.
462        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}