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 serde_json::json;
8use turbo_rcstr::{RcStr, rcstr};
9use turbo_tasks::{ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc, trace::TraceRawVcs};
10use turbo_tasks_fs::{
11    DirectoryContent, DirectoryEntry, File, FileContent, FileSystemPath,
12    glob::{Glob, GlobOptions},
13};
14use turbo_tasks_hash::HashAlgorithm;
15use turbopack::externals_tracing_module_context;
16use turbopack_core::{
17    asset::{Asset, AssetContent},
18    context::AssetContext,
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, 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(project_path: FileSystemPath) -> Result<Vc<Modules>> {
77    let asset_context = Vc::upcast(externals_tracing_module_context(
78        get_tracing_compile_time_info(),
79        false,
80        None,
81    ));
82    let next_resolve_origin = Vc::upcast(PlainResolveOrigin::new(
83        asset_context,
84        get_next_package(project_path).await?.join("_")?,
85    ));
86
87    Ok(Vc::cell(
88        cjs_resolve(
89            next_resolve_origin,
90            Request::parse_string(
91                "next/dist/compiled/next-server/pages-turbo.runtime.prod.js".into(),
92            ),
93            CommonJsReferenceSubType::Undefined,
94            None,
95            ResolveErrorMode::Error,
96        )
97        .await?
98        .primary_modules()
99        .await?
100        .to_vec(),
101    ))
102}
103
104#[turbo_tasks::task_input]
105#[derive(PartialEq, Eq, TraceRawVcs, Debug, Clone, Hash, Encode, Decode)]
106enum ServerNftType {
107    Minimal,
108    Full,
109}
110
111#[turbo_tasks::function]
112pub async fn next_server_nft_assets(project: Vc<Project>) -> Result<Vc<OutputAssets>> {
113    let is_standalone = *project.next_config().is_standalone().await?;
114
115    if *project.next_config().is_using_adapter().await? && !is_standalone {
116        // When using an adapter, `next-server.js.nft.json` / `next-minimal-server.js.nft.json` are
117        // not needed: they exist for `output: 'standalone'` (see `copyTracedFiles`), while an
118        // adapter assembles the deployment from the per-endpoint NFTs in build-complete.ts. What
119        // those two files trace on top of the endpoints - the `styled-jsx` modules the require hook
120        // needs at runtime - is part of every endpoint's trace via
121        // `Project::additional_traced_modules`, so nothing is lost here.
122        //
123        // The exception is `output: 'standalone'` configured alongside an adapter:
124        // `copyTracedFiles` reads `next-server.js.nft.json` unconditionally whenever standalone
125        // output is requested (adapter or not), so suppressing the pair crashes the build
126        // (see #96646).
127        return Ok(Vc::cell(vec![]));
128    }
129
130    let has_next_support = *project.ci_has_next_support().await?;
131
132    let minimal = ResolvedVc::upcast(
133        ServerNftJsonAsset::new(project, ServerNftType::Minimal)
134            .to_resolved()
135            .await?,
136    );
137
138    if has_next_support && !is_standalone {
139        // When deploying to Vercel, we only need next-minimal-server.js.nft.json
140        Ok(Vc::cell(vec![minimal]))
141    } else {
142        Ok(Vc::cell(vec![
143            minimal,
144            ResolvedVc::upcast(
145                ServerNftJsonAsset::new(project, ServerNftType::Full)
146                    .to_resolved()
147                    .await?,
148            ),
149        ]))
150    }
151}
152
153#[turbo_tasks::value]
154pub struct ServerNftJsonAsset {
155    project: ResolvedVc<Project>,
156    ty: ServerNftType,
157}
158
159#[turbo_tasks::value_impl]
160impl ServerNftJsonAsset {
161    #[turbo_tasks::function]
162    pub fn new(project: ResolvedVc<Project>, ty: ServerNftType) -> Vc<Self> {
163        ServerNftJsonAsset { project, ty }.cell()
164    }
165}
166
167#[turbo_tasks::value_impl]
168impl OutputAssetsReference for ServerNftJsonAsset {}
169
170#[turbo_tasks::value_impl]
171impl OutputAsset for ServerNftJsonAsset {
172    #[turbo_tasks::function]
173    async fn path(&self) -> Result<Vc<FileSystemPath>> {
174        let name = match self.ty {
175            ServerNftType::Minimal => "next-minimal-server.js.nft.json",
176            ServerNftType::Full => "next-server.js.nft.json",
177        };
178
179        Ok(self.project.node_root().await?.join(name)?.cell())
180    }
181}
182
183#[turbo_tasks::value_impl]
184impl Asset for ServerNftJsonAsset {
185    #[turbo_tasks::function]
186    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
187        let this = self.await?;
188
189        // Example: [project]/apps/my-website/.next/
190        let base_dir = this
191            .project
192            .project_root_path()
193            .await?
194            .join(&this.project.node_root().await?.path)?;
195
196        let module_graph = ModuleGraph::from_graphs(
197            vec![SingleModuleGraph::new_with_entries(
198                GraphEntries::new(vec![], self.entries().owned().await?).resolved_cell(),
199                true,
200                false,
201            )],
202            None,
203        )
204        .connect();
205
206        let hash_salt = this.project.next_config().output_hash_salt();
207
208        let mut server_output_assets = traced_modules_for_entries(
209            module_graph,
210            Modules::empty(),
211            self.entries(),
212            Some(self.ignores()),
213            None,
214        )
215        .await?
216        .iter()
217        .map(async |m| {
218            Ok((
219                base_dir
220                    .get_relative_path_to(&m.ident().await?.path)
221                    .context("failed to compute relative path for server NFT JSON")?,
222                m.source()
223                    .await?
224                    .context("NFT module has no content")?
225                    .content()
226                    .hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
227                    .await?,
228            ))
229        })
230        .try_join()
231        .await?;
232
233        let next_dir = get_next_package(this.project.project_path().owned().await?).await?;
234        for ty in ["app-page", "pages"] {
235            let dir = next_dir.join(&format!("dist/server/route-modules/{ty}"))?;
236            let module_path = dir.join("module.compiled.js")?;
237            server_output_assets.push((
238                base_dir
239                    .get_relative_path_to(&module_path)
240                    .context("failed to compute relative path for server NFT JSON")?,
241                module_path
242                    .hash_file(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
243                    .await?,
244            ));
245
246            let contexts_dir = dir.join("vendored/contexts")?;
247            let DirectoryContent::Entries(contexts_files) = &*contexts_dir.read_dir().await? else {
248                bail!(
249                    "Expected contexts directory to be a directory, found: {:?}",
250                    contexts_dir
251                );
252            };
253            for (_, entry) in contexts_files {
254                let DirectoryEntry::File(file) = entry else {
255                    continue;
256                };
257                if file.extension() == Some("js") {
258                    server_output_assets.push((
259                        base_dir
260                            .get_relative_path_to(file)
261                            .context("failed to compute relative path for server NFT JSON")?,
262                        file.hash_file(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
263                            .await?,
264                    ))
265                }
266            }
267        }
268
269        server_output_assets.sort_unstable();
270        // Dedupe as some entries may be duplicates: a file might be referenced multiple times,
271        // e.g. as a RawModule (from an FS operation) and as an EcmascriptModuleAsset because it
272        // was required.
273        server_output_assets.dedup();
274
275        let (files, file_hashes): (Vec<_>, Vec<_>) = server_output_assets.into_iter().unzip();
276        let json = json!({
277            "version": 1,
278            "files": files,
279            "fileHashes": file_hashes
280        });
281
282        Ok(AssetContent::file(
283            FileContent::Content(File::from(json.to_string())).cell(),
284        ))
285    }
286}
287
288/// These globs are used to prune the module graph for the server NFT JSON assets. They are always
289/// ignored for every page, so we can completely skip even parsing/walking these modules.
290fn next_owned_ignores(
291    ty: &ServerNftType,
292    has_next_support: bool,
293    is_standalone: bool,
294) -> Vec<RcStr> {
295    let mut globs = vec![
296        rcstr!("**/node_modules/react{,-dom,-server-dom-turbopack}/**/*.development.js"),
297        rcstr!("**/*.d.ts"),
298        rcstr!("**/*.map"),
299        rcstr!("**/next/dist/pages/**/*"),
300        rcstr!("**/next/dist/compiled/next-server/**/*.dev.js"),
301        rcstr!("**/next/dist/compiled/webpack/*"),
302        rcstr!("**/node_modules/webpack5/**/*"),
303        rcstr!("**/next/dist/server/lib/route-resolver*"),
304        // The testmode interceptors bundle reads its HTTP parser WASM with a
305        // dynamic path, making the tracer include the bundle's whole
306        // directory. Test proxying is not supported in standalone output, so
307        // keep the parser asset (and the license file picked up by the
308        // directory glob) out of production traces.
309        rcstr!("**/next/dist/compiled/@mswjs/interceptors/ClientRequest/LICENSE"),
310        rcstr!("**/next/dist/compiled/@mswjs/interceptors/ClientRequest/llhttp/**"),
311        rcstr!("**/next/dist/compiled/semver/semver/**/*.js"),
312        rcstr!("**/next/dist/compiled/jest-worker/**/*"),
313        // -- The following were added for Turbopack specifically --
314        // 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
315        rcstr!("**/next/dist/next-devtools/userspace/use-app-dev-rendering-indicator.js"),
316        // client/components/app-router.js has a process.env.NODE_ENV guard, but we
317        // can't set that.
318        rcstr!("**/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js"),
319        // server/lib/router-server.js doesn't guard this require:
320        rcstr!("**/next/dist/server/lib/router-utils/setup-dev-bundler.js"),
321        // server/next.js doesn't guard this require
322        rcstr!("**/next/dist/server/dev/next-dev-server.js"),
323        // next/dist/compiled/babel* pulls in this, but we never actually transpile at
324        // deploy-time
325        rcstr!("**/next/dist/compiled/browserslist/**"),
326    ];
327
328    // only ignore image-optimizer code when
329    // this is being handled outside of next-server
330    if has_next_support {
331        globs.extend([
332            rcstr!("**/node_modules/sharp/**/*"),
333            rcstr!("**/@img/sharp-libvips*/**/*"),
334            rcstr!("**/next/dist/server/image-optimizer.js"),
335        ]);
336    }
337
338    if !is_standalone {
339        globs.extend([
340            rcstr!("**/*/next/dist/server/next.js"),
341            rcstr!("**/*/next/dist/bin/next"),
342        ]);
343    }
344
345    if matches!(ty, ServerNftType::Minimal) {
346        globs.extend([
347            rcstr!("**/next/dist/compiled/edge-runtime/**/*"),
348            rcstr!("**/next/dist/server/web/sandbox/**/*"),
349            rcstr!("**/next/dist/server/post-process.js"),
350        ]);
351    }
352
353    globs
354}
355
356#[turbo_tasks::value_impl]
357impl ServerNftJsonAsset {
358    #[turbo_tasks::function]
359    async fn entries(&self) -> Result<Vc<Modules>> {
360        let is_standalone = *self.project.next_config().is_standalone().await?;
361
362        let prune = Glob::alternatives(
363            next_owned_ignores(
364                &self.ty,
365                *self.project.ci_has_next_support().await?,
366                is_standalone,
367            )
368            .into_iter()
369            .map(|g| Glob::new(g, GlobOptions::default()))
370            .collect(),
371        )
372        .to_resolved()
373        .await?;
374
375        let asset_context = Vc::upcast(externals_tracing_module_context(
376            get_tracing_compile_time_info(),
377            false,
378            Some((self.project.project_root_path().owned().await?, prune)),
379        ));
380
381        let project_path = self.project.project_path().owned().await?;
382
383        let next_resolve_origin = Vc::upcast(PlainResolveOrigin::new(
384            asset_context,
385            get_next_package(project_path.clone()).await?.join("_")?,
386        ));
387
388        let entries = match self.ty {
389            ServerNftType::Full => Either::Left(
390                if is_standalone {
391                    Either::Left(
392                        [
393                            "next/dist/server/lib/start-server",
394                            "next/dist/server/next",
395                            "next/dist/server/require-hook",
396                        ]
397                        .into_iter(),
398                    )
399                } else {
400                    Either::Right(std::iter::empty())
401                }
402                .chain(std::iter::once("next/dist/server/next-server")),
403            ),
404            ServerNftType::Minimal => Either::Right(std::iter::once(
405                "next/dist/compiled/next-server/server.runtime.prod",
406            )),
407        };
408
409        // The modules the require hook needs are part of every endpoint's trace (see
410        // `Project::additional_traced_modules`), but `next-server.js` / `next-minimal-server.js`
411        // are traced on their own for `output: 'standalone'`, so they have to be added here too.
412        let hook_modules = require_hook_modules(project_path, asset_context)
413            .owned()
414            .await?;
415
416        Ok(Vc::cell(
417            hook_modules
418                .into_iter()
419                .chain(
420                    entries
421                        .map(async |path| {
422                            Ok(cjs_resolve(
423                                next_resolve_origin,
424                                Request::parse_string(path.into()),
425                                CommonJsReferenceSubType::Undefined,
426                                None,
427                                ResolveErrorMode::Error,
428                            )
429                            .await?
430                            .primary_modules()
431                            .await?
432                            .into_iter())
433                        })
434                        .try_flat_join()
435                        .await?,
436                )
437                .collect(),
438        ))
439    }
440
441    #[turbo_tasks::function]
442    async fn ignores(&self) -> Result<Vc<Glob>> {
443        let is_standalone = *self.project.next_config().is_standalone().await?;
444        let has_next_support = *self.project.ci_has_next_support().await?;
445        let project_path = self.project.project_path().owned().await?;
446
447        let output_file_tracing_excludes = self
448            .project
449            .next_config()
450            .output_file_tracing_excludes(project_path)
451            .await?;
452        let mut additional_ignores = BTreeSet::new();
453
454        for (route_glob, exclude_patterns) in output_file_tracing_excludes.iter() {
455            // Check if the route matches the glob pattern
456            if route_glob.await?.matches("next-server") {
457                for (glob, root) in exclude_patterns {
458                    additional_ignores.insert(if root.path.is_empty() {
459                        glob.clone()
460                    } else {
461                        format!("{root}/{glob}").into()
462                    });
463                }
464            }
465        }
466
467        // The project-provided ignores can match one of the entry requests `entries()` resolves,
468        // so they can only be applied to the finished graph:
469        // `traced_modules_for_entries` inserts entries without consulting the glob (the
470        // `parent == None` arm in `nft.rs`), whereas pruning one would delete it and everything
471        // reachable only through it.
472        let server_ignores_glob = next_owned_ignores(&self.ty, has_next_support, is_standalone)
473            .into_iter()
474            .chain(additional_ignores)
475            .map(|g| Glob::new(g, Default::default()))
476            .collect::<Vec<_>>();
477
478        Ok(Glob::alternatives(server_ignores_glob))
479    }
480}