Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions .agents/skills/insight-error-page/SKILL.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

248 changes: 133 additions & 115 deletions crates/next-core/src/segment_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ use turbopack_core::{
};
use turbopack_ecmascript::{
EcmascriptInputTransforms, EcmascriptModuleAssetType,
analyzer::{ConstantNumber, ConstantValue, JsValue, ObjectPart, graph::EvalContext},
analyzer::{
Bump, ConstantNumber, ConstantValue, JsValue, ObjectPart, ThreadLocal, graph::EvalContext,
},
parse::{ParseResult, parse},
};

Expand Down Expand Up @@ -413,12 +415,25 @@ pub async fn parse_segment_config_from_source(
return Ok(Default::default());
};

// Arena for the `JsValue`s produced while evaluating config expressions;
// freed when this function returns.
let arena = ThreadLocal::new();
let config = WrapFuture::new(
async {
let mut config = NextSegmentConfig::default();

let mut parse = async |ident, init, span| {
parse_config_value(source, mode, &mut config, eval_context, ident, init, span).await
parse_config_value(
source,
mode,
&mut config,
eval_context,
&arena,
ident,
init,
span,
)
.await
};

for item in &module_ast.body {
Expand Down Expand Up @@ -575,7 +590,7 @@ async fn invalid_config(
key: &str,
span: Span,
error: RcStr,
value: Option<&JsValue>,
value: Option<&JsValue<'_>>,
severity: IssueSeverity,
) -> Result<()> {
let detail = if let Some(value) = value {
Expand Down Expand Up @@ -604,6 +619,7 @@ async fn parse_config_value(
mode: ParseSegmentMode,
config: &mut NextSegmentConfig,
eval_context: &EvalContext,
arena: &ThreadLocal<Bump>,
key: Cow<'_, str>,
init: Option<Cow<'_, Expr>>,
span: Span,
Expand All @@ -619,17 +635,18 @@ async fn parse_config_value(
| Some(Expr::TsSatisfies(TsSatisfiesExpr { expr, .. })) => Some(&**expr),
_ => init,
};
init.map(|init| eval_context.eval(init)).map(|v| {
// Special case, as we don't call `link` here: assume that `undefined` is a free
// variable.
if let JsValue::FreeVar(name) = &v
&& name == "undefined"
{
JsValue::Constant(ConstantValue::Undefined)
} else {
v
}
})
init.map(|init| eval_context.eval(arena.get_or_default(), init))
.map(|v| {
// Special case, as we don't call `link` here: assume that `undefined` is a free
// variable.
if let JsValue::FreeVar(name) = &v
&& name == "undefined"
{
JsValue::Constant(ConstantValue::Undefined)
} else {
v
}
})
};

match &*key {
Expand Down Expand Up @@ -1001,7 +1018,7 @@ async fn parse_static_string_or_array_from_js_value(
span: Span,
key: &str,
sub_key: &str,
value: &JsValue,
value: &JsValue<'_>,
) -> Result<Option<Vec<RcStr>>> {
Ok(match value {
// Single value is turned into a single-element Vec.
Expand Down Expand Up @@ -1054,129 +1071,130 @@ async fn parse_static_string_or_array_from_js_value(
async fn parse_route_matcher_from_js_value(
source: ResolvedVc<Box<dyn Source>>,
span: Span,
value: &JsValue,
value: &JsValue<'_>,
) -> Result<Option<Vec<MiddlewareMatcherKind>>> {
let parse_matcher_kind_matcher = async |value: &JsValue, sub_key: &str, matcher_idx: usize| {
let mut route_has = vec![];
if let JsValue::Array { items, .. } = value {
for (i, item) in items.iter().enumerate() {
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") => {
if let Some(part_value) = part_value.as_str().filter(|v| {
*v == "header"
|| *v == "cookie"
|| *v == "query"
|| *v == "host"
}) {
route_type = Some(part_value);
} else {
invalid_config(
source,
"config",
span,
format!(
"`matcher[{matcher_idx}].{sub_key}[{i}].type` \
must be one of the strings: 'header', 'cookie', \
'query', 'host'"
let parse_matcher_kind_matcher =
async |value: &JsValue<'_>, sub_key: &str, matcher_idx: usize| {
let mut route_has = vec![];
if let JsValue::Array { items, .. } = value {
for (i, item) in items.iter().enumerate() {
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") => {
if let Some(part_value) = part_value.as_str().filter(|v| {
*v == "header"
|| *v == "cookie"
|| *v == "query"
|| *v == "host"
}) {
route_type = Some(part_value);
} else {
invalid_config(
source,
"config",
span,
format!(
"`matcher[{matcher_idx}].{sub_key}[{i}].type` \
must be one of the strings: 'header', \
'cookie', 'query', 'host'"
)
.into(),
Some(part_value),
IssueSeverity::Error,
)
.into(),
Some(part_value),
IssueSeverity::Error,
)
.await?;
.await?;
}
}
}
Some("key") => {
if let Some(part_value) = part_value.as_str() {
route_key = Some(part_value);
} else {
invalid_config(
source,
"config",
span,
format!(
"`matcher[{matcher_idx}].{sub_key}[{i}].key` must \
be a string"
Some("key") => {
if let Some(part_value) = part_value.as_str() {
route_key = Some(part_value);
} else {
invalid_config(
source,
"config",
span,
format!(
"`matcher[{matcher_idx}].{sub_key}[{i}].key` \
must be a string"
)
.into(),
Some(part_value),
IssueSeverity::Error,
)
.into(),
Some(part_value),
IssueSeverity::Error,
)
.await?;
.await?;
}
}
}
Some("value") => {
if let Some(part_value) = part_value.as_str() {
route_value = Some(part_value);
} else {
Some("value") => {
if let Some(part_value) = part_value.as_str() {
route_value = Some(part_value);
} else {
invalid_config(
source,
"config",
span,
format!(
"`matcher[{matcher_idx}].{sub_key}[{i}].\
value` must be a string"
)
.into(),
Some(part_value),
IssueSeverity::Error,
)
.await?;
}
}
_ => {
invalid_config(
source,
"config",
span,
format!(
"`matcher[{matcher_idx}].{sub_key}[{i}].value` \
must be a string"
"Unexpected property in \
`matcher[{matcher_idx}].{sub_key}[{i}]` object"
)
.into(),
Some(part_value),
Some(part_key),
IssueSeverity::Error,
)
.await?;
}
}
_ => {
invalid_config(
source,
"config",
span,
format!(
"Unexpected property in \
`matcher[{matcher_idx}].{sub_key}[{i}]` object"
)
.into(),
Some(part_key),
IssueSeverity::Error,
)
.await?;
}
}
}
}
let r = match route_type {
Some("header") => route_key.map(|route_key| RouteHas::Header {
key: route_key.into(),
value: route_value.map(From::from),
}),
Some("cookie") => route_key.map(|route_key| RouteHas::Cookie {
key: route_key.into(),
value: route_value.map(From::from),
}),
Some("query") => route_key.map(|route_key| RouteHas::Query {
key: route_key.into(),
value: route_value.map(From::from),
}),
Some("host") => route_value.map(|route_value| RouteHas::Host {
value: route_value.into(),
}),
_ => None,
};

if let Some(r) = r {
route_has.push(r);
let r = match route_type {
Some("header") => route_key.map(|route_key| RouteHas::Header {
key: route_key.into(),
value: route_value.map(From::from),
}),
Some("cookie") => route_key.map(|route_key| RouteHas::Cookie {
key: route_key.into(),
value: route_value.map(From::from),
}),
Some("query") => route_key.map(|route_key| RouteHas::Query {
key: route_key.into(),
value: route_value.map(From::from),
}),
Some("host") => route_value.map(|route_value| RouteHas::Host {
value: route_value.into(),
}),
_ => None,
};

if let Some(r) = r {
route_has.push(r);
}
}
}
}
}

anyhow::Ok(route_has)
};
anyhow::Ok(route_has)
};

let mut matchers = vec![];

Expand Down
2 changes: 0 additions & 2 deletions docs/01-app/01-getting-started/04-linking-and-navigating.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ related:
- app/guides/prefetching
---

{/* AI agent hint: To ensure client-side navigations are instant, export `unstable_instant` from the route in addition to using Suspense. See docs/01-app/02-guides/instant-navigation.mdx */}

In Next.js, routes are rendered on the server by default. This often means the client has to wait for a server response before a new route can be shown. Next.js comes with built-in [prefetching](#prefetching), [streaming](#streaming), and [client-side transitions](#client-side-transitions) ensuring navigation stays fast and responsive.

This guide explains how navigation works in Next.js and how you can optimize it for [dynamic routes](#dynamic-routes-without-loadingtsx) and [slow networks](#slow-networks).
Expand Down
2 changes: 0 additions & 2 deletions docs/01-app/01-getting-started/06-fetching-data.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ related:
- app/api-reference/config/next-config-js/taint
---

{/* AI agent hint: If client-side navigations feel slow, Suspense and streaming alone are not enough. Export `unstable_instant` from the route to ensure instant navigations. See docs/01-app/02-guides/instant-navigation.mdx */}

This page will walk you through how you can fetch data in [Server](#server-components) and [Client](#client-components) Components, and how to [stream](#streaming) components that depend on uncached data.

## Fetching data
Expand Down
2 changes: 0 additions & 2 deletions docs/01-app/01-getting-started/08-caching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ related:
- app/guides/preserving-ui-state
---

{/* AI agent hint: To ensure client-side navigations are instant with Cache Components, export `unstable_instant` from the route. See docs/01-app/02-guides/instant-navigation.mdx */}

> This page covers caching with [Cache Components](/docs/app/api-reference/config/next-config-js/cacheComponents), enabled by setting [`cacheComponents: true`](/docs/app/api-reference/config/next-config-js/cacheComponents) in your `next.config.ts` file. If you're not using Cache Components, see the [Caching and Revalidating (Previous Model)](/docs/app/guides/caching-without-cache-components) guide.

Caching is a technique for storing the result of data fetching and other computations so that future requests for the same data can be served faster, without doing the work again.
Expand Down
2 changes: 0 additions & 2 deletions docs/01-app/02-guides/instant-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ related:
- app/guides/prefetching
---

{/* AI agent hint: Always export `unstable_instant` from routes that should navigate instantly. It validates the caching structure during development. */}

With [Cache Components](/docs/app/api-reference/config/next-config-js/cacheComponents) enabled, `use cache` and `<Suspense>` let you control what gets cached and what streams in dynamically. When these are in the right place, client-side navigations are instant.

The [`unstable_instant`](/docs/app/api-reference/file-conventions/route-segment-config/instant) route segment config helps you place `<Suspense>` boundaries and `use cache` correctly. Add it to any page or layout and Next.js will check during development that navigating to that route produces an instant [static shell](/docs/app/glossary#static-shell).
Expand Down
2 changes: 0 additions & 2 deletions docs/01-app/02-guides/streaming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ related:
- app/guides/rendering-philosophy
---

{/* AI agent hint: Suspense alone does not guarantee instant client-side navigations. Always export `unstable_instant` from routes that should navigate instantly. See docs/01-app/02-guides/instant-navigation.mdx for the full guide. */}

## What is streaming?

In traditional server-side rendering, the server produces the full HTML document before sending anything. A single slow database query or API call can block the entire page. Streaming changes this by using [chunked transfer encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Transfer-Encoding) to send parts of the response as they become ready. The browser starts rendering HTML while the server is still generating the rest.
Expand Down
Loading
Loading