diff --git a/packages/next-swc/crates/next-api/src/middleware.rs b/packages/next-swc/crates/next-api/src/middleware.rs index de957905b8..89fbd46c50 100644 --- a/packages/next-swc/crates/next-api/src/middleware.rs +++ b/packages/next-swc/crates/next-api/src/middleware.rs @@ -5,7 +5,7 @@ use next_core::{ next_edge::entry::wrap_edge_entry, next_manifests::{EdgeFunctionDefinition, MiddlewareMatcher, MiddlewaresManifestV2}, next_server::{get_server_runtime_entries, ServerContextType}, - util::parse_config_from_source, + util::{parse_config_from_source, MiddlewareMatcherKind}, }; use tracing::Instrument; use turbo_tasks::{Completion, Value, Vc}; @@ -138,9 +138,12 @@ impl MiddlewareEndpoint { let matchers = if let Some(matchers) = config.await?.matcher.as_ref() { matchers .iter() - .map(|matcher| MiddlewareMatcher { - original_source: matcher.to_string(), - ..Default::default() + .map(|matcher| match matcher { + MiddlewareMatcherKind::Str(matchers) => MiddlewareMatcher { + original_source: matchers.to_string(), + ..Default::default() + }, + MiddlewareMatcherKind::Matcher(matcher) => matcher.clone(), }) .collect() } else { diff --git a/packages/next-swc/crates/next-core/src/next_config.rs b/packages/next-swc/crates/next-core/src/next_config.rs index 2d7b032e3f..7ed10e26e1 100644 --- a/packages/next-swc/crates/next-core/src/next_config.rs +++ b/packages/next-swc/crates/next-core/src/next_config.rs @@ -4,7 +4,7 @@ use anyhow::{bail, Context, Result}; use indexmap::IndexMap; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value as JsonValue; -use turbo_tasks::{trace::TraceRawVcs, Vc}; +use turbo_tasks::{trace::TraceRawVcs, TaskInput, Vc}; use turbopack_binding::{ turbo::{tasks_env::EnvMap, tasks_fs::FileSystemPath}, turbopack::{ @@ -197,7 +197,19 @@ pub enum OutputType { Export, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] +#[derive( + Debug, + Clone, + Hash, + Eq, + PartialEq, + Ord, + PartialOrd, + TaskInput, + TraceRawVcs, + Serialize, + Deserialize, +)] #[serde(tag = "type", rename_all = "kebab-case")] pub enum RouteHas { Header { 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 25485c43a2..632e0cf051 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 @@ -8,7 +8,7 @@ use indexmap::IndexSet; use serde::{Deserialize, Serialize}; use turbo_tasks::{trace::TraceRawVcs, TaskInput}; -use crate::next_config::{CrossOriginConfig, Rewrites}; +use crate::next_config::{CrossOriginConfig, Rewrites, RouteHas}; #[derive(Serialize, Default, Debug)] pub struct PagesManifest { @@ -44,30 +44,20 @@ impl Default for MiddlewaresManifest { } } -#[derive(Serialize, Debug)] -#[serde(tag = "type", rename_all = "kebab-case")] -pub enum RouteHas { - Header { - key: String, - #[serde(skip_serializing_if = "Option::is_none")] - value: Option, - }, - Cookie { - key: String, - #[serde(skip_serializing_if = "Option::is_none")] - value: Option, - }, - Query { - key: String, - #[serde(skip_serializing_if = "Option::is_none")] - value: Option, - }, - Host { - value: String, - }, -} - -#[derive(Serialize, Default, Debug)] +#[derive( + Debug, + Clone, + Hash, + Eq, + PartialEq, + Ord, + PartialOrd, + TaskInput, + TraceRawVcs, + Serialize, + Deserialize, + Default, +)] #[serde(rename_all = "camelCase")] pub struct MiddlewareMatcher { // When skipped next.js with fill that during merging. diff --git a/packages/next-swc/crates/next-core/src/util.rs b/packages/next-swc/crates/next-core/src/util.rs index f0b7168aa4..2d1e837d77 100644 --- a/packages/next-swc/crates/next-core/src/util.rs +++ b/packages/next-swc/crates/next-core/src/util.rs @@ -28,7 +28,11 @@ use turbopack_binding::{ }, }; -use crate::{next_config::NextConfig, next_import_map::get_next_package}; +use crate::{ + next_config::{NextConfig, RouteHas}, + next_import_map::get_next_package, + next_manifests::MiddlewareMatcher, +}; const NEXT_TEMPLATE_PATH: &str = "dist/esm/build/templates"; @@ -151,13 +155,20 @@ impl NextRuntime { } } +#[turbo_tasks::value] +#[derive(Debug, Clone)] +pub enum MiddlewareMatcherKind { + Str(String), + Matcher(MiddlewareMatcher), +} + #[turbo_tasks::value] #[derive(Default, Clone)] pub struct NextSourceConfig { pub runtime: NextRuntime, /// Middleware router matchers - pub matcher: Option>, + pub matcher: Option>, } #[turbo_tasks::value_impl] @@ -215,6 +226,139 @@ impl Issue for NextSourceConfigParsingIssue { } } +fn emit_invalid_config_warning(ident: Vc, detail: &str, value: &JsValue) { + let (explainer, hints) = value.explain(2, 0); + NextSourceConfigParsingIssue { + ident, + detail: StyledString::Text(format!("{detail} Got {explainer}.{hints}")).cell(), + } + .cell() + .emit() +} + +fn parse_route_matcher_from_js_value( + ident: Vc, + value: &JsValue, +) -> Option> { + let parse_matcher_kind_matcher = |value: &JsValue| { + let mut route_has = vec![]; + if let JsValue::Array { items, .. } = value { + for item in items { + if let JsValue::Object { parts, .. } = item { + let mut route_type = None; + let mut route_key = None; + let mut route_value = None; + + for matcher_part in parts { + if let ObjectPart::KeyValue(part_key, part_value) = matcher_part { + match part_key.as_str() { + Some("type") => { + route_type = part_value.as_str().map(|v| v.to_string()) + } + Some("key") => { + route_key = part_value.as_str().map(|v| v.to_string()) + } + Some("value") => { + route_value = part_value.as_str().map(|v| v.to_string()) + } + _ => {} + } + } + } + let r = match route_type.as_deref() { + Some("header") => route_key.map(|route_key| RouteHas::Header { + key: route_key, + value: route_value, + }), + Some("cookie") => route_key.map(|route_key| RouteHas::Cookie { + key: route_key, + value: route_value, + }), + Some("query") => route_key.map(|route_key| RouteHas::Query { + key: route_key, + value: route_value, + }), + Some("host") => { + route_value.map(|route_value| RouteHas::Host { value: route_value }) + } + _ => None, + }; + + if let Some(r) = r { + route_has.push(r); + } + } + } + } + + route_has + }; + + let mut matchers = vec![]; + + match value { + JsValue::Constant(matcher) => { + if let Some(matcher) = matcher.as_str() { + matchers.push(MiddlewareMatcherKind::Str(matcher.to_string())); + } else { + emit_invalid_config_warning( + ident, + "The matcher property must be a string or array of strings", + value, + ); + } + } + JsValue::Array { items, .. } => { + for item in items { + if let Some(matcher) = item.as_str() { + matchers.push(MiddlewareMatcherKind::Str(matcher.to_string())); + } else if let JsValue::Object { parts, .. } = item { + let mut matcher = MiddlewareMatcher::default(); + for matcher_part in parts { + if let ObjectPart::KeyValue(key, value) = matcher_part { + match key.as_str() { + Some("source") => { + if let Some(value) = value.as_str() { + matcher.original_source = value.to_string(); + } + } + Some("missing") => { + matcher.missing = Some(parse_matcher_kind_matcher(value)) + } + Some("has") => { + matcher.has = Some(parse_matcher_kind_matcher(value)) + } + _ => { + //noop + } + } + } + } + + matchers.push(MiddlewareMatcherKind::Matcher(matcher)); + } else { + emit_invalid_config_warning( + ident, + "The matcher property must be a string or array of strings", + value, + ); + } + } + } + _ => emit_invalid_config_warning( + ident, + "The matcher property must be a string or array of strings", + value, + ), + } + + if matchers.is_empty() { + None + } else { + Some(matchers) + } +} + #[turbo_tasks::function] pub async fn parse_config_from_source(module: Vc>) -> Result> { if let Some(ecmascript_asset) = @@ -323,19 +467,12 @@ pub async fn parse_config_from_source(module: Vc>) -> Result>, value: &JsValue) -> NextSourceConfig { let mut config = NextSourceConfig::default(); - let invalid_config = |detail: &str, value: &JsValue| { - let (explainer, hints) = value.explain(2, 0); - NextSourceConfigParsingIssue { - ident: module.ident(), - detail: StyledString::Text(format!("{detail} Got {explainer}.{hints}")).cell(), - } - .cell() - .emit() - }; + if let JsValue::Object { parts, .. } = value { for part in parts { match part { - ObjectPart::Spread(_) => invalid_config( + ObjectPart::Spread(_) => emit_invalid_config_warning( + module.ident(), "Spread properties are not supported in the config export.", value, ), @@ -352,7 +489,8 @@ fn parse_config_from_js_value(module: Vc>, value: &JsValue) -> N config.runtime = NextRuntime::NodeJs; } _ => { - invalid_config( + emit_invalid_config_warning( + module.ident(), "The runtime property must be either \"nodejs\" \ or \"edge\".", value, @@ -361,48 +499,20 @@ fn parse_config_from_js_value(module: Vc>, value: &JsValue) -> N } } } else { - invalid_config( + emit_invalid_config_warning( + module.ident(), "The runtime property must be a constant string.", value, ); } } if key == "matcher" { - let mut matchers = vec![]; - match value { - JsValue::Constant(matcher) => { - if let Some(matcher) = matcher.as_str() { - matchers.push(matcher.to_string()); - } else { - invalid_config( - "The matcher property must be a string or array of \ - strings", - value, - ); - } - } - JsValue::Array { items, .. } => { - for item in items { - if let Some(matcher) = item.as_str() { - matchers.push(matcher.to_string()); - } else { - invalid_config( - "The matcher property must be a string or array \ - of strings", - value, - ); - } - } - } - _ => invalid_config( - "The matcher property must be a string or array of strings", - value, - ), - } - config.matcher = Some(matchers); + config.matcher = + parse_route_matcher_from_js_value(module.ident(), value); } } else { - invalid_config( + emit_invalid_config_warning( + module.ident(), "The exported config object must not contain non-constant strings.", key, ); @@ -411,7 +521,8 @@ fn parse_config_from_js_value(module: Vc>, value: &JsValue) -> N } } } else { - invalid_config( + emit_invalid_config_warning( + module.ident(), "The exported config object must be a valid object literal.", value, ); diff --git a/test/e2e/middleware-custom-matchers/app/middleware.js b/test/e2e/middleware-custom-matchers/app/middleware.js index 99ec191d35..da2ca8e84f 100644 --- a/test/e2e/middleware-custom-matchers/app/middleware.js +++ b/test/e2e/middleware-custom-matchers/app/middleware.js @@ -77,5 +77,13 @@ export const config = { }, ], }, + { + source: + '/((?!api|monitoring|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|manifest|icon|source-match|has-match-1|has-match-2|has-match-3|has-match-4|has-match-5|missing-match-1|missing-match-2|routes).*)', + missing: [ + { type: 'header', key: 'next-router-prefetch' }, + { type: 'header', key: 'purpose', value: 'prefetch' }, + ], + }, ], } diff --git a/test/e2e/middleware-custom-matchers/test/index.test.ts b/test/e2e/middleware-custom-matchers/test/index.test.ts index 9539e52177..001b187798 100644 --- a/test/e2e/middleware-custom-matchers/test/index.test.ts +++ b/test/e2e/middleware-custom-matchers/test/index.test.ts @@ -31,6 +31,16 @@ describe('Middleware custom matchers', () => { }, }) expect(res2.headers.get('x-from-middleware')).toBeFalsy() + + const res3 = await fetchViaHTTP(next.url, '/') + expect(res3.headers.get('x-from-middleware')).toBeDefined() + + const res4 = await fetchViaHTTP(next.url, '/', undefined, { + headers: { + purpose: 'prefetch', + }, + }) + expect(res4.headers.get('x-from-middleware')).toBeFalsy() }) it('should match missing query correctly', async () => {