feat(next-core): support parsing matcher config object (#64678)
### What - closes #63896 PR implements parsing JSValue for the matcher config if given item is an object. We had those types already declared in place but somehow parsing ignores it.
This commit is contained in:
parent
6370155240
commit
c251de878f
6 changed files with 213 additions and 79 deletions
|
@ -5,7 +5,7 @@ use next_core::{
|
||||||
next_edge::entry::wrap_edge_entry,
|
next_edge::entry::wrap_edge_entry,
|
||||||
next_manifests::{EdgeFunctionDefinition, MiddlewareMatcher, MiddlewaresManifestV2},
|
next_manifests::{EdgeFunctionDefinition, MiddlewareMatcher, MiddlewaresManifestV2},
|
||||||
next_server::{get_server_runtime_entries, ServerContextType},
|
next_server::{get_server_runtime_entries, ServerContextType},
|
||||||
util::parse_config_from_source,
|
util::{parse_config_from_source, MiddlewareMatcherKind},
|
||||||
};
|
};
|
||||||
use tracing::Instrument;
|
use tracing::Instrument;
|
||||||
use turbo_tasks::{Completion, Value, Vc};
|
use turbo_tasks::{Completion, Value, Vc};
|
||||||
|
@ -138,9 +138,12 @@ impl MiddlewareEndpoint {
|
||||||
let matchers = if let Some(matchers) = config.await?.matcher.as_ref() {
|
let matchers = if let Some(matchers) = config.await?.matcher.as_ref() {
|
||||||
matchers
|
matchers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|matcher| MiddlewareMatcher {
|
.map(|matcher| match matcher {
|
||||||
original_source: matcher.to_string(),
|
MiddlewareMatcherKind::Str(matchers) => MiddlewareMatcher {
|
||||||
|
original_source: matchers.to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
},
|
||||||
|
MiddlewareMatcherKind::Matcher(matcher) => matcher.clone(),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -4,7 +4,7 @@ use anyhow::{bail, Context, Result};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
use turbo_tasks::{trace::TraceRawVcs, Vc};
|
use turbo_tasks::{trace::TraceRawVcs, TaskInput, Vc};
|
||||||
use turbopack_binding::{
|
use turbopack_binding::{
|
||||||
turbo::{tasks_env::EnvMap, tasks_fs::FileSystemPath},
|
turbo::{tasks_env::EnvMap, tasks_fs::FileSystemPath},
|
||||||
turbopack::{
|
turbopack::{
|
||||||
|
@ -197,7 +197,19 @@ pub enum OutputType {
|
||||||
Export,
|
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")]
|
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||||
pub enum RouteHas {
|
pub enum RouteHas {
|
||||||
Header {
|
Header {
|
||||||
|
|
|
@ -8,7 +8,7 @@ use indexmap::IndexSet;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use turbo_tasks::{trace::TraceRawVcs, TaskInput};
|
use turbo_tasks::{trace::TraceRawVcs, TaskInput};
|
||||||
|
|
||||||
use crate::next_config::{CrossOriginConfig, Rewrites};
|
use crate::next_config::{CrossOriginConfig, Rewrites, RouteHas};
|
||||||
|
|
||||||
#[derive(Serialize, Default, Debug)]
|
#[derive(Serialize, Default, Debug)]
|
||||||
pub struct PagesManifest {
|
pub struct PagesManifest {
|
||||||
|
@ -44,30 +44,20 @@ impl Default for MiddlewaresManifest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(
|
||||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
Debug,
|
||||||
pub enum RouteHas {
|
Clone,
|
||||||
Header {
|
Hash,
|
||||||
key: String,
|
Eq,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
PartialEq,
|
||||||
value: Option<String>,
|
Ord,
|
||||||
},
|
PartialOrd,
|
||||||
Cookie {
|
TaskInput,
|
||||||
key: String,
|
TraceRawVcs,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
Serialize,
|
||||||
value: Option<String>,
|
Deserialize,
|
||||||
},
|
Default,
|
||||||
Query {
|
)]
|
||||||
key: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
value: Option<String>,
|
|
||||||
},
|
|
||||||
Host {
|
|
||||||
value: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Default, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MiddlewareMatcher {
|
pub struct MiddlewareMatcher {
|
||||||
// When skipped next.js with fill that during merging.
|
// When skipped next.js with fill that during merging.
|
||||||
|
|
|
@ -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";
|
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]
|
#[turbo_tasks::value]
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub struct NextSourceConfig {
|
pub struct NextSourceConfig {
|
||||||
pub runtime: NextRuntime,
|
pub runtime: NextRuntime,
|
||||||
|
|
||||||
/// Middleware router matchers
|
/// Middleware router matchers
|
||||||
pub matcher: Option<Vec<String>>,
|
pub matcher: Option<Vec<MiddlewareMatcherKind>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[turbo_tasks::value_impl]
|
#[turbo_tasks::value_impl]
|
||||||
|
@ -215,6 +226,139 @@ impl Issue for NextSourceConfigParsingIssue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn emit_invalid_config_warning(ident: Vc<AssetIdent>, 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<AssetIdent>,
|
||||||
|
value: &JsValue,
|
||||||
|
) -> Option<Vec<MiddlewareMatcherKind>> {
|
||||||
|
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]
|
#[turbo_tasks::function]
|
||||||
pub async fn parse_config_from_source(module: Vc<Box<dyn Module>>) -> Result<Vc<NextSourceConfig>> {
|
pub async fn parse_config_from_source(module: Vc<Box<dyn Module>>) -> Result<Vc<NextSourceConfig>> {
|
||||||
if let Some(ecmascript_asset) =
|
if let Some(ecmascript_asset) =
|
||||||
|
@ -323,19 +467,12 @@ pub async fn parse_config_from_source(module: Vc<Box<dyn Module>>) -> Result<Vc<
|
||||||
|
|
||||||
fn parse_config_from_js_value(module: Vc<Box<dyn Module>>, value: &JsValue) -> NextSourceConfig {
|
fn parse_config_from_js_value(module: Vc<Box<dyn Module>>, value: &JsValue) -> NextSourceConfig {
|
||||||
let mut config = NextSourceConfig::default();
|
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 {
|
if let JsValue::Object { parts, .. } = value {
|
||||||
for part in parts {
|
for part in parts {
|
||||||
match part {
|
match part {
|
||||||
ObjectPart::Spread(_) => invalid_config(
|
ObjectPart::Spread(_) => emit_invalid_config_warning(
|
||||||
|
module.ident(),
|
||||||
"Spread properties are not supported in the config export.",
|
"Spread properties are not supported in the config export.",
|
||||||
value,
|
value,
|
||||||
),
|
),
|
||||||
|
@ -352,7 +489,8 @@ fn parse_config_from_js_value(module: Vc<Box<dyn Module>>, value: &JsValue) -> N
|
||||||
config.runtime = NextRuntime::NodeJs;
|
config.runtime = NextRuntime::NodeJs;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
invalid_config(
|
emit_invalid_config_warning(
|
||||||
|
module.ident(),
|
||||||
"The runtime property must be either \"nodejs\" \
|
"The runtime property must be either \"nodejs\" \
|
||||||
or \"edge\".",
|
or \"edge\".",
|
||||||
value,
|
value,
|
||||||
|
@ -361,48 +499,20 @@ fn parse_config_from_js_value(module: Vc<Box<dyn Module>>, value: &JsValue) -> N
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
invalid_config(
|
emit_invalid_config_warning(
|
||||||
|
module.ident(),
|
||||||
"The runtime property must be a constant string.",
|
"The runtime property must be a constant string.",
|
||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if key == "matcher" {
|
if key == "matcher" {
|
||||||
let mut matchers = vec![];
|
config.matcher =
|
||||||
match value {
|
parse_route_matcher_from_js_value(module.ident(), 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);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
invalid_config(
|
emit_invalid_config_warning(
|
||||||
|
module.ident(),
|
||||||
"The exported config object must not contain non-constant strings.",
|
"The exported config object must not contain non-constant strings.",
|
||||||
key,
|
key,
|
||||||
);
|
);
|
||||||
|
@ -411,7 +521,8 @@ fn parse_config_from_js_value(module: Vc<Box<dyn Module>>, value: &JsValue) -> N
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
invalid_config(
|
emit_invalid_config_warning(
|
||||||
|
module.ident(),
|
||||||
"The exported config object must be a valid object literal.",
|
"The exported config object must be a valid object literal.",
|
||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,16 @@ describe('Middleware custom matchers', () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(res2.headers.get('x-from-middleware')).toBeFalsy()
|
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 () => {
|
it('should match missing query correctly', async () => {
|
||||||
|
|
Loading…
Reference in a new issue