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::{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 turbo_tasks_hash::HashAlgorithm;
17use turbopack::externals_tracing_module_context;
18use turbopack_core::{
19    asset::{Asset, AssetContent},
20    context::AssetContext,
21    file_source::FileSource,
22    module::{Module, Modules},
23    module_graph::{ModuleGraph, SingleModuleGraph, chunk_group_info::ChunkGroupEntry},
24    output::{OutputAsset, OutputAssets, OutputAssetsReference},
25    reference_type::{CommonJsReferenceSubType, ReferenceType},
26    resolve::{ResolveErrorMode, origin::PlainResolveOrigin, parse::Request},
27};
28use turbopack_resolve::ecmascript::cjs_resolve;
29
30use crate::{
31    nft_json::{relativize_glob, traced_modules_for_entries},
32    project::Project,
33};
34
35#[derive(
36    PartialEq, Eq, TraceRawVcs, NonLocalValue, Debug, Clone, Hash, TaskInput, Encode, Decode,
37)]
38enum ServerNftType {
39    Minimal,
40    Full,
41}
42
43#[turbo_tasks::function]
44pub async fn next_server_nft_assets(project: Vc<Project>) -> Result<Vc<OutputAssets>> {
45    if *project.next_config().is_using_adapter().await? {
46        // When using an adapter, we don't need to generate any server NFTs as build-complete
47        // doesn't use them at all.
48        return Ok(Vc::cell(vec![]));
49    }
50
51    let has_next_support = *project.ci_has_next_support().await?;
52    let is_standalone = *project.next_config().is_standalone().await?;
53
54    let minimal = ResolvedVc::upcast(
55        ServerNftJsonAsset::new(project, ServerNftType::Minimal)
56            .to_resolved()
57            .await?,
58    );
59
60    if has_next_support && !is_standalone {
61        // When deploying to Vercel, we only need next-minimal-server.js.nft.json
62        Ok(Vc::cell(vec![minimal]))
63    } else {
64        Ok(Vc::cell(vec![
65            minimal,
66            ResolvedVc::upcast(
67                ServerNftJsonAsset::new(project, ServerNftType::Full)
68                    .to_resolved()
69                    .await?,
70            ),
71        ]))
72    }
73}
74
75#[turbo_tasks::value]
76pub struct ServerNftJsonAsset {
77    project: ResolvedVc<Project>,
78    ty: ServerNftType,
79}
80
81#[turbo_tasks::value_impl]
82impl ServerNftJsonAsset {
83    #[turbo_tasks::function]
84    pub fn new(project: ResolvedVc<Project>, ty: ServerNftType) -> Vc<Self> {
85        ServerNftJsonAsset { project, ty }.cell()
86    }
87}
88
89#[turbo_tasks::value_impl]
90impl OutputAssetsReference for ServerNftJsonAsset {}
91
92#[turbo_tasks::value_impl]
93impl OutputAsset for ServerNftJsonAsset {
94    #[turbo_tasks::function]
95    async fn path(&self) -> Result<Vc<FileSystemPath>> {
96        let name = match self.ty {
97            ServerNftType::Minimal => "next-minimal-server.js.nft.json",
98            ServerNftType::Full => "next-server.js.nft.json",
99        };
100
101        Ok(self.project.node_root().await?.join(name)?.cell())
102    }
103}
104
105#[turbo_tasks::value_impl]
106impl Asset for ServerNftJsonAsset {
107    #[turbo_tasks::function]
108    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
109        let this = self.await?;
110
111        // Example: [project]/apps/my-website/.next/
112        let base_dir = this
113            .project
114            .project_root_path()
115            .await?
116            .join(&this.project.node_root().await?.path)?;
117
118        let module_graph = ModuleGraph::from_graphs(
119            vec![SingleModuleGraph::new_with_traced_entries(
120                ResolvedVc::cell(vec![ChunkGroupEntry::Entry(self.entries().owned().await?)]),
121                true,
122                false,
123            )],
124            None,
125        )
126        .connect();
127
128        let mut server_output_assets =
129            traced_modules_for_entries(module_graph, self.entries(), Some(self.ignores()), true)
130                .await?
131                .iter()
132                .map(async |m| {
133                    Ok((
134                        base_dir
135                            .get_relative_path_to(&m.ident().await?.path)
136                            .context("failed to compute relative path for server NFT JSON")?,
137                        m.source()
138                            .await?
139                            .context("NFT module has no content")?
140                            .content()
141                            .hash(HashAlgorithm::Xxh3Hash128Hex)
142                            .await?,
143                    ))
144                })
145                .try_join()
146                .await?;
147
148        let next_dir = get_next_package(this.project.project_path().owned().await?).await?;
149        for ty in ["app-page", "pages"] {
150            let dir = next_dir.join(&format!("dist/server/route-modules/{ty}"))?;
151            let module_path = dir.join("module.compiled.js")?;
152            server_output_assets.push((
153                base_dir
154                    .get_relative_path_to(&module_path)
155                    .context("failed to compute relative path for server NFT JSON")?,
156                module_path
157                    .read()
158                    .hash(HashAlgorithm::Xxh3Hash128Hex)
159                    .await?,
160            ));
161
162            let contexts_dir = dir.join("vendored/contexts")?;
163            let DirectoryContent::Entries(contexts_files) = &*contexts_dir.read_dir().await? else {
164                bail!(
165                    "Expected contexts directory to be a directory, found: {:?}",
166                    contexts_dir
167                );
168            };
169            for (_, entry) in contexts_files {
170                let DirectoryEntry::File(file) = entry else {
171                    continue;
172                };
173                if file.extension() == Some("js") {
174                    server_output_assets.push((
175                        base_dir
176                            .get_relative_path_to(file)
177                            .context("failed to compute relative path for server NFT JSON")?,
178                        file.read().hash(HashAlgorithm::Xxh3Hash128Hex).await?,
179                    ))
180                }
181            }
182        }
183
184        server_output_assets.sort_unstable();
185        // Dedupe as some entries may be duplicates: a file might be referenced multiple times,
186        // e.g. as a RawModule (from an FS operation) and as an EcmascriptModuleAsset because it
187        // was required.
188        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 cache_handler = self
222            .project
223            .next_config()
224            .cache_handler(project_path.clone())
225            .await?;
226        let cache_handlers = self
227            .project
228            .next_config()
229            .cache_handlers(project_path.clone())
230            .await?;
231
232        // These are used by packages/next/src/server/require-hook.ts
233        let shared_entries = ["styled-jsx", "styled-jsx/style", "styled-jsx/style.js"];
234
235        let cache_handler_entries = cache_handler
236            .iter()
237            .chain(cache_handlers.iter())
238            .map(|f| {
239                asset_context
240                    .process(
241                        Vc::upcast(FileSource::new(f.clone())),
242                        ReferenceType::CommonJs(CommonJsReferenceSubType::Undefined),
243                    )
244                    .module()
245            })
246            .map(|m| m.to_resolved())
247            .try_join()
248            .await?;
249
250        let entries = match self.ty {
251            ServerNftType::Full => Either::Left(
252                if is_standalone {
253                    Either::Left(
254                        [
255                            "next/dist/server/lib/start-server",
256                            "next/dist/server/next",
257                            "next/dist/server/require-hook",
258                        ]
259                        .into_iter(),
260                    )
261                } else {
262                    Either::Right(std::iter::empty())
263                }
264                .chain(std::iter::once("next/dist/server/next-server")),
265            ),
266            ServerNftType::Minimal => Either::Right(std::iter::once(
267                "next/dist/compiled/next-server/server.runtime.prod",
268            )),
269        };
270
271        Ok(Vc::cell(
272            cache_handler_entries
273                .into_iter()
274                .chain(
275                    shared_entries
276                        .into_iter()
277                        .chain(entries)
278                        .map(async |path| {
279                            Ok(cjs_resolve(
280                                next_resolve_origin,
281                                Request::parse_string(path.into()),
282                                CommonJsReferenceSubType::Undefined,
283                                None,
284                                ResolveErrorMode::Error,
285                            )
286                            .await?
287                            .primary_modules()
288                            .await?
289                            .into_iter())
290                        })
291                        .try_flat_join()
292                        .await?,
293                )
294                .collect(),
295        ))
296    }
297
298    #[turbo_tasks::function]
299    async fn ignores(&self) -> Result<Vc<Glob>> {
300        let is_standalone = *self.project.next_config().is_standalone().await?;
301        let has_next_support = *self.project.ci_has_next_support().await?;
302        let project_path = self.project.project_path().owned().await?;
303
304        let output_file_tracing_excludes = self
305            .project
306            .next_config()
307            .output_file_tracing_excludes()
308            .await?;
309        let mut additional_ignores = BTreeSet::new();
310        if let Some(output_file_tracing_excludes) = output_file_tracing_excludes
311            .as_ref()
312            .and_then(Value::as_object)
313        {
314            for (glob_pattern, exclude_patterns) in output_file_tracing_excludes {
315                // Check if the route matches the glob pattern
316                let glob = Glob::new(RcStr::from(glob_pattern.clone()), Default::default()).await?;
317                if glob.matches("next-server")
318                    && let Some(patterns) = exclude_patterns.as_array()
319                {
320                    for pattern in patterns {
321                        if let Some(pattern_str) = pattern.as_str() {
322                            let (glob, root) = relativize_glob(pattern_str, project_path.clone())?;
323                            let glob = if root.path.is_empty() {
324                                glob.to_string()
325                            } else {
326                                format!("{root}/{glob}")
327                            };
328                            additional_ignores.insert(glob);
329                        }
330                    }
331                }
332            }
333        }
334
335        let server_ignores_glob = [
336            "**/node_modules/react{,-dom,-server-dom-turbopack}/**/*.development.js",
337            "**/*.d.ts",
338            "**/*.map",
339            "**/next/dist/pages/**/*",
340            "**/next/dist/compiled/next-server/**/*.dev.js",
341            "**/next/dist/compiled/webpack/*",
342            "**/node_modules/webpack5/**/*",
343            "**/next/dist/server/lib/route-resolver*",
344            "**/next/dist/compiled/semver/semver/**/*.js",
345            "**/next/dist/compiled/jest-worker/**/*",
346            // -- The following were added for Turbopack specifically --
347            // 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
348            "**/next/dist/next-devtools/userspace/use-app-dev-rendering-indicator.js",
349            // client/components/app-router.js has a process.env.NODE_ENV guard, but we
350            // can't set that.
351            "**/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js",
352            // server/lib/router-server.js doesn't guard this require:
353            "**/next/dist/server/lib/router-utils/setup-dev-bundler.js",
354            // server/next.js doesn't guard this require
355            "**/next/dist/server/dev/next-dev-server.js",
356            // next/dist/compiled/babel* pulls in this, but we never actually transpile at
357            // deploy-time
358            "**/next/dist/compiled/browserslist/**",
359        ]
360        .into_iter()
361        .chain(additional_ignores.iter().map(|s| s.as_str()))
362        // only ignore image-optimizer code when
363        // this is being handled outside of next-server
364        .chain(if has_next_support {
365            Either::Left(
366                [
367                    "**/node_modules/sharp/**/*",
368                    "**/@img/sharp-libvips*/**/*",
369                    "**/next/dist/server/image-optimizer.js",
370                ]
371                .into_iter(),
372            )
373        } else {
374            Either::Right(std::iter::empty())
375        })
376        .chain(if is_standalone {
377            Either::Left(std::iter::empty())
378        } else {
379            Either::Right(["**/*/next/dist/server/next.js", "**/*/next/dist/bin/next"].into_iter())
380        })
381        .map(|g| Glob::new(g.into(), Default::default()))
382        .collect::<Vec<_>>();
383
384        Ok(match self.ty {
385            ServerNftType::Full => Glob::alternatives(server_ignores_glob),
386            ServerNftType::Minimal => Glob::alternatives(
387                server_ignores_glob
388                    .into_iter()
389                    .chain(
390                        [
391                            "**/next/dist/compiled/edge-runtime/**/*",
392                            "**/next/dist/server/web/sandbox/**/*",
393                            "**/next/dist/server/post-process.js",
394                        ]
395                        .into_iter()
396                        .map(|g| Glob::new(g.into(), Default::default())),
397                    )
398                    .collect(),
399            ),
400        })
401    }
402}