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