diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index b5774cd7db9b..8d61008625f4 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -36,7 +36,7 @@ export type NextConfigComplete = Required> & { prefetchInlining?: PrefetchInliningConfig // Normalized by config.ts: defaulted to 90% of staticPageGenerationTimeout useCacheTimeout: number - // Normalized by config.ts `finalizeConfig`: defaulted to `'manual-warning'` + // Normalized by config.ts `finalizeConfig`: defaulted to `'warning'` instantInsights: { validationLevel: ValidationLevel } } // The root directory of the distDir. In development mode, this is the parent directory of `distDir` @@ -1086,10 +1086,10 @@ export interface ExperimentalConfig { /** * Controls the validation behavior of Instant Insights * - * - `'warning'`: Validates all navigations for Instant UI in development - * - `'manual-warning'`: Validates navigations for Instant UI in development when configured with `unstable_instant` in Pages and Layouts + * - `'warning'` (default): Validates all navigations for Instant UI in development + * - `'manual-warning'`: Validates navigations for Instant UI in development only when configured with `unstable_instant` in Pages and Layouts * - `'experimental-error'`: Validates all navigations for Instant in development and build. Use with caution. - * - `'experimental-manual-error'`: Validates navigations for Instant UI in developement and build when configured with `unstable_instant` in Pages and Layouts. Use with caution. + * - `'experimental-manual-error'`: Validates navigations for Instant UI in development and build when configured with `unstable_instant` in Pages and Layouts. Use with caution. */ validationLevel?: ValidationLevel } diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index c61f17984c65..bb4e2d59bdf0 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -1618,7 +1618,7 @@ function assignDefaultsAndValidate( function finalizeConfig(config: NextConfigComplete): NextConfigComplete { config.experimental.instantInsights = { validationLevel: - config.experimental.instantInsights?.validationLevel ?? 'manual-warning', + config.experimental.instantInsights?.validationLevel ?? 'warning', } return config } diff --git a/test/development/app-dir/cache-components-dev-errors/next.config.js b/test/development/app-dir/cache-components-dev-errors/next.config.js index e64bae22d658..94fcd455df28 100644 --- a/test/development/app-dir/cache-components-dev-errors/next.config.js +++ b/test/development/app-dir/cache-components-dev-errors/next.config.js @@ -3,6 +3,11 @@ */ const nextConfig = { cacheComponents: true, + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, } module.exports = nextConfig diff --git a/test/development/app-dir/error-overlay/error-ignored-frames/next.config.js b/test/development/app-dir/error-overlay/error-ignored-frames/next.config.js index 807126e4cf0b..3f1322dc956a 100644 --- a/test/development/app-dir/error-overlay/error-ignored-frames/next.config.js +++ b/test/development/app-dir/error-overlay/error-ignored-frames/next.config.js @@ -1,6 +1,12 @@ /** * @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, +} module.exports = nextConfig diff --git a/test/development/app-dir/hmr-iframe/next.config.js b/test/development/app-dir/hmr-iframe/next.config.js new file mode 100644 index 000000000000..3f1322dc956a --- /dev/null +++ b/test/development/app-dir/hmr-iframe/next.config.js @@ -0,0 +1,12 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, +} + +module.exports = nextConfig diff --git a/test/development/app-dir/owner-stack-invalid-element-type/next.config.js b/test/development/app-dir/owner-stack-invalid-element-type/next.config.js index 807126e4cf0b..3f1322dc956a 100644 --- a/test/development/app-dir/owner-stack-invalid-element-type/next.config.js +++ b/test/development/app-dir/owner-stack-invalid-element-type/next.config.js @@ -1,6 +1,12 @@ /** * @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, +} module.exports = nextConfig diff --git a/test/development/app-dir/owner-stack/next.config.js b/test/development/app-dir/owner-stack/next.config.js index 807126e4cf0b..3f1322dc956a 100644 --- a/test/development/app-dir/owner-stack/next.config.js +++ b/test/development/app-dir/owner-stack/next.config.js @@ -1,6 +1,12 @@ /** * @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, +} module.exports = nextConfig diff --git a/test/e2e/app-dir/cache-components-errors/fixtures/default/next.config.js b/test/e2e/app-dir/cache-components-errors/fixtures/default/next.config.js index e64bae22d658..94fcd455df28 100644 --- a/test/e2e/app-dir/cache-components-errors/fixtures/default/next.config.js +++ b/test/e2e/app-dir/cache-components-errors/fixtures/default/next.config.js @@ -3,6 +3,11 @@ */ const nextConfig = { cacheComponents: true, + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, } module.exports = nextConfig diff --git a/test/e2e/app-dir/cache-components-errors/fixtures/http-access-fallback-prerender/next.config.js b/test/e2e/app-dir/cache-components-errors/fixtures/http-access-fallback-prerender/next.config.js index 34aa2b5aed26..e561e049a1eb 100644 --- a/test/e2e/app-dir/cache-components-errors/fixtures/http-access-fallback-prerender/next.config.js +++ b/test/e2e/app-dir/cache-components-errors/fixtures/http-access-fallback-prerender/next.config.js @@ -5,6 +5,9 @@ const nextConfig = { cacheComponents: true, experimental: { authInterrupts: true, + instantInsights: { + validationLevel: 'manual-warning', + }, }, } diff --git a/test/e2e/app-dir/cache-components/next.config.js b/test/e2e/app-dir/cache-components/next.config.js index 86f364bdd422..e83f1cf6305a 100644 --- a/test/e2e/app-dir/cache-components/next.config.js +++ b/test/e2e/app-dir/cache-components/next.config.js @@ -5,6 +5,11 @@ const nextConfig = { cacheComponents: true, adapterPath: process.env.NEXT_ADAPTER_PATH ?? require.resolve('./my-adapter.mjs'), + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, } module.exports = nextConfig diff --git a/test/e2e/app-dir/instant-validation-level-default/app/bare/page.tsx b/test/e2e/app-dir/instant-validation-level-default/app/bare/page.tsx new file mode 100644 index 000000000000..307c49df3a08 --- /dev/null +++ b/test/e2e/app-dir/instant-validation-level-default/app/bare/page.tsx @@ -0,0 +1,16 @@ +// Bare page (no `unstable_instant` config). Under the framework default +// (`'warning'`), implicit validation fires on this page in dev. The runtime +// data accessed at the top of the page is the "Suspense too high for instant +// navigation" violation that instant validation specifically flags. The root +// layout's Suspense satisfies static-shell validation, so the only error in +// dev is the instant one. +import { connection } from 'next/server' + +export default async function Page() { + await connection() + return ( +
+

bare page (no unstable_instant), runtime data at the top.

+
+ ) +} diff --git a/test/e2e/app-dir/instant-validation-level-default/app/explicit-false/page.tsx b/test/e2e/app-dir/instant-validation-level-default/app/explicit-false/page.tsx new file mode 100644 index 000000000000..27e70e2c2631 --- /dev/null +++ b/test/e2e/app-dir/instant-validation-level-default/app/explicit-false/page.tsx @@ -0,0 +1,16 @@ +// Page explicitly opts out of instant validation. Under the framework +// default (`'warning'`), this segment-level override suppresses the +// implicit validation that would otherwise fire on a bare page, so no +// redbox appears. +import { connection } from 'next/server' + +export const unstable_instant = false + +export default async function Page() { + await connection() + return ( +
+

explicit-false page (segment opts out of validation).

+
+ ) +} diff --git a/test/e2e/app-dir/instant-validation-level-default/app/layout.tsx b/test/e2e/app-dir/instant-validation-level-default/app/layout.tsx new file mode 100644 index 000000000000..a32d71748e1c --- /dev/null +++ b/test/e2e/app-dir/instant-validation-level-default/app/layout.tsx @@ -0,0 +1,23 @@ +import { Suspense, type ReactNode } from 'react' + +// Validation level is not set in next.config.ts, so the framework default +// applies. The default is 'warning' — implicit validation fires on bare +// page/default segments in dev only (build is unaffected unless a segment +// explicitly escalates with `level: 'experimental-error'`). +// +// Children are wrapped in Suspense so that pages with runtime data +// accessed at the top of the page don't fail static-shell validation +// (the Suspense fallback renders into the static shell). Instant +// validation flags "Suspense too high for instant navigation" as an +// instant-specific violation when it runs. +export const unstable_instant = false + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + loading…

}>{children}
+ + + ) +} diff --git a/test/e2e/app-dir/instant-validation-level-default/instant-validation-level-default.test.ts b/test/e2e/app-dir/instant-validation-level-default/instant-validation-level-default.test.ts new file mode 100644 index 000000000000..461bf9067e0d --- /dev/null +++ b/test/e2e/app-dir/instant-validation-level-default/instant-validation-level-default.test.ts @@ -0,0 +1,93 @@ +import { nextTestSetup } from 'e2e-utils' +import { expectBuildValidationSkipped } from 'e2e-utils/instant-validation' +import { waitForNoErrorToast } from '../../../lib/next-test-utils' + +// This fixture intentionally omits `experimental.instantInsights` from +// next.config.ts. It pins the framework default for `validationLevel` — +// the framework should resolve the default to `'warning'`, which means +// implicit validation fires on bare pages in dev. If the framework default +// ever changes, this test should fail, alerting whoever changes it. +// +// For exhaustive coverage of explicit levels and per-segment overrides, +// see the sibling `instant-validation-level-{warning,manual-warning,error, +// manual-error}` fixtures. +describe('instant validation - default level', () => { + const { next, skipped, isNextDev, isNextStart, isTurbopack } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + env: { + NEXT_TEST_LOG_VALIDATION: '1', + }, + }) + if (skipped) return + + if (isNextStart && !isTurbopack) { + it.skip('TODO: snapshot tests for webpack', () => {}) + return + } + + if (isNextStart) { + beforeAll(async () => { + await next.build({ args: ['--experimental-build-mode', 'compile'] }) + }) + afterEach(async () => { + await next.stop() + }) + } else { + beforeAll(async () => { + await next.start() + }) + } + + const prerender = async (pathname: string) => { + return await next.build({ + args: [ + '--experimental-build-mode', + 'generate', + '--debug-build-paths', + `app${pathname}/page.tsx`, + ], + }) + } + + if (isNextDev) { + describe('dev', () => { + it('bare page: framework default matches `warning`, implicit validation fires', async () => { + const browser = await next.browser('/bare') + await expect(browser).toDisplayCollapsedRedbox(` + { + "code": "E1264", + "description": "Next.js encountered uncached data during a navigation.", + "environmentLabel": "Server", + "label": "Instant", + "source": "app/bare/page.tsx (10:19) @ Page + > 10 | await connection() + | ^", + "stack": [ + "Page app/bare/page.tsx (10:19)", + ], + } + `) + }) + + it('explicit-false page: per-segment opt-out still works under default', async () => { + const browser = await next.browser('/explicit-false') + await browser.elementByCss('main') + await waitForNoErrorToast(browser, { waitInMs: 500 }) + }) + }) + } else { + describe('build', () => { + it('bare page: framework default is dev-only, build skips validation', async () => { + const result = await prerender('/bare') + expectBuildValidationSkipped(result) + }) + + it('explicit-false page: per-segment opt-out keeps build clean', async () => { + const result = await prerender('/explicit-false') + expectBuildValidationSkipped(result) + }) + }) + } +}) diff --git a/test/e2e/app-dir/instant-validation-level-default/next.config.ts b/test/e2e/app-dir/instant-validation-level-default/next.config.ts new file mode 100644 index 000000000000..9fd0c7a083d4 --- /dev/null +++ b/test/e2e/app-dir/instant-validation-level-default/next.config.ts @@ -0,0 +1,10 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + cacheComponents: true, + typescript: { + ignoreBuildErrors: true, + }, +} + +export default nextConfig diff --git a/test/e2e/app-dir/loader-file-named-export-custom-loader-error/next.config.js b/test/e2e/app-dir/loader-file-named-export-custom-loader-error/next.config.js index 77961243627b..b625c77b5dca 100644 --- a/test/e2e/app-dir/loader-file-named-export-custom-loader-error/next.config.js +++ b/test/e2e/app-dir/loader-file-named-export-custom-loader-error/next.config.js @@ -5,6 +5,11 @@ const nextConfig = { images: { loaderFile: '/dummy-loader.ts', }, + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, } module.exports = nextConfig diff --git a/test/e2e/app-dir/non-root-project-monorepo/apps/web/next.config.js b/test/e2e/app-dir/non-root-project-monorepo/apps/web/next.config.js index 807126e4cf0b..3f1322dc956a 100644 --- a/test/e2e/app-dir/non-root-project-monorepo/apps/web/next.config.js +++ b/test/e2e/app-dir/non-root-project-monorepo/apps/web/next.config.js @@ -1,6 +1,12 @@ /** * @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, +} module.exports = nextConfig diff --git a/test/e2e/app-dir/router-autoscroll/next.config.js b/test/e2e/app-dir/router-autoscroll/next.config.js index 36e8d82ea1af..ad60a12885cd 100644 --- a/test/e2e/app-dir/router-autoscroll/next.config.js +++ b/test/e2e/app-dir/router-autoscroll/next.config.js @@ -1,6 +1,12 @@ /** * @type {import('next').NextConfig} */ -const config = {} +const config = { + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, +} module.exports = config diff --git a/test/e2e/app-dir/server-source-maps/fixtures/default/next.config.js b/test/e2e/app-dir/server-source-maps/fixtures/default/next.config.js index 8a61ee02d1f4..19c0893004fa 100644 --- a/test/e2e/app-dir/server-source-maps/fixtures/default/next.config.js +++ b/test/e2e/app-dir/server-source-maps/fixtures/default/next.config.js @@ -6,6 +6,9 @@ const nextConfig = { experimental: { cpus: 1, serverSourceMaps: true, + instantInsights: { + validationLevel: 'manual-warning', + }, }, serverExternalPackages: ['external-pkg'], } diff --git a/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js b/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js index 86773e2ae9a6..0341e872150f 100644 --- a/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js +++ b/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js @@ -5,6 +5,9 @@ const nextConfig = { experimental: { cpus: 1, serverSourceMaps: true, + instantInsights: { + validationLevel: 'manual-warning', + }, }, } diff --git a/test/e2e/legacy-link-behavior/next.config.js b/test/e2e/legacy-link-behavior/next.config.js index e64bae22d658..94fcd455df28 100644 --- a/test/e2e/legacy-link-behavior/next.config.js +++ b/test/e2e/legacy-link-behavior/next.config.js @@ -3,6 +3,11 @@ */ const nextConfig = { cacheComponents: true, + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, } module.exports = nextConfig diff --git a/test/e2e/next-image-new/app-dir-localpatterns/next.config.js b/test/e2e/next-image-new/app-dir-localpatterns/next.config.js index 10c28b1a185c..635050bf29c1 100644 --- a/test/e2e/next-image-new/app-dir-localpatterns/next.config.js +++ b/test/e2e/next-image-new/app-dir-localpatterns/next.config.js @@ -7,4 +7,9 @@ module.exports = { }, ], }, + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, } diff --git a/test/unit/instant-config-normalization.test.ts b/test/unit/instant-config-normalization.test.ts index e33a8c7811db..670a6d0bbe12 100644 --- a/test/unit/instant-config-normalization.test.ts +++ b/test/unit/instant-config-normalization.test.ts @@ -14,7 +14,7 @@ function uniqueDir(tag: string) { // `experimental.instantInsights.validationLevel` to a concrete value in // one place so consumers don't each need to know the current framework default. describe('experimental.instantInsights validationLevel normalization', () => { - it('defaults to manual-warning when the instantInsights config is absent', async () => { + it('defaults to warning when the instantInsights config is absent', async () => { const config = await loadConfig( PHASE_PRODUCTION_SERVER, uniqueDir('absent'), @@ -23,11 +23,11 @@ describe('experimental.instantInsights validationLevel normalization', () => { } ) expect(config.experimental.instantInsights).toEqual({ - validationLevel: 'manual-warning', + validationLevel: 'warning', }) }) - it('defaults to manual-warning when experimental.instantInsights is an empty object', async () => { + it('defaults to warning when experimental.instantInsights is an empty object', async () => { const config = await loadConfig( PHASE_PRODUCTION_SERVER, uniqueDir('empty'), @@ -36,7 +36,7 @@ describe('experimental.instantInsights validationLevel normalization', () => { } ) expect(config.experimental.instantInsights).toEqual({ - validationLevel: 'manual-warning', + validationLevel: 'warning', }) })