next_api/
next_server_nft.rs

1use std::collections::BTreeSet;
2
3use anyhow::{Context, Result, bail};
4use either::Either;
5use next_core::{get_next_package, next_server::get_tracing_compile_time_info};
6use serde::{Deserialize, Serialize};
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::{DirectoryContent, DirectoryEntry, File, FileSystemPath, glob::Glob};
14use turbopack::externals_tracing_module_context;
15use turbopack_core::{
16    asset::{Asset, AssetContent},
17    context::AssetContext,
18    file_source::FileSource,
19    output::{OutputAsset, OutputAssets},
20    reference_type::{CommonJsReferenceSubType, ReferenceType},
21    resolve::{origin::PlainResolveOrigin, parse::Request},
22    traced_asset::TracedAsset,
23};
24use turbopack_ecmascript::resolve::cjs_resolve;
25
26use crate::{
27    nft_json::{all_assets_from_entries_filtered, relativize_glob},
28    project::Project,
29};
30
31#[derive(
32    PartialEq, Eq, TraceRawVcs, NonLocalValue, Deserialize, Serialize, Debug, Clone, Hash, TaskInput,
33)]
34enum ServerNftType {
35    Minimal,
36    Full,
37}
38
39#[turbo_tasks::function]
40pub async fn next_server_nft_assets(project: Vc<Project>) -> Result<Vc<OutputAssets>> {
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 OutputAsset for ServerNftJsonAsset {
81    #[turbo_tasks::function]
82    async fn path(&self) -> Result<Vc<FileSystemPath>> {
83        let name = match self.ty {
84            ServerNftType::Minimal => "next-minimal-server.js.nft.json",
85            ServerNftType::Full => "next-server.js.nft.json",
86        };
87
88        Ok(self.project.node_root().await?.join(name)?.cell())
89    }
90}
91
92#[turbo_tasks::value_impl]
93impl Asset for ServerNftJsonAsset {
94    #[turbo_tasks::function]
95    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
96        let this = self.await?;
97        // Example: [project]/apps/my-website/.next/
98        let base_dir = this
99            .project
100            .project_root_path()
101            .await?
102            .join(&this.project.node_root().await?.path)?;
103
104        let mut server_output_assets =
105            all_assets_from_entries_filtered(self.entries(), None, Some(self.ignores()))
106                .await?
107                .iter()
108                .map(async |m| {
109                    base_dir
110                        .get_relative_path_to(&*m.path().await?)
111                        .context("failed to compute relative path for server NFT JSON")
112                })
113                .try_join()
114                .await?;
115
116        // A few hardcoded files (not recursive)
117        server_output_assets.push("./package.json".into());
118
119        let next_dir = get_next_package(this.project.project_path().owned().await?).await?;
120        for ty in ["app-page", "pages"] {
121            let dir = next_dir.join(&format!("dist/server/route-modules/{ty}"))?;
122            let module_path = dir.join("module.compiled.js")?;
123            server_output_assets.push(
124                base_dir
125                    .get_relative_path_to(&module_path)
126                    .context("failed to compute relative path for server NFT JSON")?,
127            );
128
129            let contexts_dir = dir.join("vendored/contexts")?;
130            let DirectoryContent::Entries(contexts_files) = &*contexts_dir.read_dir().await? else {
131                bail!(
132                    "Expected contexts directory to be a directory, found: {:?}",
133                    contexts_dir
134                );
135            };
136            for (_, entry) in contexts_files {
137                let DirectoryEntry::File(file) = entry else {
138                    continue;
139                };
140                if file.extension() == "js" {
141                    server_output_assets.push(
142                        base_dir
143                            .get_relative_path_to(file)
144                            .context("failed to compute relative path for server NFT JSON")?,
145                    )
146                }
147            }
148        }
149
150        server_output_assets.sort();
151        // Dedupe as some entries may be duplicates: a file might be referenced multiple times,
152        // e.g. as a RawModule (from an FS operation) and as an EcmascriptModuleAsset because it
153        // was required.
154        server_output_assets.dedup();
155
156        let json = json!({
157          "version": 1,
158          "files": server_output_assets
159        });
160
161        Ok(AssetContent::file(File::from(json.to_string()).into()))
162    }
163}
164
165#[turbo_tasks::value_impl]
166impl ServerNftJsonAsset {
167    #[turbo_tasks::function]
168    async fn entries(&self) -> Result<Vc<OutputAssets>> {
169        let is_standalone = *self.project.next_config().is_standalone().await?;
170
171        let asset_context = Vc::upcast(externals_tracing_module_context(
172            get_tracing_compile_time_info(),
173        ));
174
175        let project_path = self.project.project_path().owned().await?;
176
177        let next_resolve_origin = Vc::upcast(PlainResolveOrigin::new(
178            asset_context,
179            get_next_package(project_path.clone()).await?.join("_")?,
180        ));
181
182        let cache_handler = self
183            .project
184            .next_config()
185            .cache_handler(project_path.clone())
186            .await?;
187        let cache_handlers = self
188            .project
189            .next_config()
190            .experimental_cache_handlers(project_path.clone())
191            .await?;
192
193        // These are used by packages/next/src/server/require-hook.ts
194        let shared_entries = ["styled-jsx", "styled-jsx/style", "styled-jsx/style.js"];
195
196        let cache_handler_entries = cache_handler
197            .into_iter()
198            .chain(cache_handlers.into_iter())
199            .map(|f| {
200                asset_context
201                    .process(
202                        Vc::upcast(FileSource::new(f.clone())),
203                        ReferenceType::CommonJs(CommonJsReferenceSubType::Undefined),
204                    )
205                    .module()
206            });
207
208        let entries = match self.ty {
209            ServerNftType::Full => Either::Left(
210                if is_standalone {
211                    Either::Left(
212                        [
213                            "next/dist/server/lib/start-server",
214                            "next/dist/server/next",
215                            "next/dist/server/require-hook",
216                        ]
217                        .into_iter(),
218                    )
219                } else {
220                    Either::Right(std::iter::empty())
221                }
222                .chain(std::iter::once("next/dist/server/next-server")),
223            ),
224            ServerNftType::Minimal => Either::Right(std::iter::once(
225                "next/dist/compiled/next-server/server.runtime.prod",
226            )),
227        };
228
229        Ok(Vc::cell(
230            cache_handler_entries
231                .chain(
232                    shared_entries
233                        .into_iter()
234                        .chain(entries)
235                        .map(async |path| {
236                            Ok(cjs_resolve(
237                                next_resolve_origin,
238                                Request::parse_string(path.into()),
239                                CommonJsReferenceSubType::Undefined,
240                                None,
241                                false,
242                            )
243                            .primary_modules()
244                            .await?
245                            .into_iter()
246                            .map(|m| **m))
247                        })
248                        .try_flat_join()
249                        .await?,
250                )
251                .map(|m| Vc::upcast::<Box<dyn OutputAsset>>(TracedAsset::new(m)).to_resolved())
252                .try_join()
253                .await?,
254        ))
255    }
256
257    #[turbo_tasks::function]
258    async fn ignores(&self) -> Result<Vc<Glob>> {
259        let is_standalone = *self.project.next_config().is_standalone().await?;
260        let has_next_support = *self.project.ci_has_next_support().await?;
261        let project_path = self.project.project_path().owned().await?;
262
263        let output_file_tracing_excludes = self
264            .project
265            .next_config()
266            .output_file_tracing_excludes()
267            .await?;
268        let mut additional_ignores = BTreeSet::new();
269        if let Some(output_file_tracing_excludes) = output_file_tracing_excludes
270            .as_ref()
271            .and_then(Value::as_object)
272        {
273            for (glob_pattern, exclude_patterns) in output_file_tracing_excludes {
274                // Check if the route matches the glob pattern
275                let glob = Glob::new(RcStr::from(glob_pattern.clone()), Default::default()).await?;
276                if glob.matches("next-server")
277                    && let Some(patterns) = exclude_patterns.as_array()
278                {
279                    for pattern in patterns {
280                        if let Some(pattern_str) = pattern.as_str() {
281                            let (glob, root) = relativize_glob(pattern_str, project_path.clone())?;
282                            let glob = if root.path.is_empty() {
283                                glob.to_string()
284                            } else {
285                                format!("{root}/{glob}")
286                            };
287                            additional_ignores.insert(glob);
288                        }
289                    }
290                }
291            }
292        }
293
294        let server_ignores_glob = [
295            "**/node_modules/react{,-dom,-server-dom-turbopack}/**/*.development.js",
296            "**/*.d.ts",
297            "**/*.map",
298            "**/next/dist/pages/**/*",
299            "**/next/dist/compiled/next-server/**/*.dev.js",
300            "**/next/dist/compiled/webpack/*",
301            "**/node_modules/webpack5/**/*",
302            "**/next/dist/server/lib/route-resolver*",
303            "**/next/dist/compiled/semver/semver/**/*.js",
304            "**/next/dist/compiled/jest-worker/**/*",
305            // -- The following were added for Turbopack specifically --
306            // 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
307            "**/next/dist/next-devtools/userspace/use-app-dev-rendering-indicator.js",
308            // client/components/app-router.js has a process.env.NODE_ENV guard, but we
309            // can't set that.
310            "**/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js",
311            // server/lib/router-server.js doesn't guard this require:
312            "**/next/dist/server/lib/router-utils/setup-dev-bundler.js",
313            // server/next.js doesn't guard this require
314            "**/next/dist/server/dev/next-dev-server.js",
315            // next/dist/compiled/babel* pulls in this, but we never actually transpile at
316            // deploy-time
317            "**/next/dist/compiled/browserslist/**",
318        ]
319        .into_iter()
320        .chain(additional_ignores.iter().map(|s| s.as_str()))
321        // only ignore image-optimizer code when
322        // this is being handled outside of next-server
323        .chain(if has_next_support {
324            Either::Left(
325                [
326                    "**/node_modules/sharp/**/*",
327                    "**/@img/sharp-libvips*/**/*",
328                    "**/next/dist/server/image-optimizer.js",
329                ]
330                .into_iter(),
331            )
332        } else {
333            Either::Right(std::iter::empty())
334        })
335        .chain(if is_standalone {
336            Either::Left(std::iter::empty())
337        } else {
338            Either::Right(["**/*/next/dist/server/next.js", "**/*/next/dist/bin/next"].into_iter())
339        })
340        .map(|g| Glob::new(g.into(), Default::default()))
341        .collect::<Vec<_>>();
342
343        Ok(match self.ty {
344            ServerNftType::Full => Glob::alternatives(server_ignores_glob),
345            ServerNftType::Minimal => Glob::alternatives(
346                server_ignores_glob
347                    .into_iter()
348                    .chain(
349                        [
350                            "**/next/dist/compiled/edge-runtime/**/*",
351                            "**/next/dist/server/web/sandbox/**/*",
352                            "**/next/dist/server/post-process.js",
353                        ]
354                        .into_iter()
355                        .map(|g| Glob::new(g.into(), Default::default())),
356                    )
357                    .collect(),
358            ),
359        })
360    }
361}