diff --git a/packages/next-swc/crates/napi/src/next_api/project.rs b/packages/next-swc/crates/napi/src/next_api/project.rs index 3b94a6afbd345..3e91939b76b23 100644 --- a/packages/next-swc/crates/napi/src/next_api/project.rs +++ b/packages/next-swc/crates/napi/src/next_api/project.rs @@ -37,8 +37,8 @@ use turbopack_binding::{ use super::{ endpoint::ExternalEndpoint, utils::{ - get_diagnostics, get_issues, serde_enum_to_string, subscribe, NapiDiagnostic, NapiIssue, - RootTask, TurbopackResult, VcArc, + get_diagnostics, get_issues, subscribe, NapiDiagnostic, NapiIssue, RootTask, + TurbopackResult, VcArc, }, }; use crate::register; @@ -265,9 +265,7 @@ impl NapiRoute { #[napi(object)] struct NapiMiddleware { - pub endpoint: External>>>, - pub runtime: String, - pub matcher: Option>, + pub endpoint: External, } impl NapiMiddleware { @@ -276,9 +274,10 @@ impl NapiMiddleware { turbo_tasks: &Arc>, ) -> Result { Ok(NapiMiddleware { - endpoint: External::new(VcArc::new(turbo_tasks.clone(), value.endpoint)), - runtime: serde_enum_to_string(&value.config.runtime)?, - matcher: value.config.matcher.clone(), + endpoint: External::new(ExternalEndpoint(VcArc::new( + turbo_tasks.clone(), + value.endpoint, + ))), }) } } diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index 4a365f9e7e4a0..e4b2ef1d7ba21 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -27,7 +27,7 @@ use next_core::{ get_server_module_options_context, get_server_resolve_options_context, get_server_runtime_entries, ServerContextType, }, - util::NextRuntime, + util::{get_asset_prefix_from_pathname, NextRuntime}, }; use serde::{Deserialize, Serialize}; use turbo_tasks::{trace::TraceRawVcs, Completion, TryFlatJoinIterExt, TryJoinIterExt, Value, Vc}; @@ -700,13 +700,13 @@ impl AppEndpoint { // TODO(alexkirsz) This should be shared with next build. let named_regex = get_named_middleware_regex(&app_entry.pathname); let matchers = MiddlewareMatcher { - regexp: named_regex, + regexp: Some(named_regex), original_source: app_entry.pathname.clone(), ..Default::default() }; let edge_function_definition = EdgeFunctionDefinition { files: files_paths_from_root, - name: app_entry.original_name.to_string(), + name: app_entry.pathname.to_string(), page: app_entry.original_name.clone(), regions: app_entry .config @@ -718,16 +718,16 @@ impl AppEndpoint { ..Default::default() }; let middleware_manifest_v2 = MiddlewaresManifestV2 { - sorted_middleware: vec![app_entry.original_name.clone()], + sorted_middleware: vec![app_entry.pathname.clone()], middleware: Default::default(), - functions: [(app_entry.original_name.clone(), edge_function_definition)] + functions: [(app_entry.pathname.clone(), edge_function_definition)] .into_iter() .collect(), }; + let manifest_path_prefix = get_asset_prefix_from_pathname(&app_entry.pathname); let middleware_manifest_v2 = Vc::upcast(VirtualOutputAsset::new( node_root.join(format!( - "server/app{original_name}/middleware-manifest.json", - original_name = app_entry.original_name + "server/app{manifest_path_prefix}/middleware-manifest.json", )), AssetContent::file( FileContent::Content(File::from(serde_json::to_string_pretty( diff --git a/packages/next-swc/crates/next-api/src/lib.rs b/packages/next-swc/crates/next-api/src/lib.rs index de054e3bc4c33..967b34edfe7c0 100644 --- a/packages/next-swc/crates/next-api/src/lib.rs +++ b/packages/next-swc/crates/next-api/src/lib.rs @@ -4,6 +4,7 @@ mod app; mod entrypoints; +mod middleware; mod pages; pub mod project; pub mod route; diff --git a/packages/next-swc/crates/next-api/src/middleware.rs b/packages/next-swc/crates/next-api/src/middleware.rs new file mode 100644 index 0000000000000..5c353cdb1ae5e --- /dev/null +++ b/packages/next-swc/crates/next-api/src/middleware.rs @@ -0,0 +1,219 @@ +use anyhow::{bail, Context, Result}; +use next_core::{ + all_server_paths, + middleware::{get_middleware_module, wrap_edge_entry}, + mode::NextMode, + next_manifests::{EdgeFunctionDefinition, MiddlewareMatcher, MiddlewaresManifestV2}, + next_server::{get_server_runtime_entries, ServerContextType}, + util::parse_config_from_source, +}; +use turbo_tasks::{Completion, TryJoinIterExt, Value, Vc}; +use turbopack_binding::{ + turbo::tasks_fs::{File, FileContent}, + turbopack::{ + core::{ + asset::AssetContent, + changed::any_content_changed_of_output_assets, + chunk::{ChunkableModule, ChunkingContext}, + context::AssetContext, + module::Module, + output::{OutputAsset, OutputAssets}, + virtual_output::VirtualOutputAsset, + }, + ecmascript::chunk::EcmascriptChunkPlaceable, + }, +}; + +use crate::{ + project::Project, + route::{Endpoint, WrittenEndpoint}, +}; + +#[turbo_tasks::value] +pub struct MiddlewareEndpoint { + project: Vc, + context: Vc>, + userland_module: Vc>, +} + +#[turbo_tasks::value_impl] +impl MiddlewareEndpoint { + #[turbo_tasks::function] + pub fn new( + project: Vc, + context: Vc>, + userland_module: Vc>, + ) -> Vc { + Self { + project, + context, + userland_module, + } + .cell() + } + + #[turbo_tasks::function] + async fn edge_files(&self) -> Result> { + let module = get_middleware_module( + self.context, + self.project.project_path(), + self.userland_module, + ); + + let module = wrap_edge_entry( + self.context, + self.project.project_path(), + module, + "middleware".to_string(), + ); + + let mut evaluatable_assets = get_server_runtime_entries( + self.project.project_path(), + self.project.env(), + Value::new(ServerContextType::Middleware), + NextMode::Development, + self.project.next_config(), + ) + .resolve_entries(self.context) + .await? + .clone_value(); + + let Some(module) = + Vc::try_resolve_downcast::>(module).await? + else { + bail!("Entry module must be evaluatable"); + }; + + let Some(evaluatable) = Vc::try_resolve_sidecast(module).await? else { + bail!("Entry module must be evaluatable"); + }; + evaluatable_assets.push(evaluatable); + + let edge_chunking_context = self.project.edge_middleware_chunking_context(); + + let edge_files = edge_chunking_context.evaluated_chunk_group( + module.as_root_chunk(Vc::upcast(edge_chunking_context)), + Vc::cell(evaluatable_assets), + ); + + Ok(edge_files) + } + + #[turbo_tasks::function] + async fn output_assets(self: Vc) -> Result> { + let this = self.await?; + + let config = parse_config_from_source(this.userland_module); + + let mut output_assets = self.edge_files().await?.clone_value(); + + let node_root = this.project.node_root(); + + let files_paths_from_root = { + let node_root = &node_root.await?; + output_assets + .iter() + .map(|&file| async move { + Ok(node_root + .get_path_to(&*file.ident().path().await?) + .context("middleware file path must be inside the node root")? + .to_string()) + }) + .try_join() + .await? + }; + + let matchers = if let Some(matchers) = config.await?.matcher.as_ref() { + matchers + .iter() + .map(|matcher| MiddlewareMatcher { + original_source: matcher.to_string(), + ..Default::default() + }) + .collect() + } else { + vec![MiddlewareMatcher { + regexp: Some("^/.*$".to_string()), + original_source: "/:path*".to_string(), + ..Default::default() + }] + }; + + let edge_function_definition = EdgeFunctionDefinition { + files: files_paths_from_root, + name: "middleware".to_string(), + page: "/".to_string(), + regions: None, + matchers, + ..Default::default() + }; + let middleware_manifest_v2 = MiddlewaresManifestV2 { + sorted_middleware: Default::default(), + middleware: [("/".to_string(), edge_function_definition)] + .into_iter() + .collect(), + functions: Default::default(), + }; + let middleware_manifest_v2 = Vc::upcast(VirtualOutputAsset::new( + node_root.join("server/middleware/middleware-manifest.json".to_string()), + AssetContent::file( + FileContent::Content(File::from(serde_json::to_string_pretty( + &middleware_manifest_v2, + )?)) + .cell(), + ), + )); + output_assets.push(middleware_manifest_v2); + + Ok(Vc::cell(output_assets)) + } +} + +#[turbo_tasks::value_impl] +impl Endpoint for MiddlewareEndpoint { + #[turbo_tasks::function] + async fn write_to_disk(self: Vc) -> Result> { + let this = self.await?; + let files = self.edge_files(); + let output_assets = self.output_assets(); + this.project + .emit_all_output_assets(Vc::cell(output_assets)) + .await?; + + let node_root = this.project.node_root(); + let server_paths = all_server_paths(output_assets, node_root) + .await? + .clone_value(); + + let node_root = &node_root.await?; + + let files = files + .await? + .iter() + .map(|&file| async move { + Ok(node_root + .get_path_to(&*file.ident().path().await?) + .context("middleware file path must be inside the node root")? + .to_string()) + }) + .try_join() + .await?; + + Ok(WrittenEndpoint::Edge { + files, + global_var_name: "TODO".to_string(), + server_paths, + } + .cell()) + } + + #[turbo_tasks::function] + fn server_changed(self: Vc) -> Vc { + any_content_changed_of_output_assets(self.output_assets()) + } + + #[turbo_tasks::function] + fn client_changed(self: Vc) -> Vc { + Completion::new() + } +} diff --git a/packages/next-swc/crates/next-api/src/pages.rs b/packages/next-swc/crates/next-api/src/pages.rs index bc661782685c8..cebb932770913 100644 --- a/packages/next-swc/crates/next-api/src/pages.rs +++ b/packages/next-swc/crates/next-api/src/pages.rs @@ -842,29 +842,30 @@ impl PageEndpoint { let pathname = this.pathname.await?; let named_regex = get_named_middleware_regex(&pathname); let matchers = MiddlewareMatcher { - regexp: named_regex, + regexp: Some(named_regex), original_source: pathname.to_string(), ..Default::default() }; let original_name = this.original_name.await?; let edge_function_definition = EdgeFunctionDefinition { files: files_paths_from_root, - name: original_name.to_string(), + name: pathname.to_string(), page: original_name.to_string(), regions: None, matchers: vec![matchers], ..Default::default() }; let middleware_manifest_v2 = MiddlewaresManifestV2 { - sorted_middleware: vec![original_name.to_string()], + sorted_middleware: vec![pathname.to_string()], middleware: Default::default(), - functions: [(original_name.to_string(), edge_function_definition)] + functions: [(pathname.to_string(), edge_function_definition)] .into_iter() .collect(), }; + let manifest_path_prefix = get_asset_prefix_from_pathname(&this.pathname.await?); let middleware_manifest_v2 = Vc::upcast(VirtualOutputAsset::new( node_root.join(format!( - "server/pages{original_name}/middleware-manifest.json" + "server/pages{manifest_path_prefix}/middleware-manifest.json" )), AssetContent::file( FileContent::Content(File::from(serde_json::to_string_pretty( diff --git a/packages/next-swc/crates/next-api/src/project.rs b/packages/next-swc/crates/next-api/src/project.rs index 0b522e34e1d13..c0ce87cf01317 100644 --- a/packages/next-swc/crates/next-api/src/project.rs +++ b/packages/next-swc/crates/next-api/src/project.rs @@ -6,17 +6,21 @@ use next_core::{ all_assets_from_entries, app_structure::find_app_dir, emit_assets, get_edge_chunking_context, get_edge_compile_time_info, + get_edge_resolve_options_context, + middleware::middleware_files, mode::NextMode, next_client::{get_client_chunking_context, get_client_compile_time_info}, next_config::{JsConfig, NextConfig}, - next_server::{get_server_chunking_context, get_server_compile_time_info}, + next_server::{ + get_server_chunking_context, get_server_compile_time_info, + get_server_module_options_context, ServerContextType, + }, next_telemetry::NextFeatureTelemetry, - util::NextSourceConfig, }; use serde::{Deserialize, Serialize}; use turbo_tasks::{ debug::ValueDebugFormat, trace::TraceRawVcs, unit, Completion, IntoTraitRef, State, TaskInput, - TransientInstance, Vc, + TransientInstance, Value, Vc, }; use turbopack_binding::{ turbo::{ @@ -28,16 +32,21 @@ use turbopack_binding::{ core::{ chunk::ChunkingContext, compile_time_info::CompileTimeInfo, + context::AssetContext, diagnostics::DiagnosticExt, environment::ServerAddr, + file_source::FileSource, output::OutputAssets, + reference_type::{EntryReferenceSubType, ReferenceType}, + resolve::{find_context_file, FindContextFileResult}, + source::Source, version::{Update, Version, VersionState, VersionedContent}, PROJECT_FILESYSTEM_NAME, }, dev::DevChunkingContext, ecmascript::chunk::EcmascriptChunkingContext, node::execution_context::ExecutionContext, - turbopack::evaluate_context::node_build_environment, + turbopack::{evaluate_context::node_build_environment, ModuleAssetContext}, }, }; @@ -45,6 +54,7 @@ use crate::{ app::{AppProject, OptionAppProject}, build, entrypoints::Entrypoints, + middleware::MiddlewareEndpoint, pages::PagesProject, route::{Endpoint, Route}, versioned_content_map::{OutputAssetsOperation, VersionedContentMap}, @@ -76,7 +86,6 @@ pub struct ProjectOptions { #[derive(Serialize, Deserialize, TraceRawVcs, PartialEq, Eq, ValueDebugFormat)] pub struct Middleware { pub endpoint: Vc>, - pub config: NextSourceConfig, } #[turbo_tasks::value] @@ -458,6 +467,14 @@ impl Project { .with_layer("edge rsc".to_string()) } + #[turbo_tasks::function] + pub(super) fn edge_middleware_chunking_context( + self: Vc, + ) -> Vc> { + self.edge_chunking_context() + .with_layer("middleware".to_string()) + } + /// Scans the app/pages directories for entry points files (matching the /// provided page_extensions). #[turbo_tasks::function] @@ -484,10 +501,22 @@ impl Project { } } - // TODO middleware + let middleware = find_context_file( + self.project_path(), + middleware_files(self.next_config().page_extensions()), + ); + let middleware = if let FindContextFileResult::Found(fs_path, _) = *middleware.await? { + let source = Vc::upcast(FileSource::new(fs_path)); + Some(Middleware { + endpoint: Vc::upcast(self.middleware_endpoint(source)), + }) + } else { + None + }; + Ok(Entrypoints { routes, - middleware: None, + middleware, pages_document_endpoint: self.pages_project().document_endpoint(), pages_app_endpoint: self.pages_project().app_endpoint(), pages_error_endpoint: self.pages_project().error_endpoint(), @@ -495,6 +524,40 @@ impl Project { .cell()) } + #[turbo_tasks::function] + fn middleware_context(self: Vc) -> Vc> { + Vc::upcast(ModuleAssetContext::new( + Default::default(), + self.edge_compile_time_info(), + get_server_module_options_context( + self.project_path(), + self.execution_context(), + Value::new(ServerContextType::Middleware), + NextMode::Development, + self.next_config(), + ), + get_edge_resolve_options_context( + self.project_path(), + Value::new(ServerContextType::Middleware), + NextMode::Development, + self.next_config(), + self.execution_context(), + ), + )) + } + + #[turbo_tasks::function] + fn middleware_endpoint(self: Vc, source: Vc>) -> Vc { + let context = self.middleware_context(); + + let module = context.process( + source, + Value::new(ReferenceType::Entry(EntryReferenceSubType::Middleware)), + ); + + MiddlewareEndpoint::new(self, context, module) + } + #[turbo_tasks::function] pub async fn emit_all_output_assets( self: Vc, diff --git a/packages/next-swc/crates/next-core/src/lib.rs b/packages/next-swc/crates/next-core/src/lib.rs index 6e1b8e916fc28..2359aa7700327 100644 --- a/packages/next-swc/crates/next-core/src/lib.rs +++ b/packages/next-swc/crates/next-core/src/lib.rs @@ -18,6 +18,7 @@ mod emit; pub mod env; mod fallback; pub mod loader_tree; +pub mod middleware; pub mod mode; pub mod next_app; mod next_build; diff --git a/packages/next-swc/crates/next-core/src/middleware.rs b/packages/next-swc/crates/next-core/src/middleware.rs new file mode 100644 index 0000000000000..e40ce81d687b6 --- /dev/null +++ b/packages/next-swc/crates/next-core/src/middleware.rs @@ -0,0 +1,92 @@ +use anyhow::Result; +use indexmap::indexmap; +use indoc::writedoc; +use turbo_tasks::{Value, Vc}; +use turbo_tasks_fs::{rope::RopeBuilder, File, FileSystemPath}; +use turbopack_binding::turbopack::{ + core::{ + asset::AssetContent, context::AssetContext, module::Module, reference_type::ReferenceType, + virtual_source::VirtualSource, + }, + ecmascript::utils::StringifyJs, +}; + +use crate::util::{load_next_js_template, virtual_next_js_template_path}; + +#[turbo_tasks::function] +pub async fn middleware_files(page_extensions: Vc>) -> Result>> { + let extensions = page_extensions.await?; + let files = ["middleware.", "src/middleware."] + .into_iter() + .flat_map(|f| { + extensions + .iter() + .map(move |ext| String::from(f) + ext.as_str()) + }) + .collect(); + Ok(Vc::cell(files)) +} + +#[turbo_tasks::function] +pub async fn get_middleware_module( + context: Vc>, + project_root: Vc, + userland_module: Vc>, +) -> Result>> { + let template_file = "build/templates/middleware.js"; + + // Load the file from the next.js codebase. + let file = load_next_js_template(project_root, template_file.to_string()).await?; + + let file = File::from(file.clone_value()); + + let template_path = virtual_next_js_template_path(project_root, template_file.to_string()); + + let virtual_source = VirtualSource::new(template_path, AssetContent::file(file.into())); + + let inner_assets = indexmap! { + "VAR_USERLAND".to_string() => userland_module + }; + + let module = context.process( + Vc::upcast(virtual_source), + Value::new(ReferenceType::Internal(Vc::cell(inner_assets))), + ); + + Ok(module) +} + +#[turbo_tasks::function] +pub async fn wrap_edge_entry( + context: Vc>, + project_root: Vc, + entry: Vc>, + original_name: String, +) -> Result>> { + use std::io::Write; + let mut source = RopeBuilder::default(); + writedoc!( + source, + r#" + import * as module from "MODULE" + + self._ENTRIES ||= {{}} + self._ENTRIES[{}] = module + "#, + StringifyJs(&format_args!("middleware_{}", original_name)) + )?; + let file = File::from(source.build()); + // TODO(alexkirsz) Figure out how to name this virtual asset. + let virtual_source = VirtualSource::new( + project_root.join("edge-wrapper.js".to_string()), + AssetContent::file(file.into()), + ); + let inner_assets = indexmap! { + "MODULE".to_string() => entry + }; + + Ok(context.process( + Vc::upcast(virtual_source), + Value::new(ReferenceType::Internal(Vc::cell(inner_assets))), + )) +} diff --git a/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs index 3b79382f3e52f..bde79e2c38b4d 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs @@ -81,7 +81,7 @@ pub async fn get_app_page_entry( let original_page_name = get_original_page_name(&original_name); - let template_file = "build/webpack/loaders/next-route-loader/templates/app-page.js"; + let template_file = "build/templates/app-page.js"; // Load the file from the next.js codebase. let file = load_next_js_template(project_root, template_file.to_string()).await?; diff --git a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs index b77c77c84e5a8..8cc80bd78926e 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs @@ -55,7 +55,7 @@ pub async fn get_app_route_entry( let original_page_name = get_original_route_name(&original_name); let path = source.ident().path(); - let template_file = "build/webpack/loaders/next-route-loader/templates/app-route.js"; + let template_file = "build/templates/app-route.js"; // Load the file from the next.js codebase. let file = load_next_js_template(project_root, template_file.to_string()).await?; @@ -143,7 +143,7 @@ pub async fn wrap_edge_entry( context: Vc, project_root: Vc, entry: Vc>, - original_name: String, + pathname: String, ) -> Result>> { let mut source = RopeBuilder::default(); writedoc!( @@ -158,7 +158,7 @@ pub async fn wrap_edge_entry( default: EdgeRouteModuleWrapper.wrap(module.routeModule), }} "#, - StringifyJs(&format_args!("middleware_{}", original_name)) + StringifyJs(&format_args!("middleware_{}", pathname)) )?; let file = File::from(source.build()); // TODO(alexkirsz) Figure out how to name this virtual asset. diff --git a/packages/next-swc/crates/next-core/src/next_manifests/mod.rs b/packages/next-swc/crates/next-core/src/next_manifests/mod.rs index fdbdce042d457..8f2ea09fea763 100644 --- a/packages/next-swc/crates/next-core/src/next_manifests/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_manifests/mod.rs @@ -67,7 +67,9 @@ pub enum RouteHas { #[derive(Serialize, Default, Debug)] #[serde(rename_all = "camelCase")] pub struct MiddlewareMatcher { - pub regexp: String, + // When skipped next.js with fill that during merging. + #[serde(skip_serializing_if = "Option::is_none")] + pub regexp: Option, #[serde(skip_serializing_if = "bool_is_true")] pub locale: bool, #[serde(skip_serializing_if = "Option::is_none")] @@ -107,7 +109,7 @@ pub enum Regions { #[derive(Serialize, Default, Debug)] pub struct MiddlewaresManifestV2 { pub sorted_middleware: Vec, - pub middleware: HashMap, + pub middleware: HashMap, pub functions: HashMap, } diff --git a/packages/next-swc/crates/next-core/src/next_pages/page_entry.rs b/packages/next-swc/crates/next-core/src/next_pages/page_entry.rs index 718819a95cb81..94629f4332020 100644 --- a/packages/next-swc/crates/next-core/src/next_pages/page_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_pages/page_entry.rs @@ -45,15 +45,15 @@ pub async fn create_page_ssr_entry_module( let template_file = match (&reference_type, runtime) { (ReferenceType::Entry(EntryReferenceSubType::Page), _) => { // Load the Page entry file. - "build/webpack/loaders/next-route-loader/templates/pages.js" + "build/templates/pages.js" } (ReferenceType::Entry(EntryReferenceSubType::PagesApi), NextRuntime::NodeJs) => { // Load the Pages API entry file. - "build/webpack/loaders/next-route-loader/templates/pages-api.js" + "build/templates/pages-api.js" } (ReferenceType::Entry(EntryReferenceSubType::PagesApi), NextRuntime::Edge) => { // Load the Pages API entry file. - "build/webpack/loaders/next-route-loader/templates/pages-edge-api.js" + "build/templates/pages-edge-api.js" } _ => bail!("Invalid path type"), }; @@ -125,7 +125,7 @@ pub async fn create_page_ssr_entry_module( ssr_module_context, project_root, ssr_module, - definition_page.to_string(), + definition_pathname.to_string(), ); } @@ -143,7 +143,7 @@ pub async fn wrap_edge_entry( context: Vc>, project_root: Vc, entry: Vc>, - original_name: String, + pathname: String, ) -> Result>> { let mut source = RopeBuilder::default(); writedoc!( @@ -154,7 +154,7 @@ pub async fn wrap_edge_entry( self._ENTRIES ||= {{}} self._ENTRIES[{}] = module "#, - StringifyJs(&format_args!("middleware_{}", original_name)) + StringifyJs(&format_args!("middleware_{}", pathname)) )?; let file = File::from(source.build()); // TODO(alexkirsz) Figure out how to name this virtual asset. diff --git a/packages/next-swc/crates/next-core/src/router.rs b/packages/next-swc/crates/next-core/src/router.rs index be57a839b7121..0174acbb6adf2 100644 --- a/packages/next-swc/crates/next-core/src/router.rs +++ b/packages/next-swc/crates/next-core/src/router.rs @@ -38,6 +38,7 @@ use turbopack_binding::{ use crate::{ embed_js::next_asset, + middleware::middleware_files, mode::NextMode, next_config::NextConfig, next_edge::{ @@ -59,20 +60,6 @@ fn next_configs() -> Vc> { ) } -#[turbo_tasks::function] -async fn middleware_files(page_extensions: Vc>) -> Result>> { - let extensions = page_extensions.await?; - let files = ["middleware.", "src/middleware."] - .into_iter() - .flat_map(|f| { - extensions - .iter() - .map(move |ext| String::from(f) + ext.as_str()) - }) - .collect(); - Ok(Vc::cell(files)) -} - #[turbo_tasks::value(shared)] #[derive(Debug, Clone, Default)] #[serde(rename_all = "camelCase")] diff --git a/packages/next-swc/crates/next-core/src/util.rs b/packages/next-swc/crates/next-core/src/util.rs index 202079d98cf61..78382b8114edf 100644 --- a/packages/next-swc/crates/next-core/src/util.rs +++ b/packages/next-swc/crates/next-core/src/util.rs @@ -124,7 +124,7 @@ pub enum NextRuntime { } #[turbo_tasks::value] -#[derive(Default)] +#[derive(Default, Clone)] pub struct NextSourceConfig { pub runtime: NextRuntime, diff --git a/packages/next/src/build/webpack/loaders/next-route-loader/load-entrypoint.ts b/packages/next/src/build/load-entrypoint.ts similarity index 97% rename from packages/next/src/build/webpack/loaders/next-route-loader/load-entrypoint.ts rename to packages/next/src/build/load-entrypoint.ts index 264cbfc232567..880899b693680 100644 --- a/packages/next/src/build/webpack/loaders/next-route-loader/load-entrypoint.ts +++ b/packages/next/src/build/load-entrypoint.ts @@ -2,13 +2,10 @@ import fs from 'fs/promises' import path from 'path' // NOTE: this should be updated if this loader file is moved. -const PACKAGE_ROOT = path.normalize(path.join(__dirname, '../../../../../..')) +const PACKAGE_ROOT = path.normalize(path.join(__dirname, '../../..')) const TEMPLATE_FOLDER = path.join(__dirname, 'templates') const TEMPLATES_ESM_FOLDER = path.normalize( - path.join( - __dirname, - '../../../../../dist/esm/build/webpack/loaders/next-route-loader/templates' - ) + path.join(__dirname, '../../dist/esm/build/templates') ) /** diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index b4c64f853d545..a9c0545e27558 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -477,8 +477,6 @@ export type TurbopackResult = T & { interface Middleware { endpoint: Endpoint - runtime: 'nodejs' | 'edge' - matcher?: string[] } export interface Entrypoints { diff --git a/packages/next/src/build/webpack/loaders/next-route-loader/templates/app-page.ts b/packages/next/src/build/templates/app-page.ts similarity index 79% rename from packages/next/src/build/webpack/loaders/next-route-loader/templates/app-page.ts rename to packages/next/src/build/templates/app-page.ts index a2fce12623e8e..c75509904c3a8 100644 --- a/packages/next/src/build/webpack/loaders/next-route-loader/templates/app-page.ts +++ b/packages/next/src/build/templates/app-page.ts @@ -1,11 +1,11 @@ -import type { LoaderTree } from '../../../../../server/lib/app-dir-module' +import type { LoaderTree } from '../../server/lib/app-dir-module' // @ts-ignore this need to be imported from next/dist to be external import * as module from 'next/dist/server/future/route-modules/app-page/module' -import { RouteKind } from '../../../../../server/future/route-kind' +import { RouteKind } from '../../server/future/route-kind' const AppPageRouteModule = - module.AppPageRouteModule as unknown as typeof import('../../../../../server/future/route-modules/app-page/module').AppPageRouteModule + module.AppPageRouteModule as unknown as typeof import('../../server/future/route-modules/app-page/module').AppPageRouteModule // These are injected by the loader afterwards. declare const tree: LoaderTree @@ -34,7 +34,7 @@ export const __next_app__ = { loadChunk: __next_app_load_chunk__, } -export * from '../../../../../server/app-render/entry-base' +export * from '../../server/app-render/entry-base' // Create and export the route module that will be consumed. export const routeModule = new AppPageRouteModule({ diff --git a/packages/next/src/build/webpack/loaders/next-route-loader/templates/app-route.ts b/packages/next/src/build/templates/app-route.ts similarity index 83% rename from packages/next/src/build/webpack/loaders/next-route-loader/templates/app-route.ts rename to packages/next/src/build/templates/app-route.ts index 7ad4211899fd0..50a8b6165a747 100644 --- a/packages/next/src/build/webpack/loaders/next-route-loader/templates/app-route.ts +++ b/packages/next/src/build/templates/app-route.ts @@ -1,15 +1,15 @@ -import '../../../../../server/node-polyfill-headers' +import '../../server/node-polyfill-headers' // @ts-ignore this need to be imported from next/dist to be external import * as module from 'next/dist/server/future/route-modules/app-route/module' -import type { AppRouteRouteModuleOptions } from '../../../../../server/future/route-modules/app-route/module' -import { RouteKind } from '../../../../../server/future/route-kind' +import type { AppRouteRouteModuleOptions } from '../../server/future/route-modules/app-route/module' +import { RouteKind } from '../../server/future/route-kind' // @ts-expect-error - replaced by webpack/turbopack loader import * as userland from 'VAR_USERLAND' const AppRouteRouteModule = - module.AppRouteRouteModule as unknown as typeof import('../../../../../server/future/route-modules/app-route/module').AppRouteRouteModule + module.AppRouteRouteModule as unknown as typeof import('../../server/future/route-modules/app-route/module').AppRouteRouteModule // These are injected by the loader afterwards. This is injected as a variable // instead of a replacement because this could also be `undefined` instead of diff --git a/packages/next/src/build/webpack/loaders/next-route-loader/helpers.ts b/packages/next/src/build/templates/helpers.ts similarity index 100% rename from packages/next/src/build/webpack/loaders/next-route-loader/helpers.ts rename to packages/next/src/build/templates/helpers.ts diff --git a/packages/next/src/build/templates/middleware.ts b/packages/next/src/build/templates/middleware.ts new file mode 100644 index 0000000000000..15dda841e1136 --- /dev/null +++ b/packages/next/src/build/templates/middleware.ts @@ -0,0 +1,26 @@ +import '../../server/web/globals' +import type { AdapterOptions } from '../../server/web/adapter' +import { adapter } from '../../server/web/adapter' + +// Import the userland code. +// @ts-expect-error - replaced by webpack/turbopack loader +import * as _mod from 'VAR_USERLAND' + +const mod = { ..._mod } +const handler = mod.middleware || mod.default + +if (typeof handler !== 'function') { + throw new Error( + `The Middleware must export a \`middleware\` or a \`default\` function` + ) +} + +export default function ( + opts: Omit +) { + return adapter({ + ...opts, + page: '', + handler, + }) +} diff --git a/packages/next/src/build/webpack/loaders/next-route-loader/templates/pages-api.ts b/packages/next/src/build/templates/pages-api.ts similarity index 83% rename from packages/next/src/build/webpack/loaders/next-route-loader/templates/pages-api.ts rename to packages/next/src/build/templates/pages-api.ts index d566f35fe99d7..a48822f9ed75a 100644 --- a/packages/next/src/build/webpack/loaders/next-route-loader/templates/pages-api.ts +++ b/packages/next/src/build/templates/pages-api.ts @@ -1,10 +1,10 @@ // @ts-ignore this need to be imported from next/dist to be external import * as module from 'next/dist/server/future/route-modules/pages-api/module' -import { RouteKind } from '../../../../../server/future/route-kind' -import { hoist } from '../helpers' +import { RouteKind } from '../../server/future/route-kind' +import { hoist } from './helpers' const PagesAPIRouteModule = - module.PagesAPIRouteModule as unknown as typeof import('../../../../../server/future/route-modules/pages-api/module').PagesAPIRouteModule + module.PagesAPIRouteModule as unknown as typeof import('../../server/future/route-modules/pages-api/module').PagesAPIRouteModule // Import the userland code. // @ts-expect-error - replaced by webpack/turbopack loader diff --git a/packages/next/src/build/webpack/loaders/next-route-loader/templates/pages-edge-api.ts b/packages/next/src/build/templates/pages-edge-api.ts similarity index 66% rename from packages/next/src/build/webpack/loaders/next-route-loader/templates/pages-edge-api.ts rename to packages/next/src/build/templates/pages-edge-api.ts index 83e9a3d4ed71d..7d453a69720b2 100644 --- a/packages/next/src/build/webpack/loaders/next-route-loader/templates/pages-edge-api.ts +++ b/packages/next/src/build/templates/pages-edge-api.ts @@ -1,7 +1,7 @@ -import '../../../../../server/web/globals' -import type { AdapterOptions } from '../../../../../server/web/adapter' -import { adapter } from '../../../../../server/web/adapter' -import { IncrementalCache } from '../../../../..//server/lib/incremental-cache' +import '../../server/web/globals' +import type { AdapterOptions } from '../../server/web/adapter' +import { adapter } from '../../server/web/adapter' +import { IncrementalCache } from '../../server/lib/incremental-cache' // Import the userland code. // @ts-expect-error - replaced by webpack/turbopack loader diff --git a/packages/next/src/build/webpack/loaders/next-route-loader/templates/pages.ts b/packages/next/src/build/templates/pages.ts similarity index 88% rename from packages/next/src/build/webpack/loaders/next-route-loader/templates/pages.ts rename to packages/next/src/build/templates/pages.ts index 6b9dd0ee0f0cc..3f3527e6650d6 100644 --- a/packages/next/src/build/webpack/loaders/next-route-loader/templates/pages.ts +++ b/packages/next/src/build/templates/pages.ts @@ -1,7 +1,7 @@ // @ts-ignore this need to be imported from next/dist to be external import * as module from 'next/dist/server/future/route-modules/pages/module' -import { RouteKind } from '../../../../../server/future/route-kind' -import { hoist } from '../helpers' +import { RouteKind } from '../../server/future/route-kind' +import { hoist } from './helpers' // Import the app and document modules. // @ts-expect-error - replaced by webpack/turbopack loader @@ -14,7 +14,7 @@ import App from 'VAR_MODULE_APP' import * as userland from 'VAR_USERLAND' const PagesRouteModule = - module.PagesRouteModule as unknown as typeof import('../../../../../server/future/route-modules/pages/module').PagesRouteModule + module.PagesRouteModule as unknown as typeof import('../../server/future/route-modules/pages/module').PagesRouteModule // Re-export the component (should be the default export). export default hoist(userland, 'default') diff --git a/packages/next/src/build/webpack/loaders/next-app-loader.ts b/packages/next/src/build/webpack/loaders/next-app-loader.ts index e0922628c7795..e89e6854fc4db 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader.ts @@ -21,7 +21,7 @@ import { AppPathnameNormalizer } from '../../../server/future/normalizers/built/ import { AppBundlePathNormalizer } from '../../../server/future/normalizers/built/app/app-bundle-path-normalizer' import { MiddlewareConfig } from '../../analysis/get-page-static-info' import { getFilenameAndExtension } from './next-metadata-route-loader' -import { loadEntrypoint } from './next-route-loader/load-entrypoint' +import { loadEntrypoint } from '../../load-entrypoint' export type AppLoaderOptions = { name: string diff --git a/packages/next/src/build/webpack/loaders/next-route-loader/index.ts b/packages/next/src/build/webpack/loaders/next-route-loader/index.ts index 42f77f74e87ad..6423a4a2b310a 100644 --- a/packages/next/src/build/webpack/loaders/next-route-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-route-loader/index.ts @@ -10,7 +10,7 @@ import { RouteKind } from '../../../../server/future/route-kind' import { normalizePagePath } from '../../../../shared/lib/page-path/normalize-page-path' import { decodeFromBase64, encodeToBase64 } from '../utils' import { isInstrumentationHookFile } from '../../../worker' -import { loadEntrypoint } from './load-entrypoint' +import { loadEntrypoint } from '../../../load-entrypoint' type RouteLoaderOptionsPagesAPIInput = { kind: RouteKind.PAGES_API diff --git a/packages/next/src/server/lib/router-utils/setup-dev.ts b/packages/next/src/server/lib/router-utils/setup-dev.ts index 7ff66ea421d6a..a53b95937691e 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev.ts @@ -86,6 +86,7 @@ import { PropagateToWorkersField } from './types' import { MiddlewareManifest } from '../../../build/webpack/plugins/middleware-plugin' import { devPageFiles } from '../../../build/webpack/plugins/next-types-plugin/shared' import type { RenderWorkers } from '../router-server' +import { pathToRegexp } from 'next/dist/compiled/path-to-regexp' type SetupOpts = { renderWorkers: RenderWorkers @@ -140,6 +141,23 @@ async function startWatcher(opts: SetupOpts) { await opts.renderWorkers.pages?.propagateServerField(field, args) } + const serverFields: { + actualMiddlewareFile?: string | undefined + actualInstrumentationHookFile?: string | undefined + appPathRoutes?: Record + middleware?: + | { + page: string + match: MiddlewareRouteMatch + matchers?: MiddlewareMatcher[] + } + | undefined + hasAppNotFound?: boolean + interceptionRoutes?: ReturnType< + typeof import('./filesystem').buildCustomRoute + >[] + } = {} + let hotReloader: InstanceType if (opts.turbo) { @@ -169,10 +187,153 @@ async function startWatcher(opts: SetupOpts) { document: undefined, error: undefined, } + let currentEntriesHandlingResolve: ((value?: unknown) => void) | undefined + let currentEntriesHandling = new Promise( + (resolve) => (currentEntriesHandlingResolve = resolve) + ) + + const issues = new Map>() + + async function processResult( + key: string, + result: TurbopackResult | undefined + ): Promise | undefined> { + if (result) { + await (global as any)._nextDeleteCache?.( + result.serverPaths + .map((p) => path.join(distDir, p)) + .concat([ + // We need to clear the chunk cache in react + require.resolve( + 'next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.development.js' + ), + // And this redirecting module as well + require.resolve( + 'next/dist/compiled/react-server-dom-webpack/client.edge.js' + ), + ]) + ) + + const oldSet = issues.get(key) ?? new Set() + const newSet = new Set() + + for (const issue of result.issues) { + // TODO better formatting + if (issue.severity !== 'error' && issue.severity !== 'fatal') continue + const issueKey = `${issue.severity} - ${issue.filePath} - ${issue.title}\n${issue.description}\n\n` + if (!oldSet.has(issueKey)) { + console.error( + ` ⚠ ${key} ${issue.severity} - ${ + issue.filePath + }\n ${issue.title.replace( + /\n/g, + '\n ' + )}\n ${issue.description.replace(/\n/g, '\n ')}\n\n` + ) + } + newSet.add(issueKey) + } + for (const issue of oldSet) { + if (!newSet.has(issue)) { + console.error(`✅ ${key} fixed ${issue}`) + } + } + + issues.set(key, newSet) + } + return result + } + + const clearCache = (filePath: string) => + (global as any)._nextDeleteCache?.([filePath]) + + async function loadPartialManifest( + name: string, + pageName: string, + type: 'pages' | 'app' | 'app-route' | 'middleware' = 'pages' + ): Promise { + const manifestPath = path.posix.join( + distDir, + `server`, + type === 'app-route' ? 'app' : type, + type === 'middleware' + ? '' + : pageName === '/' && type === 'pages' + ? 'index' + : pageName, + pageName === '/_not-found' || pageName === '/not-found' + ? '' + : type === 'app' + ? 'page' + : type === 'app-route' + ? 'route' + : '', + name + ) + return JSON.parse( + await readFile(path.posix.join(manifestPath), 'utf-8') + ) as T + } + + const buildManifests = new Map() + const appBuildManifests = new Map() + const pagesManifests = new Map() + const appPathsManifests = new Map() + const middlewareManifests = new Map() + + async function loadMiddlewareManifest( + pageName: string, + type: 'pages' | 'app' | 'app-route' | 'middleware' + ): Promise { + middlewareManifests.set( + pageName, + await loadPartialManifest(MIDDLEWARE_MANIFEST, pageName, type) + ) + } + + async function loadBuildManifest( + pageName: string, + type: 'app' | 'pages' = 'pages' + ): Promise { + buildManifests.set( + pageName, + await loadPartialManifest(BUILD_MANIFEST, pageName, type) + ) + } + + async function loadAppBuildManifest(pageName: string): Promise { + appBuildManifests.set( + pageName, + await loadPartialManifest(APP_BUILD_MANIFEST, pageName, 'app') + ) + } + + async function loadPagesManifest(pageName: string): Promise { + pagesManifests.set( + pageName, + await loadPartialManifest(PAGES_MANIFEST, pageName) + ) + } + + async function loadAppPathManifest( + pageName: string, + type: 'app' | 'app-route' = 'app' + ): Promise { + appPathsManifests.set( + pageName, + await loadPartialManifest(APP_PATHS_MANIFEST, pageName, type) + ) + } try { async function handleEntries() { for await (const entrypoints of iter) { + if (!currentEntriesHandlingResolve) { + currentEntriesHandling = new Promise( + // eslint-disable-next-line no-loop-func + (resolve) => (currentEntriesHandlingResolve = resolve) + ) + } globalEntries.app = entrypoints.pagesAppEndpoint globalEntries.document = entrypoints.pagesDocumentEndpoint globalEntries.error = entrypoints.pagesErrorEndpoint @@ -193,6 +354,33 @@ async function startWatcher(opts: SetupOpts) { break } } + + if (entrypoints.middleware) { + await processResult( + 'middleware', + await entrypoints.middleware.endpoint.writeToDisk() + ) + await loadMiddlewareManifest('middleware', 'middleware') + serverFields.actualMiddlewareFile = 'middleware' + serverFields.middleware = { + match: null as any, + page: '/', + matchers: + middlewareManifests.get('middleware')?.middleware['/'].matchers, + } + } else { + middlewareManifests.delete('middleware') + serverFields.actualMiddlewareFile = undefined + serverFields.middleware = undefined + } + await propagateToWorkers( + 'actualMiddlewareFile', + serverFields.actualMiddlewareFile + ) + await propagateToWorkers('middleware', serverFields.middleware) + + currentEntriesHandlingResolve!() + currentEntriesHandlingResolve = undefined } } handleEntries().catch((err) => { @@ -203,14 +391,6 @@ async function startWatcher(opts: SetupOpts) { console.error(e) } - const buildManifests = new Map() - const appBuildManifests = new Map() - const pagesManifests = new Map() - const appPathsManifests = new Map() - const middlewareManifests = new Map() - - const issues = new Map>() - function mergeBuildManifests(manifests: Iterable) { const manifest: Partial & Pick = { pages: { @@ -263,63 +443,25 @@ async function startWatcher(opts: SetupOpts) { } for (const m of manifests) { Object.assign(manifest.functions, m.functions) + Object.assign(manifest.middleware, m.middleware) } - return manifest - } - - async function processResult( - key: string, - result: TurbopackResult | undefined - ): Promise | undefined> { - if (result) { - await (global as any)._nextDeleteCache?.( - result.serverPaths - .map((p) => path.join(distDir, p)) - .concat([ - // We need to clear the chunk cache in react - require.resolve( - 'next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.development.js' - ), - // And this redirecting module as well - require.resolve( - 'next/dist/compiled/react-server-dom-webpack/client.edge.js' - ), - ]) - ) - - const oldSet = issues.get(key) ?? new Set() - const newSet = new Set() - - for (const issue of result.issues) { - // TODO better formatting - if (issue.severity !== 'error' && issue.severity !== 'fatal') continue - const issueKey = `${issue.severity} - ${issue.filePath} - ${issue.title}\n${issue.description}\n\n` - if (!oldSet.has(issueKey)) { - console.error( - ` ⚠ ${key} ${issue.severity} - ${ - issue.filePath - }\n ${issue.title.replace( - /\n/g, - '\n ' - )}\n ${issue.description.replace(/\n/g, '\n ')}\n\n` - ) - } - newSet.add(issueKey) - } - for (const issue of oldSet) { - if (!newSet.has(issue)) { - console.error(`✅ ${key} fixed ${issue}`) + for (const fun of Object.values(manifest.functions).concat( + Object.values(manifest.middleware) + )) { + for (const matcher of fun.matchers) { + if (!matcher.regexp) { + matcher.regexp = pathToRegexp(matcher.originalSource, [], { + delimiter: '/', + sensitive: false, + strict: true, + }).source.replaceAll('\\/', '/') } } - - issues.set(key, newSet) } - return result + manifest.sortedMiddleware = Object.keys(manifest.middleware) + return manifest } - const clearCache = (filePath: string) => - (global as any)._nextDeleteCache?.([filePath]) - async function writeBuildManifest(): Promise { const buildManifest = mergeBuildManifests(buildManifests.values()) const buildManifestPath = path.join(distDir, 'build-manifest.json') @@ -456,6 +598,7 @@ async function startWatcher(opts: SetupOpts) { 2 ) ) + await currentEntriesHandling await writeBuildManifest() await writeAppBuildManifest() await writePagesManifest() @@ -471,86 +614,6 @@ async function startWatcher(opts: SetupOpts) { ) => { let page = ensureOpts.match?.definition?.pathname ?? ensureOpts.page - async function loadPartialManifest( - name: string, - pageName: string, - isApp: boolean = false, - isRoute: boolean = false - ): Promise { - const manifestPath = path.posix.join( - distDir, - `server`, - isApp ? 'app' : 'pages', - pageName === '/' && !isApp ? 'index' : pageName, - isApp && pageName !== '/_not-found' && pageName !== '/not-found' - ? isRoute - ? 'route' - : 'page' - : '', - name - ) - return JSON.parse( - await readFile(path.posix.join(manifestPath), 'utf-8') - ) as T - } - - async function loadBuildManifest( - pageName: string, - isApp: boolean = false - ): Promise { - buildManifests.set( - pageName, - await loadPartialManifest(BUILD_MANIFEST, pageName, isApp) - ) - } - - async function loadAppBuildManifest( - pageName: string - ): Promise { - appBuildManifests.set( - pageName, - await loadPartialManifest(APP_BUILD_MANIFEST, pageName, true) - ) - } - - async function loadPagesManifest(pageName: string): Promise { - pagesManifests.set( - pageName, - await loadPartialManifest(PAGES_MANIFEST, pageName) - ) - } - - async function loadAppPathManifest( - pageName: string, - routeHandler: boolean - ): Promise { - appPathsManifests.set( - pageName, - await loadPartialManifest( - APP_PATHS_MANIFEST, - pageName, - true, - routeHandler - ) - ) - } - - async function loadMiddlewareManifest( - pageName: string, - isApp: boolean = false, - isRoute: boolean = false - ): Promise { - middlewareManifests.set( - pageName, - await loadPartialManifest( - MIDDLEWARE_MANIFEST, - pageName, - isApp, - isRoute - ) - ) - } - if (page === '/_error') { await processResult( '_app', @@ -580,12 +643,14 @@ async function startWatcher(opts: SetupOpts) { return } + await currentEntriesHandling const route = curEntries.get(page) if (!route) { // TODO: why is this entry missing in turbopack? if (page === '/_app') return if (page === '/_document') return + if (page === '/middleware') return throw new PageNotFoundError(`route not found ${page}`) } @@ -620,11 +685,15 @@ async function startWatcher(opts: SetupOpts) { await loadBuildManifest(page) await loadPagesManifest(page) - if (type === 'edge') await loadMiddlewareManifest(page, false) + if (type === 'edge') { + await loadMiddlewareManifest(page, 'pages') + } else { + middlewareManifests.delete(page) + } await writeBuildManifest() await writePagesManifest() - if (type === 'edge') await writeMiddlewareManifest() + await writeMiddlewareManifest() await writeOtherManifests() break @@ -644,10 +713,14 @@ async function startWatcher(opts: SetupOpts) { const type = writtenEndpoint?.type await loadPagesManifest(page) - if (type === 'edge') await loadMiddlewareManifest(page, false) + if (type === 'edge') { + await loadMiddlewareManifest(page, 'pages') + } else { + middlewareManifests.delete(page) + } await writePagesManifest() - if (type === 'edge') await writeMiddlewareManifest() + await writeMiddlewareManifest() await writeOtherManifests() break @@ -659,8 +732,8 @@ async function startWatcher(opts: SetupOpts) { ) await loadAppBuildManifest(page) - await loadBuildManifest(page, true) - await loadAppPathManifest(page, false) + await loadBuildManifest(page, 'app') + await loadAppPathManifest(page, 'app') await writeAppBuildManifest() await writeBuildManifest() @@ -675,14 +748,17 @@ async function startWatcher(opts: SetupOpts) { await processResult(page, await route.endpoint.writeToDisk()) )?.type - await loadAppPathManifest(page, true) - if (type === 'edge') - await loadMiddlewareManifest(page, true, true) + await loadAppPathManifest(page, 'app-route') + if (type === 'edge') { + await loadMiddlewareManifest(page, 'app-route') + } else { + middlewareManifests.delete(page) + } await writeAppBuildManifest() await writeAppPathsManifest() await writeMiddlewareManifest() - if (type === 'edge') await writeMiddlewareManifest() + await writeMiddlewareManifest() await writeOtherManifests() break @@ -761,23 +837,6 @@ async function startWatcher(opts: SetupOpts) { let resolved = false let prevSortedRoutes: string[] = [] - const serverFields: { - actualMiddlewareFile?: string | undefined - actualInstrumentationHookFile?: string | undefined - appPathRoutes?: Record - middleware?: - | { - page: string - match: MiddlewareRouteMatch - matchers?: MiddlewareMatcher[] - } - | undefined - hasAppNotFound?: boolean - interceptionRoutes?: ReturnType< - typeof import('./filesystem').buildCustomRoute - >[] - } = {} - await new Promise(async (resolve, reject) => { if (pagesDir) { // Watchpack doesn't emit an event for an empty directory diff --git a/test/integration/middleware-basic/middleware.ts b/test/integration/middleware-basic/middleware.ts new file mode 100644 index 0000000000000..921494cf4201e --- /dev/null +++ b/test/integration/middleware-basic/middleware.ts @@ -0,0 +1,7 @@ +import { NextResponse } from 'next/server' + +export default function () { + const response = NextResponse.next() + response.headers.set('X-From-Middleware', 'true') + return response +} diff --git a/test/integration/middleware-basic/pages/index.js b/test/integration/middleware-basic/pages/index.js new file mode 100644 index 0000000000000..2c7ff07191610 --- /dev/null +++ b/test/integration/middleware-basic/pages/index.js @@ -0,0 +1,2 @@ +const Page = () =>

Hi from SRC

+export default Page diff --git a/test/integration/middleware-basic/test/index.test.js b/test/integration/middleware-basic/test/index.test.js new file mode 100644 index 0000000000000..62a1c5ad1d93a --- /dev/null +++ b/test/integration/middleware-basic/test/index.test.js @@ -0,0 +1,50 @@ +/* eslint-env jest */ + +import fs from 'fs-extra' +import { join } from 'path' +import { + fetchViaHTTP, + findPort, + launchApp, + killApp, + nextBuild, + nextStart, +} from 'next-test-utils' + +let app +let appPort +const appDir = join(__dirname, '../') +const header = 'X-From-Middleware' + +function runTest() { + it('loads a middleware', async () => { + const response = await fetchViaHTTP(appPort, '/post-1') + expect(response.headers.has(header)).toBe(true) + }) +} + +describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTest() +}) + +// TODO enable that once turbopack supports middleware in dev mode +describe.skip('production mode', () => { + beforeAll(async () => { + await nextBuild(appDir) + + const outdir = join(__dirname, '..', 'out') + await fs.remove(outdir).catch(() => {}) + + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTest() +}) diff --git a/test/turbopack-tests-manifest.js b/test/turbopack-tests-manifest.js index cf9355b1f8f10..1ac91519c75a1 100644 --- a/test/turbopack-tests-manifest.js +++ b/test/turbopack-tests-manifest.js @@ -21,6 +21,7 @@ const enabledTests = [ 'test/e2e/type-module-interop/index.test.ts', 'test/e2e/undici-fetch/index.test.ts', 'test/integration/bigint/test/index.test.js', + 'test/integration/middleware-basic/test/index.test.js', ] module.exports = { enabledTests }