Sort By
+ + + > + ) +} +``` + +```jsx filename="pages/dashboard.js" switcher +import { useRouter } from 'next/router' +import { useSearchParams } from 'next/navigation' +import { useCallback } from 'react' + +export default function Dashboard() { + const router = useRouter() + const searchParams = useSearchParams() + + const createQueryString = useCallback( + (name, value) => { + const params = new URLSearchParams(searchParams?.toString()) + params.set(name, value) + return params.toString() + }, + [searchParams] + ) + + if (!searchParams) { + return null + } + + return ( + <> +Sort By
+ + + > + ) +} +``` + +### Sharing components with App Router + +`useSearchParams` from `next/navigation` works in both the Pages Router and App Router. This allows you to create shared components that work in either context: + +```tsx filename="components/search-bar.tsx" switcher +import { useSearchParams } from 'next/navigation' + +// This component works in both pages/ and app/ +export function SearchBar() { + const searchParams = useSearchParams() + + if (!searchParams) { + // Fallback for Pages Router during pre-rendering + return + } + + const search = searchParams.get('search') ?? '' + + return +} +``` + +```jsx filename="components/search-bar.js" switcher +import { useSearchParams } from 'next/navigation' + +// This component works in both pages/ and app/ +export function SearchBar() { + const searchParams = useSearchParams() + + if (!searchParams) { + // Fallback for Pages Router during pre-rendering + return + } + + const search = searchParams.get('search') ?? '' + + return +} +``` + +> **Good to know**: When using this component in the App Router, wrap it in a `Browser Log Forwarding Test
+} diff --git a/test/development/app-dir/browser-log-forwarding/fixtures/error-level/error-level.test.ts b/test/development/app-dir/browser-log-forwarding/fixtures/error-level/error-level.test.ts new file mode 100644 index 000000000000..f320803582e6 --- /dev/null +++ b/test/development/app-dir/browser-log-forwarding/fixtures/error-level/error-level.test.ts @@ -0,0 +1,35 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('browser-log-forwarding error level', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should only forward error logs to terminal', async () => { + const outputIndex = next.cliOutput.length + await next.browser('/') + + await retry(() => { + const output = next.cliOutput.slice(outputIndex) + expect(output).toContain('browser error:') + }) + + // Get final output after logs are forwarded + const output = next.cliOutput.slice(outputIndex) + + // Filter to only browser forwarded logs, excluding hydration noise + const browserLogs = output + .split('\n') + .filter( + (line) => + line.includes('[browser]') && + !line.includes('Next.js hydrate callback fire') + ) + .join('\n') + + expect(browserLogs).toMatchInlineSnapshot( + `"[browser] browser error: this is an error message "` + ) + }) +}) diff --git a/test/e2e/app-dir/cdn-cache-control-header/next.config.js b/test/development/app-dir/browser-log-forwarding/fixtures/error-level/next.config.js similarity index 62% rename from test/e2e/app-dir/cdn-cache-control-header/next.config.js rename to test/development/app-dir/browser-log-forwarding/fixtures/error-level/next.config.js index 57ec2bcaa491..587191004a62 100644 --- a/test/e2e/app-dir/cdn-cache-control-header/next.config.js +++ b/test/development/app-dir/browser-log-forwarding/fixtures/error-level/next.config.js @@ -2,9 +2,8 @@ * @type {import('next').NextConfig} */ const nextConfig = { - cacheComponents: true, experimental: { - cdnCacheControlHeader: 'Surrogate-Control', + browserDebugInfoInTerminal: 'error', }, } diff --git a/test/development/app-dir/browser-log-forwarding/fixtures/verbose-level/app/layout.tsx b/test/development/app-dir/browser-log-forwarding/fixtures/verbose-level/app/layout.tsx new file mode 100644 index 000000000000..08eaa94fdc88 --- /dev/null +++ b/test/development/app-dir/browser-log-forwarding/fixtures/verbose-level/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/development/app-dir/browser-log-forwarding/fixtures/verbose-level/app/page.tsx b/test/development/app-dir/browser-log-forwarding/fixtures/verbose-level/app/page.tsx new file mode 100644 index 000000000000..fe6fba3965b3 --- /dev/null +++ b/test/development/app-dir/browser-log-forwarding/fixtures/verbose-level/app/page.tsx @@ -0,0 +1,15 @@ +'use client' + +import { useEffect } from 'react' + +export default function Page() { + useEffect(() => { + console.log('browser log: this is a log message') + console.info('browser info: this is an info message') + console.warn('browser warn: this is a warning message') + console.error('browser error: this is an error message') + console.debug('browser debug: this is a debug message') + }, []) + + returnBrowser Log Forwarding Test
+} diff --git a/test/development/app-dir/browser-log-forwarding/fixtures/verbose-level/next.config.js b/test/development/app-dir/browser-log-forwarding/fixtures/verbose-level/next.config.js new file mode 100644 index 000000000000..9372e26f4098 --- /dev/null +++ b/test/development/app-dir/browser-log-forwarding/fixtures/verbose-level/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + browserDebugInfoInTerminal: 'verbose', + }, +} + +module.exports = nextConfig diff --git a/test/development/app-dir/browser-log-forwarding/fixtures/verbose-level/verbose-level.test.ts b/test/development/app-dir/browser-log-forwarding/fixtures/verbose-level/verbose-level.test.ts new file mode 100644 index 000000000000..5d593d8144cc --- /dev/null +++ b/test/development/app-dir/browser-log-forwarding/fixtures/verbose-level/verbose-level.test.ts @@ -0,0 +1,45 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('browser-log-forwarding verbose level', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should forward all logs to terminal', async () => { + const outputIndex = next.cliOutput.length + await next.browser('/') + + await retry(() => { + const output = next.cliOutput.slice(outputIndex) + expect(output).toContain('browser error:') + expect(output).toContain('browser warn:') + expect(output).toContain('browser log:') + }) + + // Get final output after logs are forwarded + const output = next.cliOutput.slice(outputIndex) + + // Filter to only browser forwarded logs, excluding noise + const browserLogs = output + .split('\n') + .filter( + (line) => + line.includes('[browser]') && + !line.includes('Next.js hydrate callback fire') && + !line.includes('connected to ws at') && + !line.includes('received ws message') && + !line.includes('Download the React DevTools') && + !line.includes('Next.js page already hydrated') + ) + .join('\n') + + expect(browserLogs).toMatchInlineSnapshot(` + "[browser] browser log: this is a log message (app/page.tsx:7:13) + [browser] browser info: this is an info message (app/page.tsx:8:13) + [browser] browser warn: this is a warning message (app/page.tsx:9:13) + [browser] browser error: this is an error message + [browser] browser debug: this is a debug message (app/page.tsx:11:13)" + `) + }) +}) diff --git a/test/development/app-dir/browser-log-forwarding/fixtures/warn-level/app/layout.tsx b/test/development/app-dir/browser-log-forwarding/fixtures/warn-level/app/layout.tsx new file mode 100644 index 000000000000..08eaa94fdc88 --- /dev/null +++ b/test/development/app-dir/browser-log-forwarding/fixtures/warn-level/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/development/app-dir/browser-log-forwarding/fixtures/warn-level/app/page.tsx b/test/development/app-dir/browser-log-forwarding/fixtures/warn-level/app/page.tsx new file mode 100644 index 000000000000..fe6fba3965b3 --- /dev/null +++ b/test/development/app-dir/browser-log-forwarding/fixtures/warn-level/app/page.tsx @@ -0,0 +1,15 @@ +'use client' + +import { useEffect } from 'react' + +export default function Page() { + useEffect(() => { + console.log('browser log: this is a log message') + console.info('browser info: this is an info message') + console.warn('browser warn: this is a warning message') + console.error('browser error: this is an error message') + console.debug('browser debug: this is a debug message') + }, []) + + returnBrowser Log Forwarding Test
+} diff --git a/test/development/app-dir/browser-log-forwarding/fixtures/warn-level/next.config.js b/test/development/app-dir/browser-log-forwarding/fixtures/warn-level/next.config.js new file mode 100644 index 000000000000..dfd06f543b91 --- /dev/null +++ b/test/development/app-dir/browser-log-forwarding/fixtures/warn-level/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + browserDebugInfoInTerminal: 'warn', + }, +} + +module.exports = nextConfig diff --git a/test/development/app-dir/browser-log-forwarding/fixtures/warn-level/warn-level.test.ts b/test/development/app-dir/browser-log-forwarding/fixtures/warn-level/warn-level.test.ts new file mode 100644 index 000000000000..70b42e824b81 --- /dev/null +++ b/test/development/app-dir/browser-log-forwarding/fixtures/warn-level/warn-level.test.ts @@ -0,0 +1,37 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('browser-log-forwarding warn level', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should forward warn and error logs to terminal', async () => { + const outputIndex = next.cliOutput.length + await next.browser('/') + + await retry(() => { + const output = next.cliOutput.slice(outputIndex) + expect(output).toContain('browser error:') + expect(output).toContain('browser warn:') + }) + + // Get final output after logs are forwarded + const output = next.cliOutput.slice(outputIndex) + + // Filter to only browser forwarded logs, excluding hydration noise + const browserLogs = output + .split('\n') + .filter( + (line) => + line.includes('[browser]') && + !line.includes('Next.js hydrate callback fire') + ) + .join('\n') + + expect(browserLogs).toMatchInlineSnapshot(` + "[browser] browser warn: this is a warning message (app/page.tsx:9:13) + [browser] browser error: this is an error message " + `) + }) +}) diff --git a/test/development/app-dir/isolated-dev-build-strict-route-types/app/layout.tsx b/test/development/app-dir/isolated-dev-build-strict-route-types/app/layout.tsx new file mode 100644 index 000000000000..08eaa94fdc88 --- /dev/null +++ b/test/development/app-dir/isolated-dev-build-strict-route-types/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/development/app-dir/isolated-dev-build-strict-route-types/app/page.tsx b/test/development/app-dir/isolated-dev-build-strict-route-types/app/page.tsx new file mode 100644 index 000000000000..ff7159d9149f --- /dev/null +++ b/test/development/app-dir/isolated-dev-build-strict-route-types/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + returnhello world
+} diff --git a/test/development/app-dir/isolated-dev-build-strict-route-types/isolated-dev-build-strict-route-types.test.ts b/test/development/app-dir/isolated-dev-build-strict-route-types/isolated-dev-build-strict-route-types.test.ts new file mode 100644 index 000000000000..74867182b022 --- /dev/null +++ b/test/development/app-dir/isolated-dev-build-strict-route-types/isolated-dev-build-strict-route-types.test.ts @@ -0,0 +1,52 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('isolated-dev-build with strictRouteTypes', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should use fixed path in next-env.d.ts with strictRouteTypes enabled', async () => { + await retry(async () => { + // next-env.d.ts should use the fixed path .next/types/routes.d.ts + // not the dev-specific path .next/dev/types/routes.d.ts + // even with strictRouteTypes enabled + const nextEnvContent = await next.readFile('next-env.d.ts') + expect(nextEnvContent).toContain('import "./.next/types/routes.d.ts"') + expect(nextEnvContent).not.toContain('.next/dev/types') + + // With strictRouteTypes enabled, next-env.d.ts should NOT have + // additional imports for cache-life, validator, link + // These are now re-exported from the entry file + expect(nextEnvContent).not.toContain('cache-life') + expect(nextEnvContent).not.toContain('validator') + expect(nextEnvContent).not.toContain('link.d.ts') + }) + }) + + it('should create entry file that re-exports strict route type files', async () => { + await retry(async () => { + // The entry file should exist at the fixed path + expect(await next.hasFile('.next/types/routes.d.ts')).toBe(true) + + // The entry file should reference the actual types in .next/dev/types + const entryFileContent = await next.readFile('.next/types/routes.d.ts') + expect(entryFileContent).toContain('route-types.d.ts') + expect(entryFileContent).toContain('../dev/types/') + + // With strictRouteTypes enabled, entry file should also reference + // cache-life.d.ts and validator.ts + expect(entryFileContent).toContain('cache-life.d.ts') + expect(entryFileContent).toContain('validator.ts') + }) + }) + + it('should create strict route type files in .next/dev/types/', async () => { + await retry(async () => { + // Actual type files should be in .next/dev/types/ + expect(await next.hasFile('.next/dev/types/route-types.d.ts')).toBe(true) + expect(await next.hasFile('.next/dev/types/cache-life.d.ts')).toBe(true) + expect(await next.hasFile('.next/dev/types/validator.ts')).toBe(true) + }) + }) +}) diff --git a/test/development/app-dir/isolated-dev-build-strict-route-types/next.config.js b/test/development/app-dir/isolated-dev-build-strict-route-types/next.config.js new file mode 100644 index 000000000000..39e74f88eb0e --- /dev/null +++ b/test/development/app-dir/isolated-dev-build-strict-route-types/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + strictRouteTypes: true, + }, +} + +module.exports = nextConfig diff --git a/test/development/app-dir/isolated-dev-build/isolated-dev-build.test.ts b/test/development/app-dir/isolated-dev-build/isolated-dev-build.test.ts index 97533ad4c8c6..a9504e0c52e2 100644 --- a/test/development/app-dir/isolated-dev-build/isolated-dev-build.test.ts +++ b/test/development/app-dir/isolated-dev-build/isolated-dev-build.test.ts @@ -25,4 +25,33 @@ describe('isolated-dev-build', () => { expect(await browser.elementByCss('p').text()).toBe('hello updated world') }) }) + + it('should use fixed path in next-env.d.ts regardless of isolatedDevBuild', async () => { + await retry(async () => { + // next-env.d.ts should use the fixed path .next/types/routes.d.ts + // not the dev-specific path .next/dev/types/routes.d.ts + const nextEnvContent = await next.readFile('next-env.d.ts') + expect(nextEnvContent).toContain('import "./.next/types/routes.d.ts"') + expect(nextEnvContent).not.toContain('.next/dev/types') + }) + }) + + it('should create entry file at .next/types/routes.d.ts that references dev types', async () => { + await retry(async () => { + // The entry file should exist at the fixed path + expect(await next.hasFile('.next/types/routes.d.ts')).toBe(true) + + // The entry file should reference the actual types in .next/dev/types + const entryFileContent = await next.readFile('.next/types/routes.d.ts') + expect(entryFileContent).toContain('route-types.d.ts') + expect(entryFileContent).toContain('../dev/types/') + }) + }) + + it('should create actual type files in .next/dev/types/', async () => { + await retry(async () => { + // Actual type files should be in .next/dev/types/ + expect(await next.hasFile('.next/dev/types/route-types.d.ts')).toBe(true) + }) + }) }) diff --git a/test/e2e/app-dir/cache-components-segment-configs/cache-components-edge-deduplication.test.ts b/test/e2e/app-dir/cache-components-segment-configs/cache-components-edge-deduplication.test.ts index 893920ef4ed8..2dbfd8c9cf08 100644 --- a/test/e2e/app-dir/cache-components-segment-configs/cache-components-edge-deduplication.test.ts +++ b/test/e2e/app-dir/cache-components-segment-configs/cache-components-edge-deduplication.test.ts @@ -7,6 +7,14 @@ import { getRedboxDescription, getRedboxSource, } from 'next-test-utils' + +// Filter out browser log lines from CLI output to avoid counting forwarded browser errors +function filterBrowserLogs(output: string): string { + return output + .split('\n') + .filter((line) => !line.includes('[browser]')) + .join('\n') +} ;(process.env.IS_TURBOPACK_TEST ? describe : describe.skip)( 'cache-components-edge-deduplication', () => { @@ -49,7 +57,9 @@ import { Route segment config "runtime" is not compatible with \`nextConfig.cacheComponents\`. Please remove it." `) // Count occurrences of the layout error at the specific location - const layoutErrorMatches = next.cliOutput.match( + // Filter out browser logs to avoid counting forwarded browser errors + const filteredOutput = filterBrowserLogs(next.cliOutput) + const layoutErrorMatches = filteredOutput.match( /\.\/app\/edge-with-layout\/layout\.tsx:1:14/g ) // We don't show an error stack, just the individual error messages at each location diff --git a/test/e2e/app-dir/cache-components/app/server-action-inline/form.tsx b/test/e2e/app-dir/cache-components/app/server-action-inline/form.tsx index 4e3c8b9118e6..67b7e7d0931e 100644 --- a/test/e2e/app-dir/cache-components/app/server-action-inline/form.tsx +++ b/test/e2e/app-dir/cache-components/app/server-action-inline/form.tsx @@ -1,9 +1,9 @@ 'use client' -import { useActionState } from 'react' +import { ReactNode, useActionState } from 'react' import { getSentinelValue } from '../getSentinelValue' -export function Form({ action }) { +export function Form({ action }: { action: () => Promise