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