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_tasks::{ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc, trace::TraceRawVcs};
9use turbo_tasks_fs::{
10    DirectoryContent, DirectoryEntry, File, FileContent, FileSystemPath, glob::Glob,
11};
12use turbo_tasks_hash::HashAlgorithm;
13use turbopack::externals_tracing_module_context;
14use turbopack_core::{
15    asset::{Asset, AssetContent},
16    module::{Module, Modules},
17    module_graph::{GraphEntries, ModuleGraph, SingleModuleGraph},
18    output::{OutputAsset, OutputAssets, OutputAssetsReference},
19    reference_type::CommonJsReferenceSubType,
20    resolve::{ResolveErrorMode, origin::PlainResolveOrigin, parse::Request},
21};
22use turbopack_resolve::ecmascript::cjs_resolve;
23
24use crate::{nft::traced_modules_for_entries, project::Project};
25
26#[turbo_tasks::task_input]
27#[derive(PartialEq, Eq, TraceRawVcs, Debug, Clone, Hash, Encode, Decode)]
28enum ServerNftType {
29    Minimal,
30    Full,
31}
32
33#[turbo_tasks::function]
34pub async fn next_server_nft_assets(project: Vc<Project>) -> Result<Vc<OutputAssets>> {
35    if *project.next_config().is_using_adapter().await? {
36        // When using an adapter, we don't need to generate any server NFTs as build-complete
37        // doesn't use them at all.
38        return Ok(Vc::cell(vec![]));
39    }
40
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        // When deploying to Vercel, we only need next-minimal-server.js.nft.json
52        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 OutputAssetsReference for ServerNftJsonAsset {}
81
82#[turbo_tasks::value_impl]
83impl OutputAsset for ServerNftJsonAsset {
84    #[turbo_tasks::function]
85    async fn path(&self) -> Result<Vc<FileSystemPath>> {
86        let name = match self.ty {
87            ServerNftType::Minimal => "next-minimal-server.js.nft.json",
88            ServerNftType::Full => "next-server.js.nft.json",
89        };
90
91        Ok(self.project.node_root().await?.join(name)?.cell())
92    }
93}
94
95#[turbo_tasks::value_impl]
96impl Asset for ServerNftJsonAsset {
97    #[turbo_tasks::function]
98    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
99        let this = self.await?;
100
101        // Example: [project]/apps/my-website/.next/
102        let base_dir = this
103            .project
104            .project_root_path()
105            .await?
106            .join(&this.project.node_root().await?.path)?;
107
108        let module_graph = ModuleGraph::from_graphs(
109            vec![SingleModuleGraph::new_with_entries(
110                GraphEntries::new(vec![], self.entries().owned().await?).resolved_cell(),
111                true,
112                false,
113            )],
114            None,
115        )
116        .connect();
117
118        let hash_salt = this.project.next_config().output_hash_salt();
119
120        let mut server_output_assets = traced_modules_for_entries(
121            module_graph,
122            Modules::empty(),
123            self.entries(),
124            Some(self.ignores()),
125            None,
126            hash_salt,
127        )
128        .await?
129        .iter()
130        .map(async |m| {
131            Ok((
132                base_dir
133                    .get_relative_path_to(&m.ident().await?.path)
134                    .context("failed to compute relative path for server NFT JSON")?,
135                m.source()
136                    .await?
137                    .context("NFT module has no content")?
138                    .content()
139                    .hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
140                    .await?,
141            ))
142        })
143        .try_join()
144        .await?;
145
146        let next_dir = get_next_package(this.project.project_path().owned().await?).await?;
147        for ty in ["app-page", "pages"] {
148            let dir = next_dir.join(&format!("dist/server/route-modules/{ty}"))?;
149            let module_path = dir.join("module.compiled.js")?;
150            server_output_assets.push((
151                base_dir
152                    .get_relative_path_to(&module_path)
153                    .context("failed to compute relative path for server NFT JSON")?,
154                module_path
155                    .read()
156                    .hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
157                    .await?,
158            ));
159
160            let contexts_dir = dir.join("vendored/contexts")?;
161            let DirectoryContent::Entries(contexts_files) = &*contexts_dir.read_dir().await? else {
162                bail!(
163                    "Expected contexts directory to be a directory, found: {:?}",
164                    contexts_dir
165                );
166            };
167            for (_, entry) in contexts_files {
168                let DirectoryEntry::File(file) = entry else {
169                    continue;
170                };
171                if file.extension() == Some("js") {
172                    server_output_assets.push((
173                        base_dir
174                            .get_relative_path_to(file)
175                            .context("failed to compute relative path for server NFT JSON")?,
176                        file.read()
177                            .hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
178                            .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        // These are used by packages/next/src/server/require-hook.ts
222        let shared_entries = ["styled-jsx", "styled-jsx/style", "styled-jsx/style.js"];
223
224        let entries = match self.ty {
225            ServerNftType::Full => Either::Left(
226                if is_standalone {
227                    Either::Left(
228                        [
229                            "next/dist/server/lib/start-server",
230                            "next/dist/server/next",
231                            "next/dist/server/require-hook",
232                        ]
233                        .into_iter(),
234                    )
235                } else {
236                    Either::Right(std::iter::empty())
237                }
238                .chain(std::iter::once("next/dist/server/next-server")),
239            ),
240            ServerNftType::Minimal => Either::Right(std::iter::once(
241                "next/dist/compiled/next-server/server.runtime.prod",
242            )),
243        };
244
245        Ok(Vc::cell(
246            shared_entries
247                .into_iter()
248                .chain(entries)
249                .map(async |path| {
250                    Ok(cjs_resolve(
251                        next_resolve_origin,
252                        Request::parse_string(path.into()),
253                        CommonJsReferenceSubType::Undefined,
254                        None,
255                        ResolveErrorMode::Error,
256                    )
257                    .await?
258                    .primary_modules()
259                    .await?
260                    .into_iter())
261                })
262                .try_flat_join()
263                .await?,
264        ))
265    }
266
267    #[turbo_tasks::function]
268    async fn ignores(&self) -> Result<Vc<Glob>> {
269        let is_standalone = *self.project.next_config().is_standalone().await?;
270        let has_next_support = *self.project.ci_has_next_support().await?;
271        let project_path = self.project.project_path().owned().await?;
272
273        let output_file_tracing_excludes = self
274            .project
275            .next_config()
276            .output_file_tracing_excludes(project_path)
277            .await?;
278        let mut additional_ignores = BTreeSet::new();
279
280        for (route_glob, exclude_patterns) in output_file_tracing_excludes.iter() {
281            // Check if the route matches the glob pattern
282            if route_glob.await?.matches("next-server") {
283                for (glob, root) in exclude_patterns {
284                    additional_ignores.insert(if root.path.is_empty() {
285                        glob.to_string()
286                    } else {
287                        format!("{root}/{glob}")
288                    });
289                }
290            }
291        }
292
293        let server_ignores_glob = [
294            "**/node_modules/react{,-dom,-server-dom-turbopack}/**/*.development.js",
295            "**/*.d.ts",
296            "**/*.map",
297            "**/next/dist/pages/**/*",
298            "**/next/dist/compiled/next-server/**/*.dev.js",
299            "**/next/dist/compiled/webpack/*",
300            "**/node_modules/webpack5/**/*",
301            "**/next/dist/server/lib/route-resolver*",
302            "**/next/dist/compiled/semver/semver/**/*.js",
303            "**/next/dist/compiled/jest-worker/**/*",
304            // -- The following were added for Turbopack specifically --
305            // 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
306            "**/next/dist/next-devtools/userspace/use-app-dev-rendering-indicator.js",
307            // client/components/app-router.js has a process.env.NODE_ENV guard, but we
308            // can't set that.
309            "**/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js",
310            // server/lib/router-server.js doesn't guard this require:
311            "**/next/dist/server/lib/router-utils/setup-dev-bundler.js",
312            // server/next.js doesn't guard this require
313            "**/next/dist/server/dev/next-dev-server.js",
314            // next/dist/compiled/babel* pulls in this, but we never actually transpile at
315            // deploy-time
316            "**/next/dist/compiled/browserslist/**",
317        ]
318        .into_iter()
319        .chain(additional_ignores.iter().map(|s| s.as_str()))
320        // only ignore image-optimizer code when
321        // this is being handled outside of next-server
322        .chain(if has_next_support {
323            Either::Left(
324                [
325                    "**/node_modules/sharp/**/*",
326                    "**/@img/sharp-libvips*/**/*",
327                    "**/next/dist/server/image-optimizer.js",
328                ]
329                .into_iter(),
330            )
331        } else {
332            Either::Right(std::iter::empty())
333        })
334        .chain(if is_standalone {
335            Either::Left(std::iter::empty())
336        } else {
337            Either::Right(["**/*/next/dist/server/next.js", "**/*/next/dist/bin/next"].into_iter())
338        })
339        .map(|g| Glob::new(g.into(), Default::default()))
340        .collect::<Vec<_>>();
341
342        Ok(match self.ty {
343            ServerNftType::Full => Glob::alternatives(server_ignores_glob),
344            ServerNftType::Minimal => Glob::alternatives(
345                server_ignores_glob
346                    .into_iter()
347                    .chain(
348                        [
349                            "**/next/dist/compiled/edge-runtime/**/*",
350                            "**/next/dist/server/web/sandbox/**/*",
351                            "**/next/dist/server/post-process.js",
352                        ]
353                        .into_iter()
354                        .map(|g| Glob::new(g.into(), Default::default())),
355                    )
356                    .collect(),
357            ),
358        })
359    }
360}