From 645cba71186d1e5b068b259034061536e2163a88 Mon Sep 17 00:00:00 2001 From: Will Binns-Smith Date: Mon, 20 Jan 2025 15:39:25 -0800 Subject: [PATCH] Production builds: write endpoints all at once This implements a new napi binding, `project.writeAllEndpointsToDisk` which writes chunks for all endpoints. This avoids iterating over each endpoint and calling individual napi bindings, each resulting in a call to `emit()`. While iterating entry points now occurs in Rust, `OutputAssets` are collected in an `IndexSet` to prevent unnecessary `emit()` calls. This also significantly reduces unnecessary calls to the manifest loader from JS. Test Plan: CI --- Cargo.lock | 2 +- crates/napi/src/next_api/endpoint.rs | 59 +++-- crates/napi/src/next_api/project.rs | 217 ++++++++++++++---- crates/napi/src/next_api/utils.rs | 31 ++- crates/next-api/src/app.rs | 16 +- crates/next-api/src/empty.rs | 7 +- crates/next-api/src/instrumentation.rs | 7 +- crates/next-api/src/middleware.rs | 7 +- crates/next-api/src/pages.rs | 32 ++- crates/next-api/src/route.rs | 8 + packages/next/src/build/handle-entrypoints.ts | 215 ++--------------- packages/next/src/build/index.ts | 38 +-- .../next/src/build/swc/generated-native.d.ts | 7 + packages/next/src/build/swc/index.ts | 22 ++ packages/next/src/build/swc/types.ts | 6 + .../next/src/shared/lib/turbopack/utils.ts | 32 +++ 16 files changed, 401 insertions(+), 305 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d54bbd037e0b0..c2a5eb2a794d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4534,7 +4534,7 @@ dependencies = [ "either", "fxhash", "hex", - "indexmap 2.5.0", + "indexmap 2.7.1", "indoc", "lazy_static", "modularize_imports", diff --git a/crates/napi/src/next_api/endpoint.rs b/crates/napi/src/next_api/endpoint.rs index 22d8272d5659b..5b7549fe261af 100644 --- a/crates/napi/src/next_api/endpoint.rs +++ b/crates/napi/src/next_api/endpoint.rs @@ -6,20 +6,16 @@ use next_api::{ paths::ServerPath, route::{ endpoint_server_changed_operation, endpoint_write_to_disk_operation, Endpoint, - EndpointOutputPaths, + EndpointOutputPaths, EndpointRuntime, }, }; use tracing::Instrument; -use turbo_tasks::{get_effects, Completion, Effects, OperationVc, ReadRef, Vc, VcValueType}; -use turbopack_core::{ - diagnostics::PlainDiagnostic, - error::PrettyPrintError, - issue::{IssueSeverity, PlainIssue}, -}; +use turbo_tasks::{Completion, Effects, OperationVc, ReadRef, Vc}; +use turbopack_core::{diagnostics::PlainDiagnostic, error::PrettyPrintError, issue::PlainIssue}; use super::utils::{ - get_diagnostics, get_issues, subscribe, NapiDiagnostic, NapiIssue, RootTask, TurbopackResult, - VcArc, + strongly_consistent_catch_collectables, subscribe, NapiDiagnostic, NapiIssue, RootTask, + TurbopackResult, VcArc, }; #[napi(object)] @@ -99,30 +95,6 @@ impl Deref for ExternalEndpoint { } } -// Await the source and return fatal issues if there are any, otherwise -// propagate any actual error results. -async fn strongly_consistent_catch_collectables( - source_op: OperationVc, -) -> Result<( - Option>, - Arc>>, - Arc>>, - Arc, -)> { - let result = source_op.read_strongly_consistent().await; - let issues = get_issues(source_op).await?; - let diagnostics = get_diagnostics(source_op).await?; - let effects = Arc::new(get_effects(source_op).await?); - - let result = if result.is_err() && issues.iter().any(|i| i.severity <= IssueSeverity::Error) { - None - } else { - Some(result?) - }; - - Ok((result, issues, diagnostics, effects)) -} - #[turbo_tasks::value(serialization = "none")] struct WrittenEndpointWithIssues { written: Option>, @@ -147,6 +119,27 @@ async fn get_written_endpoint_with_issues_operation( .cell()) } +#[napi] +#[tracing::instrument(skip_all)] +pub async fn endpoint_runtime( + #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External, +) -> napi::Result { + let turbo_tasks = endpoint.turbo_tasks().clone(); + let runtime = turbo_tasks + .run_once(async move { + let e = ***endpoint; + let e = e.connect(); + Ok(*e.runtime().await?.clone()) + }) + .await + .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?; + + Ok(match runtime { + EndpointRuntime::NodeJs => "nodejs".into(), + EndpointRuntime::Edge => "edge".into(), + }) +} + #[napi] #[tracing::instrument(skip_all)] pub async fn endpoint_write_to_disk( diff --git a/crates/napi/src/next_api/project.rs b/crates/napi/src/next_api/project.rs index 03b25122136c4..538312a36f861 100644 --- a/crates/napi/src/next_api/project.rs +++ b/crates/napi/src/next_api/project.rs @@ -1,6 +1,7 @@ use std::{path::PathBuf, sync::Arc, thread, time::Duration}; use anyhow::{anyhow, bail, Context, Result}; +use indexmap::IndexSet; use napi::{ bindgen_prelude::{within_runtime_if_available, External}, threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, @@ -15,7 +16,7 @@ use next_api::{ DefineEnv, DraftModeOptions, PartialProjectOptions, Project, ProjectContainer, ProjectOptions, WatchOptions, }, - route::Endpoint, + route::{Endpoint, Route}, }; use next_core::tracing_presets::{ TRACING_NEXT_OVERVIEW_TARGETS, TRACING_NEXT_TARGETS, TRACING_NEXT_TURBOPACK_TARGETS, @@ -39,6 +40,7 @@ use turbopack_core::{ diagnostics::PlainDiagnostic, error::PrettyPrintError, issue::PlainIssue, + output::{OutputAsset, OutputAssets}, source_map::{SourceMap, Token}, version::{PartialUpdate, TotalUpdate, Update, VersionState}, SOURCE_MAP_PREFIX, @@ -539,7 +541,7 @@ pub async fn project_shutdown( #[napi(object)] #[derive(Default)] -struct AppPageNapiRoute { +pub struct AppPageNapiRoute { /// The relative path from project_path to the route file pub original_name: Option, @@ -549,7 +551,7 @@ struct AppPageNapiRoute { #[napi(object)] #[derive(Default)] -struct NapiRoute { +pub struct NapiRoute { /// The router path pub pathname: String, /// The relative path from project_path to the route file @@ -627,7 +629,7 @@ impl NapiRoute { } #[napi(object)] -struct NapiMiddleware { +pub struct NapiMiddleware { pub endpoint: External, } @@ -643,7 +645,7 @@ impl NapiMiddleware { } #[napi(object)] -struct NapiInstrumentation { +pub struct NapiInstrumentation { pub node_js: External, pub edge: External, } @@ -667,7 +669,7 @@ impl NapiInstrumentation { } #[napi(object)] -struct NapiEntrypoints { +pub struct NapiEntrypoints { pub routes: Vec, pub middleware: Option, pub instrumentation: Option, @@ -676,6 +678,49 @@ struct NapiEntrypoints { pub pages_error_endpoint: External, } +impl NapiEntrypoints { + fn from_entrypoints_op( + entrypoints: &EntrypointsOperation, + turbo_tasks: &NextTurboTasks, + ) -> Result { + let routes = entrypoints + .routes + .iter() + .map(|(k, v)| NapiRoute::from_route(k.to_string(), v.clone(), turbo_tasks)) + .collect(); + let middleware = entrypoints + .middleware + .as_ref() + .map(|m| NapiMiddleware::from_middleware(m, turbo_tasks)) + .transpose()?; + let instrumentation = entrypoints + .instrumentation + .as_ref() + .map(|i| NapiInstrumentation::from_instrumentation(i, turbo_tasks)) + .transpose()?; + let pages_document_endpoint = External::new(ExternalEndpoint(VcArc::new( + turbo_tasks.clone(), + entrypoints.pages_document_endpoint, + ))); + let pages_app_endpoint = External::new(ExternalEndpoint(VcArc::new( + turbo_tasks.clone(), + entrypoints.pages_app_endpoint, + ))); + let pages_error_endpoint = External::new(ExternalEndpoint(VcArc::new( + turbo_tasks.clone(), + entrypoints.pages_error_endpoint, + ))); + Ok(NapiEntrypoints { + routes, + middleware, + instrumentation, + pages_document_endpoint, + pages_app_endpoint, + pages_error_endpoint, + }) + } +} + #[turbo_tasks::value(serialization = "none")] struct EntrypointsWithIssues { entrypoints: ReadRef, @@ -712,6 +757,130 @@ fn project_container_entrypoints_operation( container.entrypoints() } +#[turbo_tasks::value(serialization = "none")] +struct AllWrittenEntrypointsWithIssues { + entrypoints: Option>, + issues: Arc>>, + diagnostics: Arc>>, + effects: Arc, +} + +#[napi] +pub async fn project_write_all_entrypoints_to_disk( + #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External, + app_dir_only: bool, +) -> napi::Result> { + let turbo_tasks = project.turbo_tasks.clone(); + let (entrypoints, issues, diags) = turbo_tasks + .run_once(async move { + let entrypoints_with_issues_op = get_all_written_entrypoints_with_issues_operation( + project.container.to_resolved().await?, + ResolvedVc::cell(app_dir_only), + ); + + let EntrypointsWithIssues { + entrypoints, + issues, + diagnostics, + effects, + } = &*entrypoints_with_issues_op + .read_strongly_consistent() + .await?; + effects.apply().await?; + + Ok((entrypoints.clone(), issues.clone(), diagnostics.clone())) + }) + .await + .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?; + + Ok(TurbopackResult { + result: NapiEntrypoints::from_entrypoints_op(&entrypoints, &turbo_tasks)?, + issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(), + diagnostics: diags.iter().map(|d| NapiDiagnostic::from(d)).collect(), + }) +} + +#[turbo_tasks::function(operation)] +async fn get_all_written_entrypoints_with_issues_operation( + container: ResolvedVc, + app_dir_only: ResolvedVc, +) -> Result> { + let entrypoints_operation = EntrypointsOperation::new(all_entrypoints_write_to_disk_operation( + container, + app_dir_only, + )); + let entrypoints = entrypoints_operation.read_strongly_consistent().await?; + let issues = get_issues(entrypoints_operation).await?; + let diagnostics = get_diagnostics(entrypoints_operation).await?; + let effects = Arc::new(get_effects(entrypoints_operation).await?); + Ok(EntrypointsWithIssues { + entrypoints, + issues, + diagnostics, + effects, + } + .cell()) +} + +#[turbo_tasks::function(operation)] +pub async fn all_entrypoints_write_to_disk_operation( + project: ResolvedVc, + app_dir_only: ResolvedVc, +) -> Result> { + let _ = project + .project() + .emit_all_output_assets(output_assets_operation(project, app_dir_only)) + .resolve() + .await?; + + Ok(project.entrypoints()) +} + +#[turbo_tasks::function(operation)] +async fn output_assets_operation( + project: ResolvedVc, + app_dir_only: ResolvedVc, +) -> Result> { + let mut output_assets: IndexSet>> = IndexSet::new(); + let app_dir_only = *app_dir_only.await?; + + let entrypoints = &*project.entrypoints().await?; + for route in entrypoints.routes.values() { + match route { + Route::Page { + html_endpoint, + data_endpoint, + } => { + if app_dir_only { + continue; + } + + output_assets.extend(html_endpoint.output().await?.output_assets.await?); + output_assets.extend(data_endpoint.output().await?.output_assets.await?); + } + Route::PageApi { endpoint } => { + if app_dir_only { + continue; + } + + output_assets.extend(endpoint.output().await?.output_assets.await?); + } + Route::AppPage(pages) => { + for page in pages { + output_assets.extend(page.html_endpoint.output().await?.output_assets.await?); + output_assets.extend(page.html_endpoint.output().await?.output_assets.await?); + } + } + Route::AppRoute { endpoint, .. } => { + output_assets.extend(endpoint.output().await?.output_assets.await?); + } + Route::Conflict => {} + } + } + + Ok(Vc::cell(output_assets.iter().copied().collect())) +} + #[napi(ts_return_type = "{ __napiType: \"RootTask\" }")] pub fn project_entrypoints_subscribe( #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External, @@ -743,41 +912,7 @@ pub fn project_entrypoints_subscribe( let (entrypoints, issues, diags) = ctx.value; Ok(vec![TurbopackResult { - result: NapiEntrypoints { - routes: entrypoints - .routes - .iter() - .map(|(pathname, route)| { - NapiRoute::from_route( - pathname.clone().into(), - route.clone(), - &turbo_tasks, - ) - }) - .collect::>(), - middleware: entrypoints - .middleware - .as_ref() - .map(|m| NapiMiddleware::from_middleware(m, &turbo_tasks)) - .transpose()?, - instrumentation: entrypoints - .instrumentation - .as_ref() - .map(|m| NapiInstrumentation::from_instrumentation(m, &turbo_tasks)) - .transpose()?, - pages_document_endpoint: External::new(ExternalEndpoint(VcArc::new( - turbo_tasks.clone(), - entrypoints.pages_document_endpoint, - ))), - pages_app_endpoint: External::new(ExternalEndpoint(VcArc::new( - turbo_tasks.clone(), - entrypoints.pages_app_endpoint, - ))), - pages_error_endpoint: External::new(ExternalEndpoint(VcArc::new( - turbo_tasks.clone(), - entrypoints.pages_error_endpoint, - ))), - }, + result: NapiEntrypoints::from_entrypoints_op(&*entrypoints, &turbo_tasks)?, issues: issues .iter() .map(|issue| NapiIssue::from(&**issue)) diff --git a/crates/napi/src/next_api/utils.rs b/crates/napi/src/next_api/utils.rs index 0d19fcfda94d9..1312c12b20a41 100644 --- a/crates/napi/src/next_api/utils.rs +++ b/crates/napi/src/next_api/utils.rs @@ -10,7 +10,8 @@ use napi::{ }; use serde::Serialize; use turbo_tasks::{ - trace::TraceRawVcs, OperationVc, ReadRef, TaskId, TryJoinIterExt, TurboTasks, UpdateInfo, Vc, + get_effects, trace::TraceRawVcs, Effects, OperationVc, ReadRef, TaskId, TryJoinIterExt, + TurboTasks, UpdateInfo, Vc, VcValueType, }; use turbo_tasks_backend::{ default_backing_storage, noop_backing_storage, DefaultBackingStorage, NoopBackingStorage, @@ -19,7 +20,9 @@ use turbo_tasks_fs::FileContent; use turbopack_core::{ diagnostics::{Diagnostic, DiagnosticContextExt, PlainDiagnostic}, error::PrettyPrintError, - issue::{IssueDescriptionExt, PlainIssue, PlainIssueSource, PlainSource, StyledString}, + issue::{ + IssueDescriptionExt, IssueSeverity, PlainIssue, PlainIssueSource, PlainSource, StyledString, + }, source_pos::SourcePos, }; @@ -492,3 +495,27 @@ pub fn subscribe> + Send, task_id: Some(task_id), })) } + +// Await the source and return fatal issues if there are any, otherwise +// propagate any actual error results. +pub async fn strongly_consistent_catch_collectables( + source_op: OperationVc, +) -> Result<( + Option>, + Arc>>, + Arc>>, + Arc, +)> { + let result = source_op.read_strongly_consistent().await; + let issues = get_issues(source_op).await?; + let diagnostics = get_diagnostics(source_op).await?; + let effects = Arc::new(get_effects(source_op).await?); + + let result = if result.is_err() && issues.iter().any(|i| i.severity <= IssueSeverity::Error) { + None + } else { + Some(result?) + }; + + Ok((result, issues, diagnostics, effects)) +} diff --git a/crates/next-api/src/app.rs b/crates/next-api/src/app.rs index 7413de5e7ae40..4e99601f19576 100644 --- a/crates/next-api/src/app.rs +++ b/crates/next-api/src/app.rs @@ -82,7 +82,9 @@ use crate::{ get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings, }, project::{ModuleGraphs, Project}, - route::{AppPageRoute, Endpoint, EndpointOutput, EndpointOutputPaths, Route, Routes}, + route::{ + AppPageRoute, Endpoint, EndpointOutput, EndpointOutputPaths, EndpointRuntime, Route, Routes, + }, server_actions::{build_server_actions_loader, create_server_actions_manifest}, webpack_stats::generate_webpack_stats, }; @@ -1792,6 +1794,18 @@ async fn create_app_paths_manifest( #[turbo_tasks::value_impl] impl Endpoint for AppEndpoint { + #[turbo_tasks::function] + async fn runtime(self: ResolvedVc) -> Result> { + let app_entry = self.app_endpoint_entry().await?; + let runtime = app_entry.config.await?.runtime.unwrap_or_default(); + + Ok(match runtime { + NextRuntime::Edge => EndpointRuntime::Edge, + NextRuntime::NodeJs => EndpointRuntime::NodeJs, + } + .cell()) + } + #[turbo_tasks::function] async fn output(self: ResolvedVc) -> Result> { let this = self.await?; diff --git a/crates/next-api/src/empty.rs b/crates/next-api/src/empty.rs index ed3be88094880..deee5f56c9461 100644 --- a/crates/next-api/src/empty.rs +++ b/crates/next-api/src/empty.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Result}; use turbo_tasks::{Completion, Vc}; use turbopack_core::module::Modules; -use crate::route::{Endpoint, EndpointOutput}; +use crate::route::{Endpoint, EndpointOutput, EndpointRuntime}; #[turbo_tasks::value] pub struct EmptyEndpoint; @@ -17,6 +17,11 @@ impl EmptyEndpoint { #[turbo_tasks::value_impl] impl Endpoint for EmptyEndpoint { + #[turbo_tasks::function] + fn runtime(self: Vc) -> Result> { + bail!("Empty endpoint can't have runtime") + } + #[turbo_tasks::function] fn output(self: Vc) -> Result> { bail!("Empty endpoint can't have output") diff --git a/crates/next-api/src/instrumentation.rs b/crates/next-api/src/instrumentation.rs index f225854b3608d..7aa7b01127bb9 100644 --- a/crates/next-api/src/instrumentation.rs +++ b/crates/next-api/src/instrumentation.rs @@ -30,7 +30,7 @@ use crate::{ all_server_paths, get_js_paths_from_root, get_wasm_paths_from_root, wasm_paths_to_bindings, }, project::Project, - route::{Endpoint, EndpointOutput, EndpointOutputPaths}, + route::{Endpoint, EndpointOutput, EndpointOutputPaths, EndpointRuntime}, }; #[turbo_tasks::value] @@ -248,6 +248,11 @@ struct InstrumentationCoreModules { #[turbo_tasks::value_impl] impl Endpoint for InstrumentationEndpoint { + #[turbo_tasks::function] + async fn runtime(self: ResolvedVc) -> Vc { + EndpointRuntime::Edge.cell() + } + #[turbo_tasks::function] async fn output(self: ResolvedVc) -> Result> { let span = tracing::info_span!("instrumentation endpoint"); diff --git a/crates/next-api/src/middleware.rs b/crates/next-api/src/middleware.rs index 6d09104acc8fb..16eda06cd46ba 100644 --- a/crates/next-api/src/middleware.rs +++ b/crates/next-api/src/middleware.rs @@ -31,7 +31,7 @@ use crate::{ get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings, }, project::Project, - route::{Endpoint, EndpointOutput, EndpointOutputPaths}, + route::{Endpoint, EndpointOutput, EndpointOutputPaths, EndpointRuntime}, }; #[turbo_tasks::value] @@ -276,6 +276,11 @@ impl MiddlewareEndpoint { #[turbo_tasks::value_impl] impl Endpoint for MiddlewareEndpoint { + #[turbo_tasks::function] + async fn runtime(self: ResolvedVc) -> Vc { + EndpointRuntime::Edge.cell() + } + #[turbo_tasks::function] async fn output(self: ResolvedVc) -> Result> { let span = tracing::info_span!("middleware endpoint"); diff --git a/crates/next-api/src/pages.rs b/crates/next-api/src/pages.rs index e4ddb86b74ce2..8c7125fe47de8 100644 --- a/crates/next-api/src/pages.rs +++ b/crates/next-api/src/pages.rs @@ -77,7 +77,7 @@ use crate::{ get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings, }, project::Project, - route::{Endpoint, EndpointOutput, EndpointOutputPaths, Route, Routes}, + route::{Endpoint, EndpointOutput, EndpointOutputPaths, EndpointRuntime, Route, Routes}, webpack_stats::generate_webpack_stats, }; @@ -1373,6 +1373,36 @@ pub struct InternalSsrChunkModule { #[turbo_tasks::value_impl] impl Endpoint for PageEndpoint { + #[turbo_tasks::function] + async fn runtime(self: Vc) -> Result> { + let this = &*self.await?; + let (reference_type, module_context) = match this.ty { + PageEndpointType::Html | PageEndpointType::SsrOnly => ( + Value::new(ReferenceType::Entry(EntryReferenceSubType::Page)), + this.pages_project.ssr_module_context(), + ), + PageEndpointType::Data => ( + Value::new(ReferenceType::Entry(EntryReferenceSubType::Page)), + this.pages_project.ssr_data_module_context(), + ), + PageEndpointType::Api => ( + Value::new(ReferenceType::Entry(EntryReferenceSubType::PagesApi)), + this.pages_project.api_module_context(), + ), + }; + + let ssr_module = module_context + .process(self.source(), reference_type.clone()) + .module(); + + let config = parse_config_from_source(ssr_module).await?; + Ok(match config.runtime { + NextRuntime::NodeJs => EndpointRuntime::NodeJs, + NextRuntime::Edge => EndpointRuntime::Edge, + } + .cell()) + } + #[turbo_tasks::function] async fn output(self: ResolvedVc) -> Result> { let this = self.await?; diff --git a/crates/next-api/src/route.rs b/crates/next-api/src/route.rs index b671433da7e69..4430c33c32426 100644 --- a/crates/next-api/src/route.rs +++ b/crates/next-api/src/route.rs @@ -44,8 +44,16 @@ pub enum Route { Conflict, } +#[derive(Copy, Clone)] +#[turbo_tasks::value(shared)] +pub enum EndpointRuntime { + NodeJs, + Edge, +} + #[turbo_tasks::value_trait] pub trait Endpoint { + fn runtime(self: Vc) -> Vc; fn output(self: Vc) -> Vc; // fn write_to_disk(self: Vc) -> Vc; fn server_changed(self: Vc) -> Vc; diff --git a/packages/next/src/build/handle-entrypoints.ts b/packages/next/src/build/handle-entrypoints.ts index adcac7d45049f..696e2b312fe02 100644 --- a/packages/next/src/build/handle-entrypoints.ts +++ b/packages/next/src/build/handle-entrypoints.ts @@ -1,93 +1,30 @@ import type { CustomRoutes } from '../lib/load-custom-routes' import type { TurbopackManifestLoader } from '../shared/lib/turbopack/manifest-loader' import type { - TurbopackResult, - RawEntrypoints, Entrypoints, PageRoute, AppRoute, + RawEntrypoints, } from './swc/types' -import * as Log from './output/log' import { getEntryKey } from '../shared/lib/turbopack/entry-key' -import { - processIssues, - type EntryIssuesMap, -} from '../shared/lib/turbopack/utils' -export async function handleEntrypoints({ - entrypoints, - currentEntrypoints, - currentEntryIssues, - manifestLoader, - productionRewrites, - logErrors, -}: { - entrypoints: TurbopackResult - currentEntrypoints: Entrypoints - currentEntryIssues: EntryIssuesMap - manifestLoader: TurbopackManifestLoader +export async function handleEntrypoints( + entrypointsOp: RawEntrypoints, + manifestLoader: TurbopackManifestLoader, productionRewrites: CustomRoutes['rewrites'] | undefined - logErrors: boolean -}) { - currentEntrypoints.global.app = entrypoints.pagesAppEndpoint - currentEntrypoints.global.document = entrypoints.pagesDocumentEndpoint - currentEntrypoints.global.error = entrypoints.pagesErrorEndpoint - - currentEntrypoints.global.instrumentation = entrypoints.instrumentation - - currentEntrypoints.page.clear() - currentEntrypoints.app.clear() - - for (const [pathname, route] of entrypoints.routes) { - switch (route.type) { - case 'page': - case 'page-api': - currentEntrypoints.page.set(pathname, route) - break - case 'app-page': { - route.pages.forEach((page) => { - currentEntrypoints.app.set(page.originalName, { - type: 'app-page', - ...page, - }) - }) - break - } - case 'app-route': { - currentEntrypoints.app.set(route.originalName, route) - break - } - default: - Log.info(`skipping ${pathname} (${route.type})`) - break - } - } - - const { middleware, instrumentation } = entrypoints - - // We check for explicit true/false, since it's initialized to - // undefined during the first loop (middlewareChanges event is - // unnecessary during the first serve) - if (currentEntrypoints.global.middleware && !middleware) { - const key = getEntryKey('root', 'server', 'middleware') - // Went from middleware to no middleware - currentEntryIssues.delete(key) - } - - currentEntrypoints.global.middleware = middleware +): Promise { + const { middleware, instrumentation } = entrypointsOp + + const entrypoints = { + global: { + app: entrypointsOp.pagesAppEndpoint, + document: entrypointsOp.pagesDocumentEndpoint, + error: entrypointsOp.pagesErrorEndpoint, + instrumentation: entrypointsOp.instrumentation, + }, + } as Entrypoints if (instrumentation) { - const processInstrumentation = async ( - name: string, - prop: 'nodeJs' | 'edge' - ) => { - const key = getEntryKey('root', 'server', name) - - const writtenEndpoint = await instrumentation[prop].writeToDisk() - processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) - } - await processInstrumentation('instrumentation.nodeJs', 'nodeJs') - await processInstrumentation('instrumentation.edge', 'edge') await manifestLoader.loadMiddlewareManifest( 'instrumentation', 'instrumentation' @@ -95,63 +32,30 @@ export async function handleEntrypoints({ await manifestLoader.writeManifests({ devRewrites: undefined, productionRewrites, - entrypoints: currentEntrypoints, + entrypoints, }) } if (middleware) { - const key = getEntryKey('root', 'server', 'middleware') - - const endpoint = middleware.endpoint - - async function processMiddleware() { - const writtenEndpoint = await endpoint.writeToDisk() - processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) - await manifestLoader.loadMiddlewareManifest('middleware', 'middleware') - } - await processMiddleware() - } else { - manifestLoader.deleteMiddlewareManifest( - getEntryKey('root', 'server', 'middleware') - ) + await manifestLoader.loadMiddlewareManifest('middleware', 'middleware') } } export async function handlePagesErrorRoute({ - currentEntryIssues, entrypoints, manifestLoader, productionRewrites, - logErrors, }: { - currentEntryIssues: EntryIssuesMap entrypoints: Entrypoints manifestLoader: TurbopackManifestLoader productionRewrites: CustomRoutes['rewrites'] | undefined - logErrors: boolean }) { - if (entrypoints.global.app) { - const key = getEntryKey('pages', 'server', '_app') - const writtenEndpoint = await entrypoints.global.app.writeToDisk() - processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) - } await manifestLoader.loadBuildManifest('_app') await manifestLoader.loadPagesManifest('_app') await manifestLoader.loadFontManifest('_app') - if (entrypoints.global.document) { - const key = getEntryKey('pages', 'server', '_document') - const writtenEndpoint = await entrypoints.global.document.writeToDisk() - processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) - } await manifestLoader.loadPagesManifest('_document') - if (entrypoints.global.error) { - const key = getEntryKey('pages', 'server', '_error') - const writtenEndpoint = await entrypoints.global.error.writeToDisk() - processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) - } - await manifestLoader.loadBuildManifest('_error') await manifestLoader.loadPagesManifest('_error') await manifestLoader.loadFontManifest('_error') @@ -166,20 +70,11 @@ export async function handlePagesErrorRoute({ export async function handleRouteType({ page, route, - currentEntryIssues, - entrypoints, manifestLoader, - productionRewrites, - logErrors, }: { page: string route: PageRoute | AppRoute - - currentEntryIssues: EntryIssuesMap - entrypoints: Entrypoints manifestLoader: TurbopackManifestLoader - productionRewrites: CustomRoutes['rewrites'] | undefined - logErrors: boolean }) { const shouldCreateWebpackStats = process.env.TURBOPACK_STATS != null @@ -187,38 +82,11 @@ export async function handleRouteType({ case 'page': { const serverKey = getEntryKey('pages', 'server', page) - if (entrypoints.global.app) { - const key = getEntryKey('pages', 'server', '_app') - - const writtenEndpoint = await entrypoints.global.app.writeToDisk() - processIssues( - currentEntryIssues, - key, - writtenEndpoint, - false, - logErrors - ) - } await manifestLoader.loadBuildManifest('_app') await manifestLoader.loadPagesManifest('_app') - - if (entrypoints.global.document) { - const key = getEntryKey('pages', 'server', '_document') - - const writtenEndpoint = await entrypoints.global.document.writeToDisk() - processIssues( - currentEntryIssues, - key, - writtenEndpoint, - false, - logErrors - ) - } await manifestLoader.loadPagesManifest('_document') - const writtenEndpoint = await route.htmlEndpoint.writeToDisk() - - const type = writtenEndpoint?.type + const type = await route.htmlEndpoint.runtime() await manifestLoader.loadBuildManifest(page) await manifestLoader.loadPagesManifest(page) @@ -234,28 +102,12 @@ export async function handleRouteType({ await manifestLoader.loadWebpackStats(page, 'pages') } - await manifestLoader.writeManifests({ - devRewrites: undefined, - productionRewrites, - entrypoints, - }) - - processIssues( - currentEntryIssues, - serverKey, - writtenEndpoint, - false, - logErrors - ) - break } case 'page-api': { const key = getEntryKey('pages', 'server', page) - const writtenEndpoint = await route.endpoint.writeToDisk() - - const type = writtenEndpoint.type + const type = await route.endpoint.runtime() await manifestLoader.loadPagesManifest(page) if (type === 'edge') { @@ -264,20 +116,11 @@ export async function handleRouteType({ manifestLoader.deleteMiddlewareManifest(key) } - await manifestLoader.writeManifests({ - devRewrites: undefined, - productionRewrites, - entrypoints, - }) - - processIssues(currentEntryIssues, key, writtenEndpoint, true, logErrors) - break } case 'app-page': { const key = getEntryKey('app', 'server', page) - const writtenEndpoint = await route.htmlEndpoint.writeToDisk() - const type = writtenEndpoint.type + const type = await route.htmlEndpoint.runtime() if (type === 'edge') { await manifestLoader.loadMiddlewareManifest(page, 'app') @@ -295,20 +138,11 @@ export async function handleRouteType({ await manifestLoader.loadWebpackStats(page, 'app') } - await manifestLoader.writeManifests({ - devRewrites: undefined, - productionRewrites, - entrypoints, - }) - - processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) - break } case 'app-route': { const key = getEntryKey('app', 'server', page) - const writtenEndpoint = await route.endpoint.writeToDisk() - const type = writtenEndpoint.type + const type = await route.endpoint.runtime() await manifestLoader.loadAppPathsManifest(page) @@ -318,13 +152,6 @@ export async function handleRouteType({ manifestLoader.deleteMiddlewareManifest(key) } - await manifestLoader.writeManifests({ - devRewrites: undefined, - productionRewrites, - entrypoints, - }) - processIssues(currentEntryIssues, key, writtenEndpoint, true, logErrors) - break } default: { diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index e5299fc7bfefe..509d622ee5d49 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -218,7 +218,7 @@ import { getTurbopackJsConfig, isPersistentCachingEnabled, isRelevantWarning, - type EntryIssuesMap, + processIssuesForProd, } from '../shared/lib/turbopack/utils' type Fallback = null | boolean | string @@ -1437,8 +1437,7 @@ export default async function build( ) ) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const entrypointsSubscription = project.entrypointsSubscribe() + const entrypoints = await project.writeAllEntrypointsToDisk(appDirOnly) const currentEntrypoints: Entrypoints = { global: { app: undefined, @@ -1453,22 +1452,12 @@ export default async function build( page: new Map(), } - const currentEntryIssues: EntryIssuesMap = new Map() - const manifestLoader = new TurbopackManifestLoader({ buildId, distDir, encryptionKey, }) - const entrypointsResult = await entrypointsSubscription.next() - if (entrypointsResult.done) { - throw new Error('Turbopack did not return any entrypoints') - } - entrypointsSubscription.return?.().catch(() => {}) - - const entrypoints = entrypointsResult.value - const topLevelErrors: { message: string }[] = [] @@ -1486,14 +1475,15 @@ export default async function build( ) } - await handleEntrypoints({ + const currentEntryIssues = new Map() + + processIssuesForProd(entrypoints, true, false) + + await handleEntrypoints( entrypoints, - currentEntrypoints, - currentEntryIssues, manifestLoader, - productionRewrites: customRoutes.rewrites, - logErrors: false, - }) + customRoutes.rewrites + ) const progress = createProgress( currentEntrypoints.page.size + currentEntrypoints.app.size + 1, @@ -1533,11 +1523,7 @@ export default async function build( handleRouteType({ page, route, - currentEntryIssues, - entrypoints: currentEntrypoints, manifestLoader, - productionRewrites: customRoutes.rewrites, - logErrors: false, }) ) } @@ -1548,22 +1534,16 @@ export default async function build( handleRouteType({ page, route, - currentEntryIssues, - entrypoints: currentEntrypoints, manifestLoader, - productionRewrites: customRoutes.rewrites, - logErrors: false, }) ) } enqueue(() => handlePagesErrorRoute({ - currentEntryIssues, entrypoints: currentEntrypoints, manifestLoader, productionRewrites: customRoutes.rewrites, - logErrors: false, }) ) await Promise.all(promises) diff --git a/packages/next/src/build/swc/generated-native.d.ts b/packages/next/src/build/swc/generated-native.d.ts index a9e6aab2fded8..bf42063c09ca8 100644 --- a/packages/next/src/build/swc/generated-native.d.ts +++ b/packages/next/src/build/swc/generated-native.d.ts @@ -62,6 +62,9 @@ export interface NapiWrittenEndpoint { serverPaths: Array config: NapiEndpointConfig } +export declare function endpointRuntime(endpoint: { + __napiType: 'Endpoint' +}): Promise export declare function endpointWriteToDisk(endpoint: { __napiType: 'Endpoint' }): Promise @@ -239,6 +242,10 @@ export interface NapiEntrypoints { pagesAppEndpoint: ExternalObject pagesErrorEndpoint: ExternalObject } +export declare function projectWriteAllEntrypointsToDisk( + project: { __napiType: 'Project' }, + appDirOnly: boolean +): Promise export declare function projectEntrypointsSubscribe( project: { __napiType: 'Project' }, func: (...args: any[]) => any diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index 58e17490b974c..c888e74ebd761 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -32,6 +32,7 @@ import type { HmrIdentifiers, Project, ProjectOptions, + RawEntrypoints, Route, TurboEngineOptions, TurbopackResult, @@ -586,6 +587,18 @@ function bindingToApi( ) } + async writeAllEntrypointsToDisk( + appDirOnly: boolean + ): Promise> { + return await withErrorCause( + () => + binding.projectWriteAllEntrypointsToDisk( + this._nativeProject, + appDirOnly + ) as Promise> + ) + } + entrypointsSubscribe() { type NapiEndpoint = { __napiType: 'Endpoint' } @@ -802,6 +815,15 @@ function bindingToApi( ) } + async runtime(): Promise<'nodejs' | 'edge'> { + return await withErrorCause( + () => + binding.endpointRuntime(this._nativeEndpoint) as Promise< + TurbopackResult<'nodejs' | 'edge'> + > + ) + } + async clientChanged(): Promise>> { const clientSubscription = subscribe( false, diff --git a/packages/next/src/build/swc/types.ts b/packages/next/src/build/swc/types.ts index 8327e8bb5bb86..6a0a1e0cc37b2 100644 --- a/packages/next/src/build/swc/types.ts +++ b/packages/next/src/build/swc/types.ts @@ -192,6 +192,10 @@ export interface UpdateInfo { export interface Project { update(options: Partial): Promise + writeAllEntrypointsToDisk( + appDirOnly: boolean + ): Promise> + entrypointsSubscribe(): AsyncIterableIterator> hmrEvents(identifier: string): AsyncIterableIterator> @@ -247,6 +251,8 @@ export type Route = } export interface Endpoint { + runtime(): Promise<'nodejs' | 'edge'> + /** Write files for the endpoint to disk. */ writeToDisk(): Promise> diff --git a/packages/next/src/shared/lib/turbopack/utils.ts b/packages/next/src/shared/lib/turbopack/utils.ts index e200f4a5d4d55..025fea5c5231b 100644 --- a/packages/next/src/shared/lib/turbopack/utils.ts +++ b/packages/next/src/shared/lib/turbopack/utils.ts @@ -61,6 +61,38 @@ export async function getTurbopackJsConfig( return jsConfig ?? { compilerOptions: {} } } +export function processIssuesForProd( + result: TurbopackResult, + throwIssue: boolean, + logErrors:boolean +) { + const relevantIssues = new Set() + for (const issue of result.issues) { + if ( + issue.severity !== 'error' && + issue.severity !== 'fatal' && + issue.severity !== 'warning' + ) + continue + + if (issue.severity !== 'warning') { + if (throwIssue) { + const formatted = formatIssue(issue) + relevantIssues.add(formatted) + } + // if we throw the issue it will most likely get handed and logged elsewhere + else if (logErrors && isWellKnownError(issue)) { + const formatted = formatIssue(issue) + Log.error(formatted) + } + } + } + + if (relevantIssues.size && throwIssue) { + throw new ModuleBuildError([...relevantIssues].join('\n\n')) + } +} + export function processIssues( currentEntryIssues: EntryIssuesMap,