diff --git a/.alexignore b/.alexignore index f57d1811b6289..970f816cffa83 100644 --- a/.alexignore +++ b/.alexignore @@ -4,3 +4,4 @@ examples/ .claude/ AGENTS.md CLAUDE.md +packages/next/dist/docs/ diff --git a/docs/01-app/01-getting-started/04-linking-and-navigating.mdx b/docs/01-app/01-getting-started/04-linking-and-navigating.mdx index 8181f4c7c2679..326fa99251f5a 100644 --- a/docs/01-app/01-getting-started/04-linking-and-navigating.mdx +++ b/docs/01-app/01-getting-started/04-linking-and-navigating.mdx @@ -8,6 +8,8 @@ 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). diff --git a/docs/01-app/01-getting-started/06-fetching-data.mdx b/docs/01-app/01-getting-started/06-fetching-data.mdx index 67d2f2a7837d3..7d5cbdf9a1d7f 100644 --- a/docs/01-app/01-getting-started/06-fetching-data.mdx +++ b/docs/01-app/01-getting-started/06-fetching-data.mdx @@ -12,6 +12,8 @@ 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 diff --git a/docs/01-app/01-getting-started/08-caching.mdx b/docs/01-app/01-getting-started/08-caching.mdx index e12f60f1a7e2e..ee20b1059a3a9 100644 --- a/docs/01-app/01-getting-started/08-caching.mdx +++ b/docs/01-app/01-getting-started/08-caching.mdx @@ -10,6 +10,8 @@ related: - app/api-reference/config/next-config-js/cacheComponents --- +{/* 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. diff --git a/docs/01-app/02-guides/instant-navigation.mdx b/docs/01-app/02-guides/instant-navigation.mdx new file mode 100644 index 0000000000000..307cb61887a3b --- /dev/null +++ b/docs/01-app/02-guides/instant-navigation.mdx @@ -0,0 +1,287 @@ +--- +title: Ensuring instant navigations +description: Learn how to structure your app to prefetch and prerender more content, providing instant page loads and client navigations. +nav_title: Instant navigation +version: draft +related: + title: Learn more + description: Explore the full instant API, caching, and revalidation. + links: + - app/api-reference/file-conventions/route-segment-config/instant + - app/getting-started/caching + - app/getting-started/revalidating + - app/guides/prefetching +--- + +With [Cache Components](/docs/app/api-reference/config/next-config-js/cacheComponents) enabled, wrapping uncached data in `` boundaries produces instant navigations — but only if the boundaries are in the right place. A misplaced boundary can silently block client-side navigations, especially where the entry point varies by shared layout. **Always export `unstable_instant` from routes that should navigate instantly** — it validates the caching structure at dev time and build time, catching issues before they reach users. + +This guide starts with a product page that navigates instantly, then shows how to catch and fix a page where Suspense boundaries are not in the right place. + +## A page that navigates instantly + +A product page at `/store/[slug]` that fetches two things: product details (name, price) and live inventory. Product details rarely change, so they are cached with `use cache`. Inventory must be fresh and streams behind its own `` fallback: + +```tsx filename="app/store/[slug]/page.tsx" highlight={1,12-19,25} +export const unstable_instant = { prefetch: 'static' } + +import { Suspense } from 'react' + +export default async function ProductPage({ + params, +}: { + params: Promise<{ slug: string }> +}) { + return ( +
+ Loading product...

}> + {params.then(({ slug }) => ( + + ))} +
+ Checking availability...

}> + +
+
+ ) +} + +async function ProductInfo({ slug }: { slug: string }) { + 'use cache' + const product = await fetchProduct(slug) + return ( + <> +

{product.name}

+

${product.price}

+ + ) +} + +async function Inventory({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params + const inventory = await fetchInventory(slug) + return

{inventory.count} in stock

+} +``` + +There is no `generateStaticParams`, so `[slug]` is a dynamic segment and `slug` is only known at request time. Awaiting `params` suspends, which is why each component that reads it has its own `` boundary. The `params` Promise is resolved inline with `.then()` so the cached `ProductInfo` receives a plain `slug` string. + +The [`unstable_instant`](/docs/app/api-reference/file-conventions/route-segment-config/instant) export on line 1 tells Next.js to validate that this page produces an instant [static shell](/docs/app/glossary#static-shell) at every possible entry point. Validation runs during development and at build time. If a component would block navigation, the error overlay tells you exactly which one and suggests a fix. + +### Inspect it with the Next.js DevTools + +Enable the Instant Navigation DevTools toggle in your Next.js config: + +```ts filename="next.config.ts" highlight={5-7} +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + cacheComponents: true, + experimental: { + instantNavigationDevToolsToggle: true, + }, +} + +export default nextConfig +``` + +Open the Next.js DevTools and select **Instant Navs**. You will see two options: + +- **Page load**: click **Reload** to refresh the page and freeze it at the initial static UI generated for this route, before any dynamic data streams in. +- **Client navigation**: once enabled, clicking any link in your app shows the prefetched UI for that page instead of the full result. + +Try a **page load**. "Loading product..." and "Checking availability..." appear as separate fallbacks. On the first visit the cache is cold, so both fallbacks are visible. Navigate to the page again and the product name appears immediately from cache. + +Now try a **client navigation** (click a link from `/store/shoes` to `/store/hats`). The product name and price appear immediately (cached). "Checking availability..." shows where inventory will stream in. + +> **Good to know:** Page loads and client navigations can produce different shells. Client-side hooks like `useSearchParams` suspend on page loads (search params are not known at build time) but resolve synchronously on client navigations (the router already has the params). + +
+Why page loads and client navigations produce different shells + +On a page load, the entire page renders from the document root. Every component runs on the server, and anything that suspends is caught by the nearest Suspense boundary in the full tree. + +On a client navigation (link click), Next.js only re-renders below the layout that the source and destination routes share. Components above that shared layout are not re-rendered. This means a Suspense boundary in the root layout covers everything on a page load, but for a client navigation between `/store/shoes` and `/store/hats`, the shared `/store` layout is the entry point. The root Suspense sits above it and has no effect. + +This is also why client-side hooks behave differently. `useSearchParams()` suspends during server rendering because search params are not available at build time. But on a client navigation, the router already has the params from the URL, so the hook resolves synchronously. The same component can appear in the instant shell on a client navigation but behind a fallback on a page load. + +
+ +### Prevent regressions with e2e tests + +Validation catches structural problems during development and at build time. To prevent regressions as the codebase evolves, the `@next/playwright` package includes an `instant()` helper that asserts on exactly what appears in the instant shell: + +```typescript filename="e2e/navigation.test.ts" +import { test, expect } from '@playwright/test' +import { instant } from '@next/playwright' + +test('product title appears instantly', async ({ page }) => { + await page.goto('/store/shoes') + + await instant(page, async () => { + await page.click('a[href="/store/hats"]') + await expect(page.locator('h1')).toContainText('Baseball Cap') + }) + + // After instant() exits, dynamic content streams in + await expect(page.locator('text=in stock')).toBeVisible() +}) +``` + +`instant()` holds back dynamic content while the callback runs against the static shell. After it resolves, dynamic content streams in and you can assert on the full page. + +There is no need to write an `instant()` test for every navigation. Build-time validation already provides the structural guarantee. Use `instant()` for the user flows that matter most. + +## Fixing a page that blocks + +Now consider a different route, `/shop/[slug]`, that has the same data requirements but without local Suspense boundaries or caching: + +```tsx filename="app/shop/[slug]/page.tsx" +export default async function ProductPage({ + params, +}: { + params: Promise<{ slug: string }> +}) { + const { slug } = await params + const product = await fetchProduct(slug) + const inventory = await fetchInventory(slug) + return ( +
+

{product.name}

+

${product.price}

+

{inventory.count} in stock

+
+ ) +} +``` + +The root layout wraps `{children}` in ``: + +```tsx filename="app/layout.tsx" highlight={9-11} +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + Loading...

}>{children}
+ + + ) +} +``` + +On an initial page load, the root Suspense catches the async work and streams the page in behind the fallback. Everything appears to work. But on a client navigation from `/shop/shoes` to `/shop/hats`, the shared `/shop` layout is the entry point. The root `` boundary is above that layout, so it is invisible to this navigation. The page fetches uncached data with no local boundary, so the old page stays visible until the server finishes renderingm making the navigation feel unresponsive. + +### Step 1: Add instant validation + +Add the `unstable_instant` export to surface the problem: + +```tsx filename="app/shop/[slug]/page.tsx" highlight={1} +export const unstable_instant = { prefetch: 'static' } + +export default async function ProductPage({ + params, +}: { + params: Promise<{ slug: string }> +}) { + const { slug } = await params + const product = await fetchProduct(slug) + const inventory = await fetchInventory(slug) + return ( +
+

{product.name}

+

${product.price}

+

{inventory.count} in stock

+
+ ) +} +``` + +Next.js now simulates navigations at every shared layout boundary in the route. Awaiting `params` and both data fetches are flagged as violations because they suspend or access uncached data outside a Suspense boundary. Each error identifies the specific component and suggests a fix. + +### Step 2: Fix the errors + +Look at the data. There is no `generateStaticParams`, so `slug` is only known at request time. Awaiting `params` suspends, so every component that reads it needs its own `` boundary. + +Decide what to do with each fetch: + +- **Product details** (name, price) rarely change. Cache them as a function of `slug` with `use cache`. +- **Inventory** must be fresh from upstream. Leave it uncached and let it stream behind a `` fallback. + +The result is the same structure from the first section: + +```tsx filename="app/shop/[slug]/page.tsx" highlight={1,12-19,25} +export const unstable_instant = { prefetch: 'static' } + +import { Suspense } from 'react' + +export default async function ProductPage({ + params, +}: { + params: Promise<{ slug: string }> +}) { + return ( +
+ Loading product...

}> + {params.then(({ slug }) => ( + + ))} +
+ Checking availability...

}> + +
+
+ ) +} + +async function ProductInfo({ slug }: { slug: string }) { + 'use cache' + const product = await fetchProduct(slug) + return ( + <> +

{product.name}

+

${product.price}

+ + ) +} + +async function Inventory({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params + const inventory = await fetchInventory(slug) + return

{inventory.count} in stock

+} +``` + +Validation passes. Open the DevTools and try a client navigation. The product name and price appear immediately, and "Checking availability..." shows where inventory will stream in. + +
+How validation checks every entry point + +When you add `unstable_instant` to a route, Next.js does not only check the initial page load. It simulates navigations at every possible shared layout boundary in the route hierarchy. + +For a route like `/shop/[slug]`, validation checks: + +- Entry from outside (page load): the full tree renders, root layout Suspense catches everything +- Entry from a sibling under `/shop` (client navigation from `/shop/shoes` to `/shop/hats`): only the page segment re-renders, the `/shop` layout is the entry point + +Each entry point is validated independently. A Suspense boundary that covers one path might be invisible to another. This is why a page can pass the initial load check but fail for sibling navigations, and why catching these issues by hand is difficult as the number of routes grows. + +
+ +## Opting out with `instant = false` + +Not every layout can be instant. A dashboard layout that reads cookies and fetches user-specific data might be too dynamic for the first entry. You can set `instant = false` on that layout to exempt it from validation: + +```tsx filename="app/dashboard/layout.tsx" +export const unstable_instant = false +``` + +This tells validation: do not require that entry into `/dashboard` is instant, but still allows you to validate sibling navigations within it by using `instant` on those inner segments. Navigating from `/dashboard/a` to `/dashboard/b` can still be checked by adding `instant` to the page segments under `/dashboard`. + +## Next steps + +- [`instant` API reference](/docs/app/api-reference/file-conventions/route-segment-config/instant) for all configuration options, including runtime prefetching and incremental adoption with `instant = false` +- [Caching](/docs/app/getting-started/caching) for background on `use cache`, Suspense, and Partial Prerendering +- [Revalidating](/docs/app/getting-started/revalidating) for how to expire cached data with `cacheLife` and `updateTag` diff --git a/docs/01-app/02-guides/streaming.mdx b/docs/01-app/02-guides/streaming.mdx index def96693ef451..15f1e7fb4319e 100644 --- a/docs/01-app/02-guides/streaming.mdx +++ b/docs/01-app/02-guides/streaming.mdx @@ -11,6 +11,8 @@ related: - app/guides/self-hosting --- +{/* 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. diff --git a/docs/01-app/03-api-reference/03-file-conventions/02-route-segment-config/instant.mdx b/docs/01-app/03-api-reference/03-file-conventions/02-route-segment-config/instant.mdx new file mode 100644 index 0000000000000..65bf15c46de57 --- /dev/null +++ b/docs/01-app/03-api-reference/03-file-conventions/02-route-segment-config/instant.mdx @@ -0,0 +1,138 @@ +--- +title: instant +description: API reference for the instant route segment config. +version: draft +related: + title: Next Steps + description: Learn how to use instant navigations in practice. + links: + - app/guides/instant-navigation + - app/getting-started/caching + - app/getting-started/revalidating + - app/api-reference/directives/use-cache +--- + +The `unstable_instant` route segment config opts a route into validation for instant client-side navigations. Next.js checks, during development and at build time, that the caching structure produces an instant [static shell](/docs/app/glossary#static-shell) at every possible entry point into the route. + +> **Good to know**: +> +> - The `unstable_instant` export only works when [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents) is enabled. +> - `unstable_instant` cannot be used in Client Components. It will throw an error. + +```tsx filename="layout.tsx | page.tsx" switcher +export const unstable_instant = { + prefetch: 'static', +} + +export default function Page() { + return
...
+} +``` + +```jsx filename="layout.js | page.js" switcher +export const unstable_instant = { + prefetch: 'static', +} + +export default function Page() { + return
...
+} +``` + +## Reference + +### `prefetch` + +Controls the validation and prefetching mode. + +```tsx filename="page.tsx" +export const unstable_instant = { + prefetch: 'static', +} +``` + +- **`'static'`**: Enables validation. Prefetching behavior stays the same (static by default). Components that read cookies or headers are treated as dynamic and must be behind Suspense. + +### Disabling instant + +Set `false` to exempt a segment from validation: + +```tsx filename="app/dashboard/layout.tsx" +export const unstable_instant = false +``` + +## How validation works + +`unstable_instant` triggers validation at every shared layout boundary in the route. Validation runs during development (on page loads and HMR updates) and at build time. Errors appear in the dev error overlay or fail the build. + +Each error identifies the component that would block navigation. The fix is usually to cache the data with `use cache` or wrap it in a `` boundary. + +## Inspecting loading states + +Enable the DevTools toggle with the experimental flag: + +```js filename="next.config.js" +module.exports = { + experimental: { + instantNavigationDevToolsToggle: true, + }, +} +``` + +Open the Next.js DevTools and select **Instant Navs**. Two options are available: + +- **Page load**: click **Reload** to refresh the page and freeze it at the initial static UI that was generated for this route, before any dynamic data streams in. +- **Client navigation**: once enabled, clicking any link in your app shows the prefetched UI for that page instead of the full result. + +Use both to check that your loading states look right on first visit and on navigation. + +## Testing instant navigation + +The `@next/playwright` package exports an `instant()` helper that holds back dynamic content while the callback runs against the static shell. See the [guide](/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests) for a full example. + +```typescript +import { instant } from '@next/playwright' +``` + +{/* TODO: remove when fixed and from prod-docs-release */} + +## Known issue: shared cookie across projects + +The DevTools use a `next-instant-navigation-testing` cookie to freeze the UI at the static shell. Because cookies are scoped to the domain and not the port, running multiple projects on the same domain (typically `localhost`) means the cookie is shared across them and can cause unexpected behavior. Clear the cookie or close the Instant Navs panel when switching between projects to avoid issues. + +> **Good to know:** This will be fixed as part of stabilizing the feature. + +## TypeScript + +```tsx +type RuntimeSample = { + cookies?: Array<{ name: string; value: string }> + headers?: Array<[string, string]> + params?: Record + searchParams?: Record +} + +type InstantConfig = + | false + | { + prefetch: 'static' + from?: string[] + unstable_disableValidation?: boolean + } + | { + prefetch: 'runtime' + samples: RuntimeSample[] + from?: string[] + unstable_disableValidation?: boolean + } + +export const unstable_instant: InstantConfig = { + prefetch: 'static', +} +``` + +## Version History + +| Version | Changes | +| --------- | ------------------------------------------------------------ | +| `v16.x.x` | `unstable_instant` export introduced (Cache Components only) | diff --git a/docs/01-app/03-api-reference/03-file-conventions/loading.mdx b/docs/01-app/03-api-reference/03-file-conventions/loading.mdx index 20b62e3d34572..e46bcd7a72e69 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/loading.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/loading.mdx @@ -3,6 +3,8 @@ title: loading.js description: API reference for the loading.js file. --- +{/* AI agent hint: loading.js provides fallback UI but does not guarantee instant client-side navigations. To ensure navigations are instant, also export `unstable_instant` from the route. See docs/01-app/02-guides/instant-navigation.mdx */} + The special file `loading.js` helps you create meaningful Loading UI with [React Suspense](https://react.dev/reference/react/Suspense). With this convention, you can show an [instant loading state](#instant-loading-states) from the server while the content of a route segment streams in. The new content is automatically swapped in once complete. { + const productPage = sourceFiles.find( + (f) => f.path.includes('product') && /page\.(tsx?|jsx?)$/.test(f.path) + ) + + expect(productPage).toBeDefined() + expect(productPage!.content).toMatch( + /export\s+(const|var|let)\s+unstable_instant\b/ + ) + expect(productPage!.content).toMatch(/prefetch\s*:\s*['"]static['"]/) +}) diff --git a/evals/evals/agent-040-unstable-instant/PROMPT.md b/evals/evals/agent-040-unstable-instant/PROMPT.md new file mode 100644 index 0000000000000..f8cf59cb07064 --- /dev/null +++ b/evals/evals/agent-040-unstable-instant/PROMPT.md @@ -0,0 +1 @@ +Navigating from home to the product page is slow. The title should appear immediately. diff --git a/evals/evals/agent-040-unstable-instant/app/layout.tsx b/evals/evals/agent-040-unstable-instant/app/layout.tsx new file mode 100644 index 0000000000000..af78e3aaeec31 --- /dev/null +++ b/evals/evals/agent-040-unstable-instant/app/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'Shop', + description: 'E-commerce product store', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + {children} + + ) +} diff --git a/evals/evals/agent-040-unstable-instant/app/page.tsx b/evals/evals/agent-040-unstable-instant/app/page.tsx new file mode 100644 index 0000000000000..cac908f8fe570 --- /dev/null +++ b/evals/evals/agent-040-unstable-instant/app/page.tsx @@ -0,0 +1,11 @@ +import Link from 'next/link' + +export default function Home() { + return ( +
+

Home

+

Welcome to our store!

+ View Product +
+ ) +} diff --git a/evals/evals/agent-040-unstable-instant/app/product/page.tsx b/evals/evals/agent-040-unstable-instant/app/product/page.tsx new file mode 100644 index 0000000000000..ec5859e55a974 --- /dev/null +++ b/evals/evals/agent-040-unstable-instant/app/product/page.tsx @@ -0,0 +1,13 @@ +import { getInventory } from '@/lib/data' + +export default async function ProductPage() { + const inventory = await getInventory() + + return ( +
+

Premium Widget

+

{inventory.count} in stock

+

Price: ${inventory.price}

+
+ ) +} diff --git a/evals/evals/agent-040-unstable-instant/lib/data.ts b/evals/evals/agent-040-unstable-instant/lib/data.ts new file mode 100644 index 0000000000000..497a18ec2994f --- /dev/null +++ b/evals/evals/agent-040-unstable-instant/lib/data.ts @@ -0,0 +1,9 @@ +import { connection } from 'next/server' + +export async function getInventory() { + await connection() + return { + count: Math.floor(Math.random() * 50), + price: (Math.random() * 100).toFixed(2), + } +} diff --git a/evals/evals/agent-040-unstable-instant/next.config.ts b/evals/evals/agent-040-unstable-instant/next.config.ts new file mode 100644 index 0000000000000..fa33c7c54f24c --- /dev/null +++ b/evals/evals/agent-040-unstable-instant/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + cacheComponents: true, +} + +export default nextConfig diff --git a/evals/evals/agent-040-unstable-instant/package.json b/evals/evals/agent-040-unstable-instant/package.json new file mode 100644 index 0000000000000..97f5fdec21f92 --- /dev/null +++ b/evals/evals/agent-040-unstable-instant/package.json @@ -0,0 +1,23 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "^16", + "react": "19.1.0", + "react-dom": "19.1.0" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^4.4.1", + "typescript": "^5", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.1.3" + } +} diff --git a/evals/evals/agent-040-unstable-instant/tsconfig.json b/evals/evals/agent-040-unstable-instant/tsconfig.json new file mode 100644 index 0000000000000..64c21044c49f6 --- /dev/null +++ b/evals/evals/agent-040-unstable-instant/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + }, + "target": "ES2017" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/lerna.json b/lerna.json index aada7b55dc77b..ced1cca475ea7 100644 --- a/lerna.json +++ b/lerna.json @@ -15,5 +15,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "16.2.0-canary.97" + "version": "16.2.0-canary.98" } \ No newline at end of file diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 1afe3188658ce..a313306467d13 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 18b7f23fd4422..65d83662c7a61 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "description": "ESLint configuration used by Next.js.", "license": "MIT", "repository": { @@ -12,7 +12,7 @@ "dist" ], "dependencies": { - "@next/eslint-plugin-next": "16.2.0-canary.97", + "@next/eslint-plugin-next": "16.2.0-canary.98", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 2b6dc6cdb9b98..1e50019ed3555 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,7 +1,7 @@ { "name": "@next/eslint-plugin-internal", "private": true, - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "description": "ESLint plugin for working on Next.js.", "exports": { ".": "./src/eslint-plugin-internal.js" diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 3bcaa4c3c75c4..acd17c339a532 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/font/package.json b/packages/font/package.json index d1867df1026bd..40cc8fb449ff7 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 775100ff5694c..d33d26e53026f 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 9c7eb7f7d0418..f5946e793fc7f 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 5b6654922b4fa..3f6a6b4bd5048 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index dc0d33a422798..3aaedf83c30bb 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-playwright/package.json b/packages/next-playwright/package.json index 5a29215d3bd0c..1f3e5309c8129 100644 --- a/packages/next-playwright/package.json +++ b/packages/next-playwright/package.json @@ -1,6 +1,6 @@ { "name": "@next/playwright", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "repository": { "url": "vercel/next.js", "directory": "packages/next-playwright" diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index ba4e35995468b..9a2d89ece616c 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 66d4891940edb..a29de2ed237be 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 49f9c5541bd36..dc83bbc8a80d1 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-routing/package.json b/packages/next-routing/package.json index 2e3aa12bb7af2..0609939b44d6a 100644 --- a/packages/next-routing/package.json +++ b/packages/next-routing/package.json @@ -1,6 +1,6 @@ { "name": "@next/routing", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "keywords": [ "react", "next", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index b3f280abd3246..7f898be6112ee 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -1,6 +1,6 @@ { "name": "next-rspack", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 226a789e93894..9a044c00f58ae 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "private": true, "files": [ "native/" diff --git a/packages/next/package.json b/packages/next/package.json index 905ef6b879b47..935a44594b27a 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -97,7 +97,7 @@ ] }, "dependencies": { - "@next/env": "16.2.0-canary.97", + "@next/env": "16.2.0-canary.98", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -161,11 +161,11 @@ "@modelcontextprotocol/sdk": "1.18.1", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/font": "16.2.0-canary.97", - "@next/polyfill-module": "16.2.0-canary.97", - "@next/polyfill-nomodule": "16.2.0-canary.97", - "@next/react-refresh-utils": "16.2.0-canary.97", - "@next/swc": "16.2.0-canary.97", + "@next/font": "16.2.0-canary.98", + "@next/polyfill-module": "16.2.0-canary.98", + "@next/polyfill-nomodule": "16.2.0-canary.98", + "@next/react-refresh-utils": "16.2.0-canary.98", + "@next/swc": "16.2.0-canary.98", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.51.1", "@rspack/core": "1.6.7", diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index af6ce81056d70..c5b88186e3809 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -32,9 +32,18 @@ export async function copy_regenerator_runtime(task, opts) { } export async function copy_docs(task, opts) { - // Copy documentation from repo root into the package + // Copy documentation from repo root into the package. + // Rename .mdx → .md so AI agents find them when globbing for *.md. const docsSource = join(__dirname, '../../docs') - await task.source(join(docsSource, '**/*')).target('dist/docs') + await task + .source(join(docsSource, '**/*')) + // eslint-disable-next-line require-yield + .run({ every: true }, function* (file) { + if (file.base.endsWith('.mdx')) { + file.base = file.base.replace(/\.mdx$/, '.md') + } + }) + .target('dist/docs') } export async function copy_styled_jsx_assets(task, opts) { diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index d80f114545b81..cee743e546180 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index 4bcfd498f8498..5f828f968e43c 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "16.2.0-canary.97", + "version": "16.2.0-canary.98", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "16.2.0-canary.97", + "next": "16.2.0-canary.98", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "5.9.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dc98490cf3f2..93d91e96b04ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1017,7 +1017,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 16.2.0-canary.97 + specifier: 16.2.0-canary.98 version: link:../eslint-plugin-next eslint: specifier: '>=9.0.0' @@ -1094,7 +1094,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 16.2.0-canary.97 + specifier: 16.2.0-canary.98 version: link:../next-env '@swc/helpers': specifier: 0.5.15 @@ -1219,19 +1219,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 16.2.0-canary.97 + specifier: 16.2.0-canary.98 version: link:../font '@next/polyfill-module': - specifier: 16.2.0-canary.97 + specifier: 16.2.0-canary.98 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 16.2.0-canary.97 + specifier: 16.2.0-canary.98 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 16.2.0-canary.97 + specifier: 16.2.0-canary.98 version: link:../react-refresh-utils '@next/swc': - specifier: 16.2.0-canary.97 + specifier: 16.2.0-canary.98 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1956,7 +1956,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 16.2.0-canary.97 + specifier: 16.2.0-canary.98 version: link:../next outdent: specifier: 0.8.0 diff --git a/test/e2e/app-dir/sub-shell-generation-middleware/sub-shell-generation-middleware.test.ts b/test/e2e/app-dir/sub-shell-generation-middleware/sub-shell-generation-middleware.test.ts index 7f13890b2d1d5..4410498800547 100644 --- a/test/e2e/app-dir/sub-shell-generation-middleware/sub-shell-generation-middleware.test.ts +++ b/test/e2e/app-dir/sub-shell-generation-middleware/sub-shell-generation-middleware.test.ts @@ -3,9 +3,14 @@ import * as cheerio from 'cheerio' import { getCacheHeader, retry } from 'next-test-utils' import { computeCacheBustingSearchParam } from 'next/dist/shared/lib/router/utils/cache-busting-search-param' +const isAdapterTest = Boolean(process.env.NEXT_ENABLE_ADAPTER) + describe('middleware-static-rewrite', () => { const { next, isNextDeploy, isNextDev } = nextTestSetup({ files: __dirname, + // The latest changes to support this behavior on deployed infra are available in the adapter, + // and are not being backported to the CLI + skipDeployment: !isAdapterTest, }) if (isNextDev) { diff --git a/test/e2e/app-dir/sub-shell-generation/sub-shell-generation.test.ts b/test/e2e/app-dir/sub-shell-generation/sub-shell-generation.test.ts index 54e3e99fdcfa5..925702ab2cd3d 100644 --- a/test/e2e/app-dir/sub-shell-generation/sub-shell-generation.test.ts +++ b/test/e2e/app-dir/sub-shell-generation/sub-shell-generation.test.ts @@ -1,9 +1,14 @@ import { nextTestSetup } from 'e2e-utils' import * as cheerio from 'cheerio' +const isAdapterTest = Boolean(process.env.NEXT_ENABLE_ADAPTER) + describe('sub-shell-generation', () => { const { next, isNextDev, isNextDeploy } = nextTestSetup({ files: __dirname, + // The latest changes to support this behavior on deployed infra are available in the adapter, + // and are not being backported to the CLI + skipDeployment: !isAdapterTest, }) if (isNextDev) {