From c52cb5ad8305c366d9088396b0e6b439f65d634a Mon Sep 17 00:00:00 2001 From: Jimmy Lai Date: Wed, 10 Jan 2024 00:26:24 +0100 Subject: [PATCH] feat(app): add `experimental.missingSuspenseWithCSRBailout` (#57642) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What? This PR adds a new flag called `experimental.missingSuspenseWithCSRBailout`. ### Why? Via this PR we can break a build when calling `useSearchParams` without wrapping it in a suspense boundary. If no suspense boundaries are present, Next.js must avoid doing SSR and defer the entire page's rendering to the client. This is not a great default. Instead, we will now break the build so that you are forced to add a boundary. ### How? Add an experimental flag. If a `BailoutToCSRError` error is thrown and this flag is enabled, the build should fail and log an error, instead of showing a warning and bail the entire page to client-side rendering. Closes NEXT-1770 --------- Co-authored-by: Balázs Orbán Co-authored-by: Wyatt Johnson --- errors/missing-suspense-with-csr-bailout.mdx | 15 + .../components/bailout-to-client-rendering.ts | 13 +- .../next/src/client/components/navigation.ts | 2 +- .../next/src/client/on-recoverable-error.ts | 6 +- .../export/helpers/is-dynamic-usage-error.ts | 2 - packages/next/src/export/index.ts | 6 +- packages/next/src/export/routes/pages.ts | 14 +- packages/next/src/export/worker.ts | 7 +- .../next/src/server/app-render/app-render.tsx | 19 +- .../app-render/create-error-handler.tsx | 8 +- packages/next/src/server/app-render/types.ts | 2 +- ...static-generation-async-storage-wrapper.ts | 2 +- packages/next/src/server/config-schema.ts | 1 + packages/next/src/server/config-shared.ts | 11 + .../shared/lib/lazy-dynamic/bailout-to-csr.ts | 14 + .../lazy-dynamic/dynamic-bailout-to-csr.tsx | 21 + .../lib/lazy-dynamic/dynamic-no-ssr.tsx | 14 - .../src/shared/lib/lazy-dynamic/loadable.tsx | 46 +- .../shared/lib/lazy-dynamic/no-ssr-error.ts | 13 - .../e2e/app-dir/app-static/app-static.test.ts | 496 ++++++++---------- .../use-search-params/static-bailout/page.js | 10 - test/e2e/app-dir/app-static/next.config.js | 2 +- .../hooks/app/hooks/use-search-params/page.js | 9 + .../app-dir/not-found-default/app/layout.js | 4 +- .../app/layout-no-suspense.js | 8 + .../app-dir/use-search-params/app/layout.js | 14 + .../e2e/app-dir/use-search-params/app/page.js | 8 + .../app-dir/use-search-params/next.config.js | 8 + .../use-search-params.test.ts | 35 ++ test/lib/next-modes/base.ts | 48 +- 30 files changed, 471 insertions(+), 387 deletions(-) create mode 100644 errors/missing-suspense-with-csr-bailout.mdx create mode 100644 packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts create mode 100644 packages/next/src/shared/lib/lazy-dynamic/dynamic-bailout-to-csr.tsx delete mode 100644 packages/next/src/shared/lib/lazy-dynamic/dynamic-no-ssr.tsx delete mode 100644 packages/next/src/shared/lib/lazy-dynamic/no-ssr-error.ts delete mode 100644 test/e2e/app-dir/app-static/app/hooks/use-search-params/static-bailout/page.js create mode 100644 test/e2e/app-dir/use-search-params/app/layout-no-suspense.js create mode 100644 test/e2e/app-dir/use-search-params/app/layout.js create mode 100644 test/e2e/app-dir/use-search-params/app/page.js create mode 100644 test/e2e/app-dir/use-search-params/next.config.js create mode 100644 test/e2e/app-dir/use-search-params/use-search-params.test.ts diff --git a/errors/missing-suspense-with-csr-bailout.mdx b/errors/missing-suspense-with-csr-bailout.mdx new file mode 100644 index 0000000000..bf5aa02a01 --- /dev/null +++ b/errors/missing-suspense-with-csr-bailout.mdx @@ -0,0 +1,15 @@ +--- +title: Missing Suspense with CSR Bailout +--- + +#### Why This Error Occurred + +Certain methods like `useSearchParams()` opt Next.js into client-side rendering. Without a suspense boundary, this will opt the entire page into client-side rendering, which is likely not intended. + +#### Possible Ways to Fix It + +Make sure that the method is wrapped in a suspense boundary. This way Next.js will only opt the component into client-side rendering up to the suspense boundary. + +### Useful Links + +- [`useSearchParams`](https://nextjs.org/docs/app/api-reference/functions/use-search-params) diff --git a/packages/next/src/client/components/bailout-to-client-rendering.ts b/packages/next/src/client/components/bailout-to-client-rendering.ts index 6af1fe9dab..cf2317d989 100644 --- a/packages/next/src/client/components/bailout-to-client-rendering.ts +++ b/packages/next/src/client/components/bailout-to-client-rendering.ts @@ -1,14 +1,11 @@ -import { throwWithNoSSR } from '../../shared/lib/lazy-dynamic/no-ssr-error' +import { BailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' import { staticGenerationAsyncStorage } from './static-generation-async-storage.external' -export function bailoutToClientRendering(): void | never { +export function bailoutToClientRendering(reason: string): void | never { const staticGenerationStore = staticGenerationAsyncStorage.getStore() - if (staticGenerationStore?.forceStatic) { - return - } + if (staticGenerationStore?.forceStatic) return - if (staticGenerationStore?.isStaticGeneration) { - throwWithNoSSR() - } + if (staticGenerationStore?.isStaticGeneration) + throw new BailoutToCSRError(reason) } diff --git a/packages/next/src/client/components/navigation.ts b/packages/next/src/client/components/navigation.ts index 8290091e41..a391ef23b4 100644 --- a/packages/next/src/client/components/navigation.ts +++ b/packages/next/src/client/components/navigation.ts @@ -92,7 +92,7 @@ export function useSearchParams(): ReadonlyURLSearchParams { const { bailoutToClientRendering } = require('./bailout-to-client-rendering') as typeof import('./bailout-to-client-rendering') // TODO-APP: handle dynamic = 'force-static' here and on the client - bailoutToClientRendering() + bailoutToClientRendering('useSearchParams()') } return readonlySearchParams diff --git a/packages/next/src/client/on-recoverable-error.ts b/packages/next/src/client/on-recoverable-error.ts index c22e5eec89..43ba74f341 100644 --- a/packages/next/src/client/on-recoverable-error.ts +++ b/packages/next/src/client/on-recoverable-error.ts @@ -1,6 +1,6 @@ -import { isBailoutCSRError } from '../shared/lib/lazy-dynamic/no-ssr-error' +import { isBailoutToCSRError } from '../shared/lib/lazy-dynamic/bailout-to-csr' -export default function onRecoverableError(err: any) { +export default function onRecoverableError(err: unknown) { // Using default react onRecoverableError // x-ref: https://github.com/facebook/react/blob/d4bc16a7d69eb2ea38a88c8ac0b461d5f72cdcab/packages/react-dom/src/client/ReactDOMRoot.js#L83 const defaultOnRecoverableError = @@ -13,7 +13,7 @@ export default function onRecoverableError(err: any) { } // Skip certain custom errors which are not expected to be reported on client - if (isBailoutCSRError(err)) return + if (isBailoutToCSRError(err)) return defaultOnRecoverableError(err) } diff --git a/packages/next/src/export/helpers/is-dynamic-usage-error.ts b/packages/next/src/export/helpers/is-dynamic-usage-error.ts index deede653d1..0b5c535660 100644 --- a/packages/next/src/export/helpers/is-dynamic-usage-error.ts +++ b/packages/next/src/export/helpers/is-dynamic-usage-error.ts @@ -1,10 +1,8 @@ import { DYNAMIC_ERROR_CODE } from '../../client/components/hooks-server-context' import { isNotFoundError } from '../../client/components/not-found' import { isRedirectError } from '../../client/components/redirect' -import { isBailoutCSRError } from '../../shared/lib/lazy-dynamic/no-ssr-error' export const isDynamicUsageError = (err: any) => err.digest === DYNAMIC_ERROR_CODE || isNotFoundError(err) || - isBailoutCSRError(err) || isRedirectError(err) diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index d99993f196..e5ba675d59 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -506,7 +506,11 @@ export async function exportAppImpl( : {}), strictNextHead: !!nextConfig.experimental.strictNextHead, deploymentId: nextConfig.experimental.deploymentId, - experimental: { ppr: nextConfig.experimental.ppr === true }, + experimental: { + ppr: nextConfig.experimental.ppr === true, + missingSuspenseWithCSRBailout: + nextConfig.experimental.missingSuspenseWithCSRBailout, + }, } const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig diff --git a/packages/next/src/export/routes/pages.ts b/packages/next/src/export/routes/pages.ts index 936b5159c9..15329d564f 100644 --- a/packages/next/src/export/routes/pages.ts +++ b/packages/next/src/export/routes/pages.ts @@ -15,7 +15,7 @@ import { NEXT_DATA_SUFFIX, SERVER_PROPS_EXPORT_ERROR, } from '../../lib/constants' -import { isBailoutCSRError } from '../../shared/lib/lazy-dynamic/no-ssr-error' +import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' import AmpHtmlValidator from 'next/dist/compiled/amphtml-validator' import { FileType, fileExists } from '../../lib/file-exists' import { lazyRenderPagesPage } from '../../server/future/route-modules/pages/module.render' @@ -105,10 +105,8 @@ export async function exportPages( query, renderOpts ) - } catch (err: any) { - if (!isBailoutCSRError(err)) { - throw err - } + } catch (err) { + if (!isBailoutToCSRError(err)) throw err } } @@ -163,10 +161,8 @@ export async function exportPages( { ...query, amp: '1' }, renderOpts ) - } catch (err: any) { - if (!isBailoutCSRError(err)) { - throw err - } + } catch (err) { + if (!isBailoutToCSRError(err)) throw err } const ampHtml = diff --git a/packages/next/src/export/worker.ts b/packages/next/src/export/worker.ts index 5d2fd08578..7ba65044f7 100644 --- a/packages/next/src/export/worker.ts +++ b/packages/next/src/export/worker.ts @@ -35,6 +35,7 @@ import { createIncrementalCache } from './helpers/create-incremental-cache' import { isPostpone } from '../server/lib/router-utils/is-postpone' import { isMissingPostponeDataError } from '../server/app-render/is-missing-postpone-error' import { isDynamicUsageError } from './helpers/is-dynamic-usage-error' +import { isBailoutToCSRError } from '../shared/lib/lazy-dynamic/bailout-to-csr' const envConfig = require('../shared/lib/runtime-config.external') @@ -318,9 +319,11 @@ async function exportPageImpl( // if this is a postpone error, it's logged elsewhere, so no need to log it again here if (!isMissingPostponeDataError(err)) { console.error( - `\nError occurred prerendering page "${path}". Read more: https://nextjs.org/docs/messages/prerender-error\n` + - (isError(err) && err.stack ? err.stack : err) + `\nError occurred prerendering page "${path}". Read more: https://nextjs.org/docs/messages/prerender-error\n` ) + if (!isBailoutToCSRError(err)) { + console.error(isError(err) && err.stack ? err.stack : err) + } } return { error: true } diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 5c62fecf83..36e0dc72f4 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -63,7 +63,7 @@ import { parseAndValidateFlightRouterState } from './parse-and-validate-flight-r import { validateURL } from './validate-url' import { createFlightRouterStateFromLoaderTree } from './create-flight-router-state-from-loader-tree' import { handleAction } from './action-handler' -import { isBailoutCSRError } from '../../shared/lib/lazy-dynamic/no-ssr-error' +import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' import { warn, error } from '../../build/output/log' import { appendMutableCookies } from '../web/spec-extension/adapters/request-cookies' import { createServerInsertedHTML } from './server-inserted-html' @@ -996,12 +996,19 @@ async function renderToHTMLOrFlightImpl( throw err } - // True if this error was a bailout to client side rendering error. - const shouldBailoutToCSR = isBailoutCSRError(err) + /** True if this error was a bailout to client side rendering error. */ + const shouldBailoutToCSR = isBailoutToCSRError(err) if (shouldBailoutToCSR) { + console.log() + + if (renderOpts.experimental.missingSuspenseWithCSRBailout) { + error( + `${err.message} should be wrapped in a suspense boundary at page "${pagePath}". https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout` + ) + throw err + } warn( - `Entire page ${pagePath} deopted into client-side rendering. https://nextjs.org/docs/messages/deopted-into-client-rendering`, - pagePath + `Entire page "${pagePath}" deopted into client-side rendering. https://nextjs.org/docs/messages/deopted-into-client-rendering` ) } @@ -1212,7 +1219,7 @@ async function renderToHTMLOrFlightImpl( renderOpts.experimental.ppr && staticGenerationStore.postponeWasTriggered && !metadata.postponed && - (!response.err || !isBailoutCSRError(response.err)) + (!response.err || !isBailoutToCSRError(response.err)) ) { // a call to postpone was made but was caught and not detected by Next.js. We should fail the build immediately // as we won't be able to generate the static part diff --git a/packages/next/src/server/app-render/create-error-handler.tsx b/packages/next/src/server/app-render/create-error-handler.tsx index 60ea9fac2d..126823ce92 100644 --- a/packages/next/src/server/app-render/create-error-handler.tsx +++ b/packages/next/src/server/app-render/create-error-handler.tsx @@ -3,6 +3,7 @@ import { formatServerError } from '../../lib/format-server-error' import { SpanStatusCode, getTracer } from '../lib/trace/tracer' import { isAbortError } from '../pipe-readable' import { isDynamicUsageError } from '../../export/helpers/is-dynamic-usage-error' +import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' export type ErrorHandler = (err: any) => string | undefined @@ -34,11 +35,12 @@ export function createErrorHandler({ return (err) => { if (allCapturedErrors) allCapturedErrors.push(err) + // A formatted error is already logged for this type of error + if (isBailoutToCSRError(err)) return + // These errors are expected. We return the digest // so that they can be properly handled. - if (isDynamicUsageError(err)) { - return err.digest - } + if (isDynamicUsageError(err)) return err.digest // If the response was closed, we don't need to log the error. if (isAbortError(err)) return diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index 3cd3e1d2c8..a460d9d4f3 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -142,7 +142,7 @@ export interface RenderOptsPartial { } params?: ParsedUrlQuery isPrefetch?: boolean - experimental: { ppr: boolean } + experimental: { ppr: boolean; missingSuspenseWithCSRBailout?: boolean } postponed?: string } diff --git a/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts b/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts index f3068d3f39..70c25d5b8b 100644 --- a/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts +++ b/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts @@ -18,7 +18,7 @@ export type StaticGenerationContext = { isDraftMode?: boolean isServerAction?: boolean waitUntil?: Promise - experimental: { ppr: boolean } + experimental: { ppr: boolean; missingSuspenseWithCSRBailout?: boolean } /** * A hack around accessing the store value outside the context of the diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index bd6d207bc4..032c185731 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -369,6 +369,7 @@ export const configSchema: zod.ZodType = z.lazy(() => staticWorkerRequestDeduping: z.boolean().optional(), useWasmBinary: z.boolean().optional(), useLightningcss: z.boolean().optional(), + missingSuspenseWithCSRBailout: z.boolean().optional(), }) .optional(), exportPathMap: z diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index e2b60bf513..70566a5e49 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -351,6 +351,16 @@ export interface ExperimentalConfig { * Use lightningcss instead of swc_css */ useLightningcss?: boolean + + /** + * Certain methods calls like `useSearchParams()` can bail out of server-side rendering of **entire** pages to client-side rendering, + * if they are not wrapped in a suspense boundary. + * + * When this flag is set to `true`, Next.js will break the build instead of warning, to force the developer to add a suspense boundary above the method call. + * + * @default false + */ + missingSuspenseWithCSRBailout?: boolean } export type ExportPathMap = { @@ -811,6 +821,7 @@ export const defaultConfig: NextConfig = { ? true : false, webpackBuildWorker: undefined, + missingSuspenseWithCSRBailout: false, }, } diff --git a/packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts b/packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts new file mode 100644 index 0000000000..24de76c203 --- /dev/null +++ b/packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts @@ -0,0 +1,14 @@ +// This has to be a shared module which is shared between client component error boundary and dynamic component + +const BAILOUT_TO_CSR = 'BAILOUT_TO_CLIENT_SIDE_RENDERING' + +/** An error that should be thrown when we want to bail out to client-side rendering. */ +export class BailoutToCSRError extends Error { + digest: typeof BAILOUT_TO_CSR = BAILOUT_TO_CSR +} + +/** Checks if a passed argument is an error that is thrown if we want to bail out to client-side rendering. */ +export function isBailoutToCSRError(err: unknown): err is BailoutToCSRError { + if (typeof err !== 'object' || err === null) return false + return 'digest' in err && err.digest === BAILOUT_TO_CSR +} diff --git a/packages/next/src/shared/lib/lazy-dynamic/dynamic-bailout-to-csr.tsx b/packages/next/src/shared/lib/lazy-dynamic/dynamic-bailout-to-csr.tsx new file mode 100644 index 0000000000..dcb1ed1d8d --- /dev/null +++ b/packages/next/src/shared/lib/lazy-dynamic/dynamic-bailout-to-csr.tsx @@ -0,0 +1,21 @@ +'use client' + +import type { ReactElement } from 'react' +import { BailoutToCSRError } from './bailout-to-csr' + +interface BailoutToCSRProps { + reason: string + children: ReactElement +} + +/** + * If rendered on the server, this component throws an error + * to signal Next.js that it should bail out to client-side rendering instead. + */ +export function BailoutToCSR({ reason, children }: BailoutToCSRProps) { + if (typeof window === 'undefined') { + throw new BailoutToCSRError(reason) + } + + return children +} diff --git a/packages/next/src/shared/lib/lazy-dynamic/dynamic-no-ssr.tsx b/packages/next/src/shared/lib/lazy-dynamic/dynamic-no-ssr.tsx deleted file mode 100644 index 12795c79a6..0000000000 --- a/packages/next/src/shared/lib/lazy-dynamic/dynamic-no-ssr.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client' - -import type React from 'react' -import { throwWithNoSSR } from './no-ssr-error' - -type Child = React.ReactElement - -export function NoSSR({ children }: { children: Child }): Child { - if (typeof window === 'undefined') { - throwWithNoSSR() - } - - return children -} diff --git a/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx b/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx index d5dc700d8e..301eaeb2e1 100644 --- a/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx +++ b/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx @@ -1,43 +1,45 @@ -import { Suspense, lazy, Fragment } from 'react' -import { NoSSR } from './dynamic-no-ssr' +import { Suspense, lazy } from 'react' +import { BailoutToCSR } from './dynamic-bailout-to-csr' import type { ComponentModule } from './types' // Normalize loader to return the module as form { default: Component } for `React.lazy`. // Also for backward compatible since next/dynamic allows to resolve a component directly with loader // Client component reference proxy need to be converted to a module. function convertModule

(mod: React.ComponentType

| ComponentModule

) { - return { default: (mod as ComponentModule

)?.default || mod } + return { default: (mod as ComponentModule

)?.default ?? mod } } -function Loadable(options: any) { - const opts = { - loader: null, - loading: null, - ssr: true, - ...options, - } +const defaultOptions = { + loader: () => Promise.resolve(convertModule(() => null)), + loading: null, + ssr: true, +} - const loader = () => - opts.loader != null - ? opts.loader().then(convertModule) - : Promise.resolve(convertModule(() => null)) +interface LoadableOptions { + loader?: () => Promise | ComponentModule> + loading?: React.ComponentType | null + ssr?: boolean +} - const Lazy = lazy(loader) +function Loadable(options: LoadableOptions) { + const opts = { ...defaultOptions, ...options } + const Lazy = lazy(() => opts.loader().then(convertModule)) const Loading = opts.loading - const Wrap = opts.ssr ? Fragment : NoSSR function LoadableComponent(props: any) { const fallbackElement = Loading ? ( ) : null - return ( - - - - - + const children = opts.ssr ? ( + + ) : ( + + + ) + + return {children} } LoadableComponent.displayName = 'LoadableComponent' diff --git a/packages/next/src/shared/lib/lazy-dynamic/no-ssr-error.ts b/packages/next/src/shared/lib/lazy-dynamic/no-ssr-error.ts deleted file mode 100644 index 3bce562a5b..0000000000 --- a/packages/next/src/shared/lib/lazy-dynamic/no-ssr-error.ts +++ /dev/null @@ -1,13 +0,0 @@ -// This has to be a shared module which is shared between client component error boundary and dynamic component - -export const NEXT_DYNAMIC_NO_SSR_CODE = 'NEXT_DYNAMIC_NO_SSR_CODE' - -export function throwWithNoSSR() { - const error = new Error(NEXT_DYNAMIC_NO_SSR_CODE) - ;(error as any).digest = NEXT_DYNAMIC_NO_SSR_CODE - throw error -} - -export function isBailoutCSRError(err: any) { - return err?.digest === NEXT_DYNAMIC_NO_SSR_CODE -} diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index a0d9119cb9..314a2df2d2 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -494,235 +494,231 @@ createNextDescribe( ) }) - expect(files.sort()).toEqual( + expect(files.sort()).toMatchInlineSnapshot(` [ - 'page.js', - 'index.rsc', - 'index.html', - 'blog/seb.rsc', - 'blog/tim.rsc', - '_not-found.js', - 'blog/seb.html', - 'blog/tim.html', - 'isr-error-handling.rsc', - '_not-found.rsc', - '_not-found.html', - 'blog/styfle.rsc', - 'force-cache.rsc', - 'blog/styfle.html', - 'force-cache.html', - 'isr-error-handling/page.js', - 'ssg-draft-mode.rsc', - 'ssr-forced/page.js', - 'articles/works.rsc', - 'force-cache/page.js', - 'force-cache/large-data/page.js', - 'force-cache/large-data/page_client-reference-manifest.js', - 'ssg-draft-mode.html', - 'articles/works.html', - 'no-store/static.rsc', - '(new)/custom/page.js', - 'force-static/page.js', - 'response-url/page.js', - 'no-store/static.html', - 'blog/[author]/page.js', - 'default-cache/page.js', - 'fetch-no-cache/page.js', - 'force-no-store/page.js', - 'force-static-fetch-no-store.html', - 'force-static-fetch-no-store.rsc', - 'force-static-fetch-no-store/page.js', - 'force-static-fetch-no-store/page_client-reference-manifest.js', - 'force-static/first.rsc', - 'api/draft-mode/route.js', - 'api/large-data/route.js', - 'blog/tim/first-post.rsc', - 'force-static/first.html', - 'force-static/second.rsc', - 'ssg-draft-mode/test.rsc', - 'isr-error-handling.html', - 'articles/[slug]/page.js', - 'no-store/static/page.js', - 'blog/seb/second-post.rsc', - 'blog/tim/first-post.html', - 'force-static/second.html', - 'ssg-draft-mode/test.html', - 'no-store/dynamic/page.js', - 'blog/seb/second-post.html', - 'ssg-draft-mode/test-2.rsc', - 'blog/styfle/first-post.rsc', - 'dynamic-error/[id]/page.js', - 'ssg-draft-mode/test-2.html', - 'blog/styfle/first-post.html', - 'blog/styfle/second-post.rsc', - 'force-static/[slug]/page.js', - 'hooks/use-pathname/slug.rsc', - 'route-handler/post/route.js', - 'blog/[author]/[slug]/page.js', - 'blog/styfle/second-post.html', - 'hooks/use-pathname/slug.html', - 'flight/[slug]/[slug2]/page.js', - 'variable-revalidate/cookie.rsc', - 'ssr-auto/cache-no-store/page.js', - 'variable-revalidate/cookie.html', - 'api/revalidate-tag-edge/route.js', - 'api/revalidate-tag-node/route.js', - 'variable-revalidate/encoding.rsc', - 'api/revalidate-path-edge/route.js', - 'api/revalidate-path-node/route.js', - 'gen-params-dynamic/[slug]/page.js', - 'hooks/use-pathname/[slug]/page.js', - 'page_client-reference-manifest.js', - 'react-fetch-deduping-edge/page.js', - 'react-fetch-deduping-node/page.js', - 'variable-revalidate/encoding.html', - 'variable-revalidate/cookie/page.js', - 'ssg-draft-mode/[[...route]]/page.js', - 'variable-revalidate/post-method.rsc', - 'stale-cache-serving/app-page/page.js', - 'dynamic-no-gen-params/[slug]/page.js', - 'static-to-dynamic-error/[id]/page.js', - 'variable-revalidate/encoding/page.js', - 'variable-revalidate/no-store/page.js', - 'variable-revalidate/post-method.html', - 'variable-revalidate/revalidate-3.rsc', - 'gen-params-dynamic-revalidate/one.rsc', - 'route-handler/revalidate-360/route.js', - 'route-handler/static-cookies/route.js', - 'variable-revalidate-edge/body/page.js', - 'variable-revalidate/authorization.rsc', - 'variable-revalidate/revalidate-3.html', - 'force-dynamic-prerender/[slug]/page.js', - 'gen-params-dynamic-revalidate/one.html', - 'ssr-auto/fetch-revalidate-zero/page.js', - 'variable-revalidate/authorization.html', - '_not-found_client-reference-manifest.js', - 'force-dynamic-no-prerender/[id]/page.js', - 'variable-revalidate/post-method/page.js', - 'variable-revalidate/status-code/page.js', - 'dynamic-no-gen-params-ssr/[slug]/page.js', - 'hooks/use-search-params/force-static.rsc', - 'partial-gen-params/[lang]/[slug]/page.js', - 'variable-revalidate/headers-instance.rsc', - 'variable-revalidate/revalidate-3/page.js', - 'stale-cache-serving-edge/app-page/page.js', - 'hooks/use-search-params/force-static.html', - 'hooks/use-search-params/with-suspense.rsc', - 'route-handler/revalidate-360-isr/route.js', - 'variable-revalidate-edge/encoding/page.js', - 'variable-revalidate-edge/no-store/page.js', - 'variable-revalidate/authorization/page.js', - 'variable-revalidate/headers-instance.html', - 'stale-cache-serving/route-handler/route.js', - 'hooks/use-search-params/with-suspense.html', - 'route-handler-edge/revalidate-360/route.js', - 'variable-revalidate/revalidate-360-isr.rsc', - 'variable-revalidate/revalidate-360/page.js', - 'static-to-dynamic-error-forced/[id]/page.js', - 'variable-config-revalidate/revalidate-3.rsc', - 'variable-revalidate/revalidate-360-isr.html', - 'isr-error-handling/page_client-reference-manifest.js', - 'gen-params-dynamic-revalidate/[slug]/page.js', - 'hooks/use-search-params/force-static/page.js', - 'ssr-forced/page_client-reference-manifest.js', - 'variable-config-revalidate/revalidate-3.html', - 'variable-revalidate-edge/post-method/page.js', - 'variable-revalidate/headers-instance/page.js', - 'force-cache/page_client-reference-manifest.js', - 'hooks/use-search-params/with-suspense/page.js', - 'variable-revalidate-edge/revalidate-3/page.js', - '(new)/custom/page_client-reference-manifest.js', - 'force-static/page_client-reference-manifest.js', - 'response-url/page_client-reference-manifest.js', - 'variable-revalidate/revalidate-360-isr/page.js', - 'stale-cache-serving-edge/route-handler/route.js', - 'blog/[author]/page_client-reference-manifest.js', - 'default-cache/page_client-reference-manifest.js', - 'variable-config-revalidate/revalidate-3/page.js', - 'variable-revalidate/post-method-request/page.js', - 'fetch-no-cache/page_client-reference-manifest.js', - 'force-dynamic-catch-all/[slug]/[[...id]]/page.js', - 'force-no-store/page_client-reference-manifest.js', - 'partial-gen-params-no-additional-lang/en/RAND.rsc', - 'partial-gen-params-no-additional-lang/fr/RAND.rsc', - 'partial-gen-params-no-additional-slug/en/RAND.rsc', - 'partial-gen-params-no-additional-slug/fr/RAND.rsc', - 'articles/[slug]/page_client-reference-manifest.js', - 'no-store/static/page_client-reference-manifest.js', - 'partial-gen-params-no-additional-lang/en/RAND.html', - 'partial-gen-params-no-additional-lang/en/first.rsc', - 'partial-gen-params-no-additional-lang/fr/RAND.html', - 'partial-gen-params-no-additional-lang/fr/first.rsc', - 'partial-gen-params-no-additional-slug/en/RAND.html', - 'partial-gen-params-no-additional-slug/en/first.rsc', - 'partial-gen-params-no-additional-slug/fr/RAND.html', - 'partial-gen-params-no-additional-slug/fr/first.rsc', - 'no-store/dynamic/page_client-reference-manifest.js', - 'partial-gen-params-no-additional-lang/en/first.html', - 'partial-gen-params-no-additional-lang/en/second.rsc', - 'partial-gen-params-no-additional-lang/fr/first.html', - 'partial-gen-params-no-additional-lang/fr/second.rsc', - 'partial-gen-params-no-additional-slug/en/first.html', - 'partial-gen-params-no-additional-slug/en/second.rsc', - 'partial-gen-params-no-additional-slug/fr/first.html', - 'partial-gen-params-no-additional-slug/fr/second.rsc', - 'dynamic-error/[id]/page_client-reference-manifest.js', - 'partial-gen-params-no-additional-lang/en/second.html', - 'partial-gen-params-no-additional-lang/fr/second.html', - 'partial-gen-params-no-additional-slug/en/second.html', - 'partial-gen-params-no-additional-slug/fr/second.html', - 'variable-revalidate-edge/post-method-request/page.js', - 'force-static/[slug]/page_client-reference-manifest.js', - 'blog/[author]/[slug]/page_client-reference-manifest.js', - 'flight/[slug]/[slug2]/page_client-reference-manifest.js', - 'hooks/use-search-params/static-bailout.html', - 'hooks/use-search-params/static-bailout.rsc', - 'hooks/use-search-params/static-bailout/page.js', - 'hooks/use-search-params/static-bailout/page_client-reference-manifest.js', - 'ssr-auto/cache-no-store/page_client-reference-manifest.js', - 'gen-params-dynamic/[slug]/page_client-reference-manifest.js', - 'hooks/use-pathname/[slug]/page_client-reference-manifest.js', - 'partial-gen-params-no-additional-lang/[lang]/[slug]/page.js', - 'partial-gen-params-no-additional-slug/[lang]/[slug]/page.js', - 'react-fetch-deduping-edge/page_client-reference-manifest.js', - 'react-fetch-deduping-node/page_client-reference-manifest.js', - 'variable-revalidate/cookie/page_client-reference-manifest.js', - 'ssg-draft-mode/[[...route]]/page_client-reference-manifest.js', - 'stale-cache-serving/app-page/page_client-reference-manifest.js', - 'dynamic-no-gen-params/[slug]/page_client-reference-manifest.js', - 'static-to-dynamic-error/[id]/page_client-reference-manifest.js', - 'variable-revalidate/encoding/page_client-reference-manifest.js', - 'variable-revalidate/no-store/page_client-reference-manifest.js', - 'variable-revalidate-edge/body/page_client-reference-manifest.js', - 'force-dynamic-prerender/[slug]/page_client-reference-manifest.js', - 'ssr-auto/fetch-revalidate-zero/page_client-reference-manifest.js', - 'force-dynamic-no-prerender/[id]/page_client-reference-manifest.js', - 'variable-revalidate/post-method/page_client-reference-manifest.js', - 'variable-revalidate/status-code/page_client-reference-manifest.js', - 'dynamic-no-gen-params-ssr/[slug]/page_client-reference-manifest.js', - 'partial-gen-params/[lang]/[slug]/page_client-reference-manifest.js', - 'variable-revalidate/revalidate-3/page_client-reference-manifest.js', - 'stale-cache-serving-edge/app-page/page_client-reference-manifest.js', - 'variable-revalidate-edge/encoding/page_client-reference-manifest.js', - 'variable-revalidate-edge/no-store/page_client-reference-manifest.js', - 'variable-revalidate/authorization/page_client-reference-manifest.js', - 'variable-revalidate/revalidate-360/page_client-reference-manifest.js', - 'static-to-dynamic-error-forced/[id]/page_client-reference-manifest.js', - 'gen-params-dynamic-revalidate/[slug]/page_client-reference-manifest.js', - 'hooks/use-search-params/force-static/page_client-reference-manifest.js', - 'variable-revalidate-edge/post-method/page_client-reference-manifest.js', - 'variable-revalidate/headers-instance/page_client-reference-manifest.js', - 'hooks/use-search-params/with-suspense/page_client-reference-manifest.js', - 'variable-revalidate-edge/revalidate-3/page_client-reference-manifest.js', - 'variable-revalidate/revalidate-360-isr/page_client-reference-manifest.js', - 'variable-config-revalidate/revalidate-3/page_client-reference-manifest.js', - 'variable-revalidate/post-method-request/page_client-reference-manifest.js', - 'force-dynamic-catch-all/[slug]/[[...id]]/page_client-reference-manifest.js', - 'variable-revalidate-edge/post-method-request/page_client-reference-manifest.js', - 'partial-gen-params-no-additional-lang/[lang]/[slug]/page_client-reference-manifest.js', - 'partial-gen-params-no-additional-slug/[lang]/[slug]/page_client-reference-manifest.js', - ].sort() - ) + "(new)/custom/page.js", + "(new)/custom/page_client-reference-manifest.js", + "_not-found.html", + "_not-found.js", + "_not-found.rsc", + "_not-found_client-reference-manifest.js", + "api/draft-mode/route.js", + "api/large-data/route.js", + "api/revalidate-path-edge/route.js", + "api/revalidate-path-node/route.js", + "api/revalidate-tag-edge/route.js", + "api/revalidate-tag-node/route.js", + "articles/[slug]/page.js", + "articles/[slug]/page_client-reference-manifest.js", + "articles/works.html", + "articles/works.rsc", + "blog/[author]/[slug]/page.js", + "blog/[author]/[slug]/page_client-reference-manifest.js", + "blog/[author]/page.js", + "blog/[author]/page_client-reference-manifest.js", + "blog/seb.html", + "blog/seb.rsc", + "blog/seb/second-post.html", + "blog/seb/second-post.rsc", + "blog/styfle.html", + "blog/styfle.rsc", + "blog/styfle/first-post.html", + "blog/styfle/first-post.rsc", + "blog/styfle/second-post.html", + "blog/styfle/second-post.rsc", + "blog/tim.html", + "blog/tim.rsc", + "blog/tim/first-post.html", + "blog/tim/first-post.rsc", + "default-cache/page.js", + "default-cache/page_client-reference-manifest.js", + "dynamic-error/[id]/page.js", + "dynamic-error/[id]/page_client-reference-manifest.js", + "dynamic-no-gen-params-ssr/[slug]/page.js", + "dynamic-no-gen-params-ssr/[slug]/page_client-reference-manifest.js", + "dynamic-no-gen-params/[slug]/page.js", + "dynamic-no-gen-params/[slug]/page_client-reference-manifest.js", + "fetch-no-cache/page.js", + "fetch-no-cache/page_client-reference-manifest.js", + "flight/[slug]/[slug2]/page.js", + "flight/[slug]/[slug2]/page_client-reference-manifest.js", + "force-cache.html", + "force-cache.rsc", + "force-cache/large-data/page.js", + "force-cache/large-data/page_client-reference-manifest.js", + "force-cache/page.js", + "force-cache/page_client-reference-manifest.js", + "force-dynamic-catch-all/[slug]/[[...id]]/page.js", + "force-dynamic-catch-all/[slug]/[[...id]]/page_client-reference-manifest.js", + "force-dynamic-no-prerender/[id]/page.js", + "force-dynamic-no-prerender/[id]/page_client-reference-manifest.js", + "force-dynamic-prerender/[slug]/page.js", + "force-dynamic-prerender/[slug]/page_client-reference-manifest.js", + "force-no-store/page.js", + "force-no-store/page_client-reference-manifest.js", + "force-static-fetch-no-store.html", + "force-static-fetch-no-store.rsc", + "force-static-fetch-no-store/page.js", + "force-static-fetch-no-store/page_client-reference-manifest.js", + "force-static/[slug]/page.js", + "force-static/[slug]/page_client-reference-manifest.js", + "force-static/first.html", + "force-static/first.rsc", + "force-static/page.js", + "force-static/page_client-reference-manifest.js", + "force-static/second.html", + "force-static/second.rsc", + "gen-params-dynamic-revalidate/[slug]/page.js", + "gen-params-dynamic-revalidate/[slug]/page_client-reference-manifest.js", + "gen-params-dynamic-revalidate/one.html", + "gen-params-dynamic-revalidate/one.rsc", + "gen-params-dynamic/[slug]/page.js", + "gen-params-dynamic/[slug]/page_client-reference-manifest.js", + "hooks/use-pathname/[slug]/page.js", + "hooks/use-pathname/[slug]/page_client-reference-manifest.js", + "hooks/use-pathname/slug.html", + "hooks/use-pathname/slug.rsc", + "hooks/use-search-params/force-static.html", + "hooks/use-search-params/force-static.rsc", + "hooks/use-search-params/force-static/page.js", + "hooks/use-search-params/force-static/page_client-reference-manifest.js", + "hooks/use-search-params/with-suspense.html", + "hooks/use-search-params/with-suspense.rsc", + "hooks/use-search-params/with-suspense/page.js", + "hooks/use-search-params/with-suspense/page_client-reference-manifest.js", + "index.html", + "index.rsc", + "isr-error-handling.html", + "isr-error-handling.rsc", + "isr-error-handling/page.js", + "isr-error-handling/page_client-reference-manifest.js", + "no-store/dynamic/page.js", + "no-store/dynamic/page_client-reference-manifest.js", + "no-store/static.html", + "no-store/static.rsc", + "no-store/static/page.js", + "no-store/static/page_client-reference-manifest.js", + "page.js", + "page_client-reference-manifest.js", + "partial-gen-params-no-additional-lang/[lang]/[slug]/page.js", + "partial-gen-params-no-additional-lang/[lang]/[slug]/page_client-reference-manifest.js", + "partial-gen-params-no-additional-lang/en/RAND.html", + "partial-gen-params-no-additional-lang/en/RAND.rsc", + "partial-gen-params-no-additional-lang/en/first.html", + "partial-gen-params-no-additional-lang/en/first.rsc", + "partial-gen-params-no-additional-lang/en/second.html", + "partial-gen-params-no-additional-lang/en/second.rsc", + "partial-gen-params-no-additional-lang/fr/RAND.html", + "partial-gen-params-no-additional-lang/fr/RAND.rsc", + "partial-gen-params-no-additional-lang/fr/first.html", + "partial-gen-params-no-additional-lang/fr/first.rsc", + "partial-gen-params-no-additional-lang/fr/second.html", + "partial-gen-params-no-additional-lang/fr/second.rsc", + "partial-gen-params-no-additional-slug/[lang]/[slug]/page.js", + "partial-gen-params-no-additional-slug/[lang]/[slug]/page_client-reference-manifest.js", + "partial-gen-params-no-additional-slug/en/RAND.html", + "partial-gen-params-no-additional-slug/en/RAND.rsc", + "partial-gen-params-no-additional-slug/en/first.html", + "partial-gen-params-no-additional-slug/en/first.rsc", + "partial-gen-params-no-additional-slug/en/second.html", + "partial-gen-params-no-additional-slug/en/second.rsc", + "partial-gen-params-no-additional-slug/fr/RAND.html", + "partial-gen-params-no-additional-slug/fr/RAND.rsc", + "partial-gen-params-no-additional-slug/fr/first.html", + "partial-gen-params-no-additional-slug/fr/first.rsc", + "partial-gen-params-no-additional-slug/fr/second.html", + "partial-gen-params-no-additional-slug/fr/second.rsc", + "partial-gen-params/[lang]/[slug]/page.js", + "partial-gen-params/[lang]/[slug]/page_client-reference-manifest.js", + "react-fetch-deduping-edge/page.js", + "react-fetch-deduping-edge/page_client-reference-manifest.js", + "react-fetch-deduping-node/page.js", + "react-fetch-deduping-node/page_client-reference-manifest.js", + "response-url/page.js", + "response-url/page_client-reference-manifest.js", + "route-handler-edge/revalidate-360/route.js", + "route-handler/post/route.js", + "route-handler/revalidate-360-isr/route.js", + "route-handler/revalidate-360/route.js", + "route-handler/static-cookies/route.js", + "ssg-draft-mode.html", + "ssg-draft-mode.rsc", + "ssg-draft-mode/[[...route]]/page.js", + "ssg-draft-mode/[[...route]]/page_client-reference-manifest.js", + "ssg-draft-mode/test-2.html", + "ssg-draft-mode/test-2.rsc", + "ssg-draft-mode/test.html", + "ssg-draft-mode/test.rsc", + "ssr-auto/cache-no-store/page.js", + "ssr-auto/cache-no-store/page_client-reference-manifest.js", + "ssr-auto/fetch-revalidate-zero/page.js", + "ssr-auto/fetch-revalidate-zero/page_client-reference-manifest.js", + "ssr-forced/page.js", + "ssr-forced/page_client-reference-manifest.js", + "stale-cache-serving-edge/app-page/page.js", + "stale-cache-serving-edge/app-page/page_client-reference-manifest.js", + "stale-cache-serving-edge/route-handler/route.js", + "stale-cache-serving/app-page/page.js", + "stale-cache-serving/app-page/page_client-reference-manifest.js", + "stale-cache-serving/route-handler/route.js", + "static-to-dynamic-error-forced/[id]/page.js", + "static-to-dynamic-error-forced/[id]/page_client-reference-manifest.js", + "static-to-dynamic-error/[id]/page.js", + "static-to-dynamic-error/[id]/page_client-reference-manifest.js", + "variable-config-revalidate/revalidate-3.html", + "variable-config-revalidate/revalidate-3.rsc", + "variable-config-revalidate/revalidate-3/page.js", + "variable-config-revalidate/revalidate-3/page_client-reference-manifest.js", + "variable-revalidate-edge/body/page.js", + "variable-revalidate-edge/body/page_client-reference-manifest.js", + "variable-revalidate-edge/encoding/page.js", + "variable-revalidate-edge/encoding/page_client-reference-manifest.js", + "variable-revalidate-edge/no-store/page.js", + "variable-revalidate-edge/no-store/page_client-reference-manifest.js", + "variable-revalidate-edge/post-method-request/page.js", + "variable-revalidate-edge/post-method-request/page_client-reference-manifest.js", + "variable-revalidate-edge/post-method/page.js", + "variable-revalidate-edge/post-method/page_client-reference-manifest.js", + "variable-revalidate-edge/revalidate-3/page.js", + "variable-revalidate-edge/revalidate-3/page_client-reference-manifest.js", + "variable-revalidate/authorization.html", + "variable-revalidate/authorization.rsc", + "variable-revalidate/authorization/page.js", + "variable-revalidate/authorization/page_client-reference-manifest.js", + "variable-revalidate/cookie.html", + "variable-revalidate/cookie.rsc", + "variable-revalidate/cookie/page.js", + "variable-revalidate/cookie/page_client-reference-manifest.js", + "variable-revalidate/encoding.html", + "variable-revalidate/encoding.rsc", + "variable-revalidate/encoding/page.js", + "variable-revalidate/encoding/page_client-reference-manifest.js", + "variable-revalidate/headers-instance.html", + "variable-revalidate/headers-instance.rsc", + "variable-revalidate/headers-instance/page.js", + "variable-revalidate/headers-instance/page_client-reference-manifest.js", + "variable-revalidate/no-store/page.js", + "variable-revalidate/no-store/page_client-reference-manifest.js", + "variable-revalidate/post-method-request/page.js", + "variable-revalidate/post-method-request/page_client-reference-manifest.js", + "variable-revalidate/post-method.html", + "variable-revalidate/post-method.rsc", + "variable-revalidate/post-method/page.js", + "variable-revalidate/post-method/page_client-reference-manifest.js", + "variable-revalidate/revalidate-3.html", + "variable-revalidate/revalidate-3.rsc", + "variable-revalidate/revalidate-3/page.js", + "variable-revalidate/revalidate-3/page_client-reference-manifest.js", + "variable-revalidate/revalidate-360-isr.html", + "variable-revalidate/revalidate-360-isr.rsc", + "variable-revalidate/revalidate-360-isr/page.js", + "variable-revalidate/revalidate-360-isr/page_client-reference-manifest.js", + "variable-revalidate/revalidate-360/page.js", + "variable-revalidate/revalidate-360/page_client-reference-manifest.js", + "variable-revalidate/status-code/page.js", + "variable-revalidate/status-code/page_client-reference-manifest.js", + ] + `) }) it('should have correct prerender-manifest entries', async () => { @@ -1033,22 +1029,6 @@ createNextDescribe( "initialRevalidateSeconds": false, "srcRoute": "/hooks/use-search-params/force-static", }, - "/hooks/use-search-params/static-bailout": { - "dataRoute": "/hooks/use-search-params/static-bailout.rsc", - "experimentalBypassFor": [ - { - "key": "Next-Action", - "type": "header", - }, - { - "key": "content-type", - "type": "header", - "value": "multipart/form-data", - }, - ], - "initialRevalidateSeconds": false, - "srcRoute": "/hooks/use-search-params/static-bailout", - }, "/hooks/use-search-params/with-suspense": { "dataRoute": "/hooks/use-search-params/with-suspense.rsc", "experimentalBypassFor": [ @@ -2888,26 +2868,6 @@ createNextDescribe( describe('useSearchParams', () => { describe('client', () => { - it('should bailout to client rendering - without suspense boundary', async () => { - const url = - '/hooks/use-search-params/static-bailout?first=value&second=other&third' - const browser = await next.browser(url) - - expect(await browser.elementByCss('#params-first').text()).toBe( - 'value' - ) - expect(await browser.elementByCss('#params-second').text()).toBe( - 'other' - ) - expect(await browser.elementByCss('#params-third').text()).toBe('') - expect(await browser.elementByCss('#params-not-real').text()).toBe( - 'N/A' - ) - - const $ = await next.render$(url) - expect($('meta[content=noindex]').length).toBe(0) - }) - it('should bailout to client rendering - with suspense boundary', async () => { const url = '/hooks/use-search-params/with-suspense?first=value&second=other&third' @@ -2977,14 +2937,6 @@ createNextDescribe( // Don't run these tests in dev mode since they won't be statically generated if (!isDev) { describe('server response', () => { - it('should bailout to client rendering - without suspense boundary', async () => { - const res = await next.fetch( - '/hooks/use-search-params/static-bailout' - ) - const html = await res.text() - expect(html).toInclude('') - }) - it('should bailout to client rendering - with suspense boundary', async () => { const res = await next.fetch( '/hooks/use-search-params/with-suspense' diff --git a/test/e2e/app-dir/app-static/app/hooks/use-search-params/static-bailout/page.js b/test/e2e/app-dir/app-static/app/hooks/use-search-params/static-bailout/page.js deleted file mode 100644 index bdf2409132..0000000000 --- a/test/e2e/app-dir/app-static/app/hooks/use-search-params/static-bailout/page.js +++ /dev/null @@ -1,10 +0,0 @@ -import UseSearchParams from '../search-params' - -export default function Page() { - return ( - <> -

- - - ) -} diff --git a/test/e2e/app-dir/app-static/next.config.js b/test/e2e/app-dir/app-static/next.config.js index bc0912e06b..57153652e4 100644 --- a/test/e2e/app-dir/app-static/next.config.js +++ b/test/e2e/app-dir/app-static/next.config.js @@ -13,7 +13,7 @@ module.exports = { afterFiles: [ { source: '/rewritten-use-search-params', - destination: '/hooks/use-search-params/static-bailout', + destination: '/hooks/use-search-params/with-suspense', }, { source: '/rewritten-use-pathname', diff --git a/test/e2e/app-dir/hooks/app/hooks/use-search-params/page.js b/test/e2e/app-dir/hooks/app/hooks/use-search-params/page.js index d84e0f522c..6c8e29a6cc 100644 --- a/test/e2e/app-dir/hooks/app/hooks/use-search-params/page.js +++ b/test/e2e/app-dir/hooks/app/hooks/use-search-params/page.js @@ -1,8 +1,17 @@ 'use client' import { useSearchParams } from 'next/navigation' +import { Suspense } from 'react' export default function Page() { + return ( + Loading...}> + + + ) +} + +function Component() { const params = useSearchParams() return ( diff --git a/test/e2e/app-dir/not-found-default/app/layout.js b/test/e2e/app-dir/not-found-default/app/layout.js index 5b344e2614..bd97c16add 100644 --- a/test/e2e/app-dir/not-found-default/app/layout.js +++ b/test/e2e/app-dir/not-found-default/app/layout.js @@ -13,7 +13,9 @@ export default function Root({ children }) { return ( - + Loading...}> + + diff --git a/test/e2e/app-dir/use-search-params/app/layout-no-suspense.js b/test/e2e/app-dir/use-search-params/app/layout-no-suspense.js new file mode 100644 index 0000000000..f3791f288f --- /dev/null +++ b/test/e2e/app-dir/use-search-params/app/layout-no-suspense.js @@ -0,0 +1,8 @@ +export default function Layout({ children }) { + return ( + + + {children} + + ) +} diff --git a/test/e2e/app-dir/use-search-params/app/layout.js b/test/e2e/app-dir/use-search-params/app/layout.js new file mode 100644 index 0000000000..06c28b2c36 --- /dev/null +++ b/test/e2e/app-dir/use-search-params/app/layout.js @@ -0,0 +1,14 @@ +import React from 'react' + +export default function Layout({ children }) { + return ( + + + + Loading...}> + {children} + + + + ) +} diff --git a/test/e2e/app-dir/use-search-params/app/page.js b/test/e2e/app-dir/use-search-params/app/page.js new file mode 100644 index 0000000000..db29d22f58 --- /dev/null +++ b/test/e2e/app-dir/use-search-params/app/page.js @@ -0,0 +1,8 @@ +'use client' + +import { useSearchParams } from 'next/navigation' + +export default function Page() { + useSearchParams() + return

Page
+} diff --git a/test/e2e/app-dir/use-search-params/next.config.js b/test/e2e/app-dir/use-search-params/next.config.js new file mode 100644 index 0000000000..459c99acaf --- /dev/null +++ b/test/e2e/app-dir/use-search-params/next.config.js @@ -0,0 +1,8 @@ +/** @type {import("next").NextConfig} */ +const config = { + experimental: { + missingSuspenseWithCSRBailout: true, + }, +} + +module.exports = config diff --git a/test/e2e/app-dir/use-search-params/use-search-params.test.ts b/test/e2e/app-dir/use-search-params/use-search-params.test.ts new file mode 100644 index 0000000000..61de9db81a --- /dev/null +++ b/test/e2e/app-dir/use-search-params/use-search-params.test.ts @@ -0,0 +1,35 @@ +import { createNextDescribe } from 'e2e-utils' + +createNextDescribe( + 'use-search-params', + { files: __dirname, skipStart: true }, + ({ next, isNextStart }) => { + if (!isNextStart) { + it('skip test for dev mode', () => {}) + return + } + + const message = `useSearchParams() should be wrapped in a suspense boundary at page "/".` + + it('should pass build if useSearchParams is wrapped in a suspense boundary', async () => { + await expect(next.build()).resolves.toEqual({ + exitCode: 0, + cliOutput: expect.not.stringContaining(message), + }) + }) + + it('should fail build if useSearchParams is not wrapped in a suspense boundary', async () => { + await next.clean() + await next.renameFile('app/layout.js', 'app/layout-suspense.js') + await next.renameFile('app/layout-no-suspense.js', 'app/layout.js') + + await expect(next.build()).resolves.toEqual({ + exitCode: 1, + cliOutput: expect.stringContaining(message), + }) + + await next.renameFile('app/layout.js', 'app/layout-no-suspense.js') + await next.renameFile('app/layout-suspense.js', 'app/layout.js') + }) + } +) diff --git a/test/lib/next-modes/base.ts b/test/lib/next-modes/base.ts index 241cfc8ef1..7597f25d8c 100644 --- a/test/lib/next-modes/base.ts +++ b/test/lib/next-modes/base.ts @@ -1,6 +1,6 @@ import os from 'os' import path from 'path' -import fs from 'fs-extra' +import { existsSync, promises as fs } from 'fs' import treeKill from 'tree-kill' import type { NextConfig } from 'next' import { FileRef } from '../e2e-utils' @@ -100,17 +100,18 @@ export class NextInstance { `FileRef passed to "files" in "createNext" is not a directory ${files.fsPath}` ) } - await fs.copy(files.fsPath, this.testDir) + + await fs.cp(files.fsPath, this.testDir, { recursive: true }) } else { for (const filename of Object.keys(files)) { const item = files[filename] const outputFilename = path.join(this.testDir, filename) if (typeof item === 'string') { - await fs.ensureDir(path.dirname(outputFilename)) + await fs.mkdir(path.dirname(outputFilename), { recursive: true }) await fs.writeFile(outputFilename, item) } else { - await fs.copy(item.fsPath, outputFilename) + await fs.cp(item.fsPath, outputFilename, { recursive: true }) } } } @@ -158,7 +159,7 @@ export class NextInstance { if (skipInstall || skipIsolatedNext) { const pkgScripts = (this.packageJson['scripts'] as {}) || {} - await fs.ensureDir(this.testDir) + await fs.mkdir(this.testDir, { recursive: true }) await fs.writeFile( path.join(this.testDir, 'package.json'), JSON.stringify( @@ -193,7 +194,9 @@ export class NextInstance { !this.packageJson && !(global as any).isNextDeploy ) { - await fs.copy(process.env.NEXT_TEST_STARTER, this.testDir) + await fs.cp(process.env.NEXT_TEST_STARTER, this.testDir, { + recursive: true, + }) } else { const { installDir } = await createNextInstall({ parentSpan: rootSpan, @@ -218,7 +221,7 @@ export class NextInstance { file.startsWith('next.config.') ) - if (await fs.pathExists(path.join(this.testDir, 'next.config.js'))) { + if (existsSync(path.join(this.testDir, 'next.config.js'))) { nextConfigFile = 'next.config.js' } @@ -330,7 +333,10 @@ export class NextInstance { ] for (const file of await fs.readdir(this.testDir)) { if (!keptFiles.includes(file)) { - await fs.remove(path.join(this.testDir, file)) + await fs.rm(path.join(this.testDir, file), { + recursive: true, + force: true, + }) } } await this.writeInitialFiles() @@ -378,7 +384,7 @@ export class NextInstance { if (process.env.TRACE_PLAYWRIGHT) { await fs - .copy( + .cp( path.join(this.testDir, '.next/trace'), path.join( __dirname, @@ -390,7 +396,8 @@ export class NextInstance { ) .replace(/\//g, '-')}`, `next-trace` - ) + ), + { recursive: true } ) .catch((e) => { require('console').error(e) @@ -398,7 +405,7 @@ export class NextInstance { } if (!process.env.NEXT_TEST_SKIP_CLEANUP) { - await fs.remove(this.testDir) + await fs.rm(this.testDir, { recursive: true, force: true }) } require('console').log(`destroyed next instance`) } catch (err) { @@ -424,13 +431,15 @@ export class NextInstance { // TODO: block these in deploy mode public async hasFile(filename: string) { - return fs.pathExists(path.join(this.testDir, filename)) + return existsSync(path.join(this.testDir, filename)) } public async readFile(filename: string) { return fs.readFile(path.join(this.testDir, filename), 'utf8') } public async readJSON(filename: string) { - return fs.readJSON(path.join(this.testDir, filename)) + return JSON.parse( + await fs.readFile(path.join(this.testDir, filename), 'utf-8') + ) } private async handleDevWatchDelayBeforeChange(filename: string) { // This is a temporary workaround for turbopack starting watching too late. @@ -462,8 +471,8 @@ export class NextInstance { await this.handleDevWatchDelayBeforeChange(filename) const outputPath = path.join(this.testDir, filename) - const newFile = !(await fs.pathExists(outputPath)) - await fs.ensureDir(path.dirname(outputPath)) + const newFile = !existsSync(outputPath) + await fs.mkdir(path.dirname(outputPath), { recursive: true }) await fs.writeFile( outputPath, typeof content === 'function' @@ -486,12 +495,13 @@ export class NextInstance { path.join(this.testDir, filename), path.join(this.testDir, newFilename) ) + await this.handleDevWatchDelayAfterChange(filename) } public async renameFolder(foldername: string, newFoldername: string) { await this.handleDevWatchDelayBeforeChange(foldername) - await fs.move( + await fs.rename( path.join(this.testDir, foldername), path.join(this.testDir, newFoldername) ) @@ -500,7 +510,11 @@ export class NextInstance { public async deleteFile(filename: string) { await this.handleDevWatchDelayBeforeChange(filename) - await fs.remove(path.join(this.testDir, filename)) + await fs.rm(path.join(this.testDir, filename), { + recursive: true, + force: true, + }) + await this.handleDevWatchDelayAfterChange(filename) }