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:
OJ Kwon 2024-04-23 00:55:24 -07:00 committed by GitHub
parent 6370155240
commit c251de878f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 213 additions and 79 deletions

View file

@ -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 {

View file

@ -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 {

View file

@ -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<String>,
},
Cookie {
key: String,
#[serde(skip_serializing_if = "Option::is_none")]
value: Option<String>,
},
Query {
key: String,
#[serde(skip_serializing_if = "Option::is_none")]
value: Option<String>,
},
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.

View file

@ -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<Vec<String>>,
pub matcher: Option<Vec<MiddlewareMatcherKind>>,
}
#[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]
pub async fn parse_config_from_source(module: Vc<Box<dyn Module>>) -> Result<Vc<NextSourceConfig>> {
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 {
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<Box<dyn Module>>, 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<Box<dyn Module>>, 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<Box<dyn Module>>, value: &JsValue) -> N
}
}
} else {
invalid_config(
emit_invalid_config_warning(
module.ident(),
"The exported config object must be a valid object literal.",
value,
);

View file

@ -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' },
],
},
],
}

View file

@ -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 () => {