feat(app): add experimental.missingSuspenseWithCSRBailout
(#57642)
### 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 <info@balazsorban.com> Co-authored-by: Wyatt Johnson <accounts+github@wyattjoh.ca>
This commit is contained in:
parent
8aced5bc64
commit
c52cb5ad83
30 changed files with 471 additions and 387 deletions
15
errors/missing-suspense-with-csr-bailout.mdx
Normal file
15
errors/missing-suspense-with-csr-bailout.mdx
Normal file
|
@ -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)
|
|
@ -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'
|
import { staticGenerationAsyncStorage } from './static-generation-async-storage.external'
|
||||||
|
|
||||||
export function bailoutToClientRendering(): void | never {
|
export function bailoutToClientRendering(reason: string): void | never {
|
||||||
const staticGenerationStore = staticGenerationAsyncStorage.getStore()
|
const staticGenerationStore = staticGenerationAsyncStorage.getStore()
|
||||||
|
|
||||||
if (staticGenerationStore?.forceStatic) {
|
if (staticGenerationStore?.forceStatic) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (staticGenerationStore?.isStaticGeneration) {
|
if (staticGenerationStore?.isStaticGeneration)
|
||||||
throwWithNoSSR()
|
throw new BailoutToCSRError(reason)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,7 @@ export function useSearchParams(): ReadonlyURLSearchParams {
|
||||||
const { bailoutToClientRendering } =
|
const { bailoutToClientRendering } =
|
||||||
require('./bailout-to-client-rendering') as typeof import('./bailout-to-client-rendering')
|
require('./bailout-to-client-rendering') as typeof import('./bailout-to-client-rendering')
|
||||||
// TODO-APP: handle dynamic = 'force-static' here and on the client
|
// TODO-APP: handle dynamic = 'force-static' here and on the client
|
||||||
bailoutToClientRendering()
|
bailoutToClientRendering('useSearchParams()')
|
||||||
}
|
}
|
||||||
|
|
||||||
return readonlySearchParams
|
return readonlySearchParams
|
||||||
|
|
|
@ -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
|
// Using default react onRecoverableError
|
||||||
// x-ref: https://github.com/facebook/react/blob/d4bc16a7d69eb2ea38a88c8ac0b461d5f72cdcab/packages/react-dom/src/client/ReactDOMRoot.js#L83
|
// x-ref: https://github.com/facebook/react/blob/d4bc16a7d69eb2ea38a88c8ac0b461d5f72cdcab/packages/react-dom/src/client/ReactDOMRoot.js#L83
|
||||||
const defaultOnRecoverableError =
|
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
|
// Skip certain custom errors which are not expected to be reported on client
|
||||||
if (isBailoutCSRError(err)) return
|
if (isBailoutToCSRError(err)) return
|
||||||
|
|
||||||
defaultOnRecoverableError(err)
|
defaultOnRecoverableError(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { DYNAMIC_ERROR_CODE } from '../../client/components/hooks-server-context'
|
import { DYNAMIC_ERROR_CODE } from '../../client/components/hooks-server-context'
|
||||||
import { isNotFoundError } from '../../client/components/not-found'
|
import { isNotFoundError } from '../../client/components/not-found'
|
||||||
import { isRedirectError } from '../../client/components/redirect'
|
import { isRedirectError } from '../../client/components/redirect'
|
||||||
import { isBailoutCSRError } from '../../shared/lib/lazy-dynamic/no-ssr-error'
|
|
||||||
|
|
||||||
export const isDynamicUsageError = (err: any) =>
|
export const isDynamicUsageError = (err: any) =>
|
||||||
err.digest === DYNAMIC_ERROR_CODE ||
|
err.digest === DYNAMIC_ERROR_CODE ||
|
||||||
isNotFoundError(err) ||
|
isNotFoundError(err) ||
|
||||||
isBailoutCSRError(err) ||
|
|
||||||
isRedirectError(err)
|
isRedirectError(err)
|
||||||
|
|
|
@ -506,7 +506,11 @@ export async function exportAppImpl(
|
||||||
: {}),
|
: {}),
|
||||||
strictNextHead: !!nextConfig.experimental.strictNextHead,
|
strictNextHead: !!nextConfig.experimental.strictNextHead,
|
||||||
deploymentId: nextConfig.experimental.deploymentId,
|
deploymentId: nextConfig.experimental.deploymentId,
|
||||||
experimental: { ppr: nextConfig.experimental.ppr === true },
|
experimental: {
|
||||||
|
ppr: nextConfig.experimental.ppr === true,
|
||||||
|
missingSuspenseWithCSRBailout:
|
||||||
|
nextConfig.experimental.missingSuspenseWithCSRBailout,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
|
const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
NEXT_DATA_SUFFIX,
|
NEXT_DATA_SUFFIX,
|
||||||
SERVER_PROPS_EXPORT_ERROR,
|
SERVER_PROPS_EXPORT_ERROR,
|
||||||
} from '../../lib/constants'
|
} 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 AmpHtmlValidator from 'next/dist/compiled/amphtml-validator'
|
||||||
import { FileType, fileExists } from '../../lib/file-exists'
|
import { FileType, fileExists } from '../../lib/file-exists'
|
||||||
import { lazyRenderPagesPage } from '../../server/future/route-modules/pages/module.render'
|
import { lazyRenderPagesPage } from '../../server/future/route-modules/pages/module.render'
|
||||||
|
@ -105,10 +105,8 @@ export async function exportPages(
|
||||||
query,
|
query,
|
||||||
renderOpts
|
renderOpts
|
||||||
)
|
)
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
if (!isBailoutCSRError(err)) {
|
if (!isBailoutToCSRError(err)) throw err
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,10 +161,8 @@ export async function exportPages(
|
||||||
{ ...query, amp: '1' },
|
{ ...query, amp: '1' },
|
||||||
renderOpts
|
renderOpts
|
||||||
)
|
)
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
if (!isBailoutCSRError(err)) {
|
if (!isBailoutToCSRError(err)) throw err
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ampHtml =
|
const ampHtml =
|
||||||
|
|
|
@ -35,6 +35,7 @@ import { createIncrementalCache } from './helpers/create-incremental-cache'
|
||||||
import { isPostpone } from '../server/lib/router-utils/is-postpone'
|
import { isPostpone } from '../server/lib/router-utils/is-postpone'
|
||||||
import { isMissingPostponeDataError } from '../server/app-render/is-missing-postpone-error'
|
import { isMissingPostponeDataError } from '../server/app-render/is-missing-postpone-error'
|
||||||
import { isDynamicUsageError } from './helpers/is-dynamic-usage-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')
|
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 this is a postpone error, it's logged elsewhere, so no need to log it again here
|
||||||
if (!isMissingPostponeDataError(err)) {
|
if (!isMissingPostponeDataError(err)) {
|
||||||
console.error(
|
console.error(
|
||||||
`\nError occurred prerendering page "${path}". Read more: https://nextjs.org/docs/messages/prerender-error\n` +
|
`\nError occurred prerendering page "${path}". Read more: https://nextjs.org/docs/messages/prerender-error\n`
|
||||||
(isError(err) && err.stack ? err.stack : err)
|
|
||||||
)
|
)
|
||||||
|
if (!isBailoutToCSRError(err)) {
|
||||||
|
console.error(isError(err) && err.stack ? err.stack : err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { error: true }
|
return { error: true }
|
||||||
|
|
|
@ -63,7 +63,7 @@ import { parseAndValidateFlightRouterState } from './parse-and-validate-flight-r
|
||||||
import { validateURL } from './validate-url'
|
import { validateURL } from './validate-url'
|
||||||
import { createFlightRouterStateFromLoaderTree } from './create-flight-router-state-from-loader-tree'
|
import { createFlightRouterStateFromLoaderTree } from './create-flight-router-state-from-loader-tree'
|
||||||
import { handleAction } from './action-handler'
|
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 { warn, error } from '../../build/output/log'
|
||||||
import { appendMutableCookies } from '../web/spec-extension/adapters/request-cookies'
|
import { appendMutableCookies } from '../web/spec-extension/adapters/request-cookies'
|
||||||
import { createServerInsertedHTML } from './server-inserted-html'
|
import { createServerInsertedHTML } from './server-inserted-html'
|
||||||
|
@ -996,12 +996,19 @@ async function renderToHTMLOrFlightImpl(
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
// True if this error was a bailout to client side rendering error.
|
/** True if this error was a bailout to client side rendering error. */
|
||||||
const shouldBailoutToCSR = isBailoutCSRError(err)
|
const shouldBailoutToCSR = isBailoutToCSRError(err)
|
||||||
if (shouldBailoutToCSR) {
|
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(
|
warn(
|
||||||
`Entire page ${pagePath} deopted into client-side rendering. https://nextjs.org/docs/messages/deopted-into-client-rendering`,
|
`Entire page "${pagePath}" deopted into client-side rendering. https://nextjs.org/docs/messages/deopted-into-client-rendering`
|
||||||
pagePath
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1212,7 +1219,7 @@ async function renderToHTMLOrFlightImpl(
|
||||||
renderOpts.experimental.ppr &&
|
renderOpts.experimental.ppr &&
|
||||||
staticGenerationStore.postponeWasTriggered &&
|
staticGenerationStore.postponeWasTriggered &&
|
||||||
!metadata.postponed &&
|
!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
|
// 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
|
// as we won't be able to generate the static part
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { formatServerError } from '../../lib/format-server-error'
|
||||||
import { SpanStatusCode, getTracer } from '../lib/trace/tracer'
|
import { SpanStatusCode, getTracer } from '../lib/trace/tracer'
|
||||||
import { isAbortError } from '../pipe-readable'
|
import { isAbortError } from '../pipe-readable'
|
||||||
import { isDynamicUsageError } from '../../export/helpers/is-dynamic-usage-error'
|
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
|
export type ErrorHandler = (err: any) => string | undefined
|
||||||
|
|
||||||
|
@ -34,11 +35,12 @@ export function createErrorHandler({
|
||||||
return (err) => {
|
return (err) => {
|
||||||
if (allCapturedErrors) allCapturedErrors.push(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
|
// These errors are expected. We return the digest
|
||||||
// so that they can be properly handled.
|
// so that they can be properly handled.
|
||||||
if (isDynamicUsageError(err)) {
|
if (isDynamicUsageError(err)) return err.digest
|
||||||
return err.digest
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the response was closed, we don't need to log the error.
|
// If the response was closed, we don't need to log the error.
|
||||||
if (isAbortError(err)) return
|
if (isAbortError(err)) return
|
||||||
|
|
|
@ -142,7 +142,7 @@ export interface RenderOptsPartial {
|
||||||
}
|
}
|
||||||
params?: ParsedUrlQuery
|
params?: ParsedUrlQuery
|
||||||
isPrefetch?: boolean
|
isPrefetch?: boolean
|
||||||
experimental: { ppr: boolean }
|
experimental: { ppr: boolean; missingSuspenseWithCSRBailout?: boolean }
|
||||||
postponed?: string
|
postponed?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ export type StaticGenerationContext = {
|
||||||
isDraftMode?: boolean
|
isDraftMode?: boolean
|
||||||
isServerAction?: boolean
|
isServerAction?: boolean
|
||||||
waitUntil?: Promise<any>
|
waitUntil?: Promise<any>
|
||||||
experimental: { ppr: boolean }
|
experimental: { ppr: boolean; missingSuspenseWithCSRBailout?: boolean }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hack around accessing the store value outside the context of the
|
* A hack around accessing the store value outside the context of the
|
||||||
|
|
|
@ -369,6 +369,7 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
|
||||||
staticWorkerRequestDeduping: z.boolean().optional(),
|
staticWorkerRequestDeduping: z.boolean().optional(),
|
||||||
useWasmBinary: z.boolean().optional(),
|
useWasmBinary: z.boolean().optional(),
|
||||||
useLightningcss: z.boolean().optional(),
|
useLightningcss: z.boolean().optional(),
|
||||||
|
missingSuspenseWithCSRBailout: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
exportPathMap: z
|
exportPathMap: z
|
||||||
|
|
|
@ -351,6 +351,16 @@ export interface ExperimentalConfig {
|
||||||
* Use lightningcss instead of swc_css
|
* Use lightningcss instead of swc_css
|
||||||
*/
|
*/
|
||||||
useLightningcss?: boolean
|
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 = {
|
export type ExportPathMap = {
|
||||||
|
@ -811,6 +821,7 @@ export const defaultConfig: NextConfig = {
|
||||||
? true
|
? true
|
||||||
: false,
|
: false,
|
||||||
webpackBuildWorker: undefined,
|
webpackBuildWorker: undefined,
|
||||||
|
missingSuspenseWithCSRBailout: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
14
packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts
Normal file
14
packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import type React from 'react'
|
|
||||||
import { throwWithNoSSR } from './no-ssr-error'
|
|
||||||
|
|
||||||
type Child = React.ReactElement<any, any>
|
|
||||||
|
|
||||||
export function NoSSR({ children }: { children: Child }): Child {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
throwWithNoSSR()
|
|
||||||
}
|
|
||||||
|
|
||||||
return children
|
|
||||||
}
|
|
|
@ -1,43 +1,45 @@
|
||||||
import { Suspense, lazy, Fragment } from 'react'
|
import { Suspense, lazy } from 'react'
|
||||||
import { NoSSR } from './dynamic-no-ssr'
|
import { BailoutToCSR } from './dynamic-bailout-to-csr'
|
||||||
import type { ComponentModule } from './types'
|
import type { ComponentModule } from './types'
|
||||||
|
|
||||||
// Normalize loader to return the module as form { default: Component } for `React.lazy`.
|
// 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
|
// 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.
|
// Client component reference proxy need to be converted to a module.
|
||||||
function convertModule<P>(mod: React.ComponentType<P> | ComponentModule<P>) {
|
function convertModule<P>(mod: React.ComponentType<P> | ComponentModule<P>) {
|
||||||
return { default: (mod as ComponentModule<P>)?.default || mod }
|
return { default: (mod as ComponentModule<P>)?.default ?? mod }
|
||||||
}
|
}
|
||||||
|
|
||||||
function Loadable(options: any) {
|
const defaultOptions = {
|
||||||
const opts = {
|
loader: () => Promise.resolve(convertModule(() => null)),
|
||||||
loader: null,
|
loading: null,
|
||||||
loading: null,
|
ssr: true,
|
||||||
ssr: true,
|
}
|
||||||
...options,
|
|
||||||
}
|
|
||||||
|
|
||||||
const loader = () =>
|
interface LoadableOptions {
|
||||||
opts.loader != null
|
loader?: () => Promise<React.ComponentType<any> | ComponentModule<any>>
|
||||||
? opts.loader().then(convertModule)
|
loading?: React.ComponentType<any> | null
|
||||||
: Promise.resolve(convertModule(() => 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 Loading = opts.loading
|
||||||
const Wrap = opts.ssr ? Fragment : NoSSR
|
|
||||||
|
|
||||||
function LoadableComponent(props: any) {
|
function LoadableComponent(props: any) {
|
||||||
const fallbackElement = Loading ? (
|
const fallbackElement = Loading ? (
|
||||||
<Loading isLoading={true} pastDelay={true} error={null} />
|
<Loading isLoading={true} pastDelay={true} error={null} />
|
||||||
) : null
|
) : null
|
||||||
|
|
||||||
return (
|
const children = opts.ssr ? (
|
||||||
<Suspense fallback={fallbackElement}>
|
<Lazy {...props} />
|
||||||
<Wrap>
|
) : (
|
||||||
<Lazy {...props} />
|
<BailoutToCSR reason="next/dynamic">
|
||||||
</Wrap>
|
<Lazy {...props} />
|
||||||
</Suspense>
|
</BailoutToCSR>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return <Suspense fallback={fallbackElement}>{children}</Suspense>
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadableComponent.displayName = 'LoadableComponent'
|
LoadableComponent.displayName = 'LoadableComponent'
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -494,235 +494,231 @@ createNextDescribe(
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(files.sort()).toEqual(
|
expect(files.sort()).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
'page.js',
|
"(new)/custom/page.js",
|
||||||
'index.rsc',
|
"(new)/custom/page_client-reference-manifest.js",
|
||||||
'index.html',
|
"_not-found.html",
|
||||||
'blog/seb.rsc',
|
"_not-found.js",
|
||||||
'blog/tim.rsc',
|
"_not-found.rsc",
|
||||||
'_not-found.js',
|
"_not-found_client-reference-manifest.js",
|
||||||
'blog/seb.html',
|
"api/draft-mode/route.js",
|
||||||
'blog/tim.html',
|
"api/large-data/route.js",
|
||||||
'isr-error-handling.rsc',
|
"api/revalidate-path-edge/route.js",
|
||||||
'_not-found.rsc',
|
"api/revalidate-path-node/route.js",
|
||||||
'_not-found.html',
|
"api/revalidate-tag-edge/route.js",
|
||||||
'blog/styfle.rsc',
|
"api/revalidate-tag-node/route.js",
|
||||||
'force-cache.rsc',
|
"articles/[slug]/page.js",
|
||||||
'blog/styfle.html',
|
"articles/[slug]/page_client-reference-manifest.js",
|
||||||
'force-cache.html',
|
"articles/works.html",
|
||||||
'isr-error-handling/page.js',
|
"articles/works.rsc",
|
||||||
'ssg-draft-mode.rsc',
|
"blog/[author]/[slug]/page.js",
|
||||||
'ssr-forced/page.js',
|
"blog/[author]/[slug]/page_client-reference-manifest.js",
|
||||||
'articles/works.rsc',
|
"blog/[author]/page.js",
|
||||||
'force-cache/page.js',
|
"blog/[author]/page_client-reference-manifest.js",
|
||||||
'force-cache/large-data/page.js',
|
"blog/seb.html",
|
||||||
'force-cache/large-data/page_client-reference-manifest.js',
|
"blog/seb.rsc",
|
||||||
'ssg-draft-mode.html',
|
"blog/seb/second-post.html",
|
||||||
'articles/works.html',
|
"blog/seb/second-post.rsc",
|
||||||
'no-store/static.rsc',
|
"blog/styfle.html",
|
||||||
'(new)/custom/page.js',
|
"blog/styfle.rsc",
|
||||||
'force-static/page.js',
|
"blog/styfle/first-post.html",
|
||||||
'response-url/page.js',
|
"blog/styfle/first-post.rsc",
|
||||||
'no-store/static.html',
|
"blog/styfle/second-post.html",
|
||||||
'blog/[author]/page.js',
|
"blog/styfle/second-post.rsc",
|
||||||
'default-cache/page.js',
|
"blog/tim.html",
|
||||||
'fetch-no-cache/page.js',
|
"blog/tim.rsc",
|
||||||
'force-no-store/page.js',
|
"blog/tim/first-post.html",
|
||||||
'force-static-fetch-no-store.html',
|
"blog/tim/first-post.rsc",
|
||||||
'force-static-fetch-no-store.rsc',
|
"default-cache/page.js",
|
||||||
'force-static-fetch-no-store/page.js',
|
"default-cache/page_client-reference-manifest.js",
|
||||||
'force-static-fetch-no-store/page_client-reference-manifest.js',
|
"dynamic-error/[id]/page.js",
|
||||||
'force-static/first.rsc',
|
"dynamic-error/[id]/page_client-reference-manifest.js",
|
||||||
'api/draft-mode/route.js',
|
"dynamic-no-gen-params-ssr/[slug]/page.js",
|
||||||
'api/large-data/route.js',
|
"dynamic-no-gen-params-ssr/[slug]/page_client-reference-manifest.js",
|
||||||
'blog/tim/first-post.rsc',
|
"dynamic-no-gen-params/[slug]/page.js",
|
||||||
'force-static/first.html',
|
"dynamic-no-gen-params/[slug]/page_client-reference-manifest.js",
|
||||||
'force-static/second.rsc',
|
"fetch-no-cache/page.js",
|
||||||
'ssg-draft-mode/test.rsc',
|
"fetch-no-cache/page_client-reference-manifest.js",
|
||||||
'isr-error-handling.html',
|
"flight/[slug]/[slug2]/page.js",
|
||||||
'articles/[slug]/page.js',
|
"flight/[slug]/[slug2]/page_client-reference-manifest.js",
|
||||||
'no-store/static/page.js',
|
"force-cache.html",
|
||||||
'blog/seb/second-post.rsc',
|
"force-cache.rsc",
|
||||||
'blog/tim/first-post.html',
|
"force-cache/large-data/page.js",
|
||||||
'force-static/second.html',
|
"force-cache/large-data/page_client-reference-manifest.js",
|
||||||
'ssg-draft-mode/test.html',
|
"force-cache/page.js",
|
||||||
'no-store/dynamic/page.js',
|
"force-cache/page_client-reference-manifest.js",
|
||||||
'blog/seb/second-post.html',
|
"force-dynamic-catch-all/[slug]/[[...id]]/page.js",
|
||||||
'ssg-draft-mode/test-2.rsc',
|
"force-dynamic-catch-all/[slug]/[[...id]]/page_client-reference-manifest.js",
|
||||||
'blog/styfle/first-post.rsc',
|
"force-dynamic-no-prerender/[id]/page.js",
|
||||||
'dynamic-error/[id]/page.js',
|
"force-dynamic-no-prerender/[id]/page_client-reference-manifest.js",
|
||||||
'ssg-draft-mode/test-2.html',
|
"force-dynamic-prerender/[slug]/page.js",
|
||||||
'blog/styfle/first-post.html',
|
"force-dynamic-prerender/[slug]/page_client-reference-manifest.js",
|
||||||
'blog/styfle/second-post.rsc',
|
"force-no-store/page.js",
|
||||||
'force-static/[slug]/page.js',
|
"force-no-store/page_client-reference-manifest.js",
|
||||||
'hooks/use-pathname/slug.rsc',
|
"force-static-fetch-no-store.html",
|
||||||
'route-handler/post/route.js',
|
"force-static-fetch-no-store.rsc",
|
||||||
'blog/[author]/[slug]/page.js',
|
"force-static-fetch-no-store/page.js",
|
||||||
'blog/styfle/second-post.html',
|
"force-static-fetch-no-store/page_client-reference-manifest.js",
|
||||||
'hooks/use-pathname/slug.html',
|
"force-static/[slug]/page.js",
|
||||||
'flight/[slug]/[slug2]/page.js',
|
"force-static/[slug]/page_client-reference-manifest.js",
|
||||||
'variable-revalidate/cookie.rsc',
|
"force-static/first.html",
|
||||||
'ssr-auto/cache-no-store/page.js',
|
"force-static/first.rsc",
|
||||||
'variable-revalidate/cookie.html',
|
"force-static/page.js",
|
||||||
'api/revalidate-tag-edge/route.js',
|
"force-static/page_client-reference-manifest.js",
|
||||||
'api/revalidate-tag-node/route.js',
|
"force-static/second.html",
|
||||||
'variable-revalidate/encoding.rsc',
|
"force-static/second.rsc",
|
||||||
'api/revalidate-path-edge/route.js',
|
"gen-params-dynamic-revalidate/[slug]/page.js",
|
||||||
'api/revalidate-path-node/route.js',
|
"gen-params-dynamic-revalidate/[slug]/page_client-reference-manifest.js",
|
||||||
'gen-params-dynamic/[slug]/page.js',
|
"gen-params-dynamic-revalidate/one.html",
|
||||||
'hooks/use-pathname/[slug]/page.js',
|
"gen-params-dynamic-revalidate/one.rsc",
|
||||||
'page_client-reference-manifest.js',
|
"gen-params-dynamic/[slug]/page.js",
|
||||||
'react-fetch-deduping-edge/page.js',
|
"gen-params-dynamic/[slug]/page_client-reference-manifest.js",
|
||||||
'react-fetch-deduping-node/page.js',
|
"hooks/use-pathname/[slug]/page.js",
|
||||||
'variable-revalidate/encoding.html',
|
"hooks/use-pathname/[slug]/page_client-reference-manifest.js",
|
||||||
'variable-revalidate/cookie/page.js',
|
"hooks/use-pathname/slug.html",
|
||||||
'ssg-draft-mode/[[...route]]/page.js',
|
"hooks/use-pathname/slug.rsc",
|
||||||
'variable-revalidate/post-method.rsc',
|
"hooks/use-search-params/force-static.html",
|
||||||
'stale-cache-serving/app-page/page.js',
|
"hooks/use-search-params/force-static.rsc",
|
||||||
'dynamic-no-gen-params/[slug]/page.js',
|
"hooks/use-search-params/force-static/page.js",
|
||||||
'static-to-dynamic-error/[id]/page.js',
|
"hooks/use-search-params/force-static/page_client-reference-manifest.js",
|
||||||
'variable-revalidate/encoding/page.js',
|
"hooks/use-search-params/with-suspense.html",
|
||||||
'variable-revalidate/no-store/page.js',
|
"hooks/use-search-params/with-suspense.rsc",
|
||||||
'variable-revalidate/post-method.html',
|
"hooks/use-search-params/with-suspense/page.js",
|
||||||
'variable-revalidate/revalidate-3.rsc',
|
"hooks/use-search-params/with-suspense/page_client-reference-manifest.js",
|
||||||
'gen-params-dynamic-revalidate/one.rsc',
|
"index.html",
|
||||||
'route-handler/revalidate-360/route.js',
|
"index.rsc",
|
||||||
'route-handler/static-cookies/route.js',
|
"isr-error-handling.html",
|
||||||
'variable-revalidate-edge/body/page.js',
|
"isr-error-handling.rsc",
|
||||||
'variable-revalidate/authorization.rsc',
|
"isr-error-handling/page.js",
|
||||||
'variable-revalidate/revalidate-3.html',
|
"isr-error-handling/page_client-reference-manifest.js",
|
||||||
'force-dynamic-prerender/[slug]/page.js',
|
"no-store/dynamic/page.js",
|
||||||
'gen-params-dynamic-revalidate/one.html',
|
"no-store/dynamic/page_client-reference-manifest.js",
|
||||||
'ssr-auto/fetch-revalidate-zero/page.js',
|
"no-store/static.html",
|
||||||
'variable-revalidate/authorization.html',
|
"no-store/static.rsc",
|
||||||
'_not-found_client-reference-manifest.js',
|
"no-store/static/page.js",
|
||||||
'force-dynamic-no-prerender/[id]/page.js',
|
"no-store/static/page_client-reference-manifest.js",
|
||||||
'variable-revalidate/post-method/page.js',
|
"page.js",
|
||||||
'variable-revalidate/status-code/page.js',
|
"page_client-reference-manifest.js",
|
||||||
'dynamic-no-gen-params-ssr/[slug]/page.js',
|
"partial-gen-params-no-additional-lang/[lang]/[slug]/page.js",
|
||||||
'hooks/use-search-params/force-static.rsc',
|
"partial-gen-params-no-additional-lang/[lang]/[slug]/page_client-reference-manifest.js",
|
||||||
'partial-gen-params/[lang]/[slug]/page.js',
|
"partial-gen-params-no-additional-lang/en/RAND.html",
|
||||||
'variable-revalidate/headers-instance.rsc',
|
"partial-gen-params-no-additional-lang/en/RAND.rsc",
|
||||||
'variable-revalidate/revalidate-3/page.js',
|
"partial-gen-params-no-additional-lang/en/first.html",
|
||||||
'stale-cache-serving-edge/app-page/page.js',
|
"partial-gen-params-no-additional-lang/en/first.rsc",
|
||||||
'hooks/use-search-params/force-static.html',
|
"partial-gen-params-no-additional-lang/en/second.html",
|
||||||
'hooks/use-search-params/with-suspense.rsc',
|
"partial-gen-params-no-additional-lang/en/second.rsc",
|
||||||
'route-handler/revalidate-360-isr/route.js',
|
"partial-gen-params-no-additional-lang/fr/RAND.html",
|
||||||
'variable-revalidate-edge/encoding/page.js',
|
"partial-gen-params-no-additional-lang/fr/RAND.rsc",
|
||||||
'variable-revalidate-edge/no-store/page.js',
|
"partial-gen-params-no-additional-lang/fr/first.html",
|
||||||
'variable-revalidate/authorization/page.js',
|
"partial-gen-params-no-additional-lang/fr/first.rsc",
|
||||||
'variable-revalidate/headers-instance.html',
|
"partial-gen-params-no-additional-lang/fr/second.html",
|
||||||
'stale-cache-serving/route-handler/route.js',
|
"partial-gen-params-no-additional-lang/fr/second.rsc",
|
||||||
'hooks/use-search-params/with-suspense.html',
|
"partial-gen-params-no-additional-slug/[lang]/[slug]/page.js",
|
||||||
'route-handler-edge/revalidate-360/route.js',
|
"partial-gen-params-no-additional-slug/[lang]/[slug]/page_client-reference-manifest.js",
|
||||||
'variable-revalidate/revalidate-360-isr.rsc',
|
"partial-gen-params-no-additional-slug/en/RAND.html",
|
||||||
'variable-revalidate/revalidate-360/page.js',
|
"partial-gen-params-no-additional-slug/en/RAND.rsc",
|
||||||
'static-to-dynamic-error-forced/[id]/page.js',
|
"partial-gen-params-no-additional-slug/en/first.html",
|
||||||
'variable-config-revalidate/revalidate-3.rsc',
|
"partial-gen-params-no-additional-slug/en/first.rsc",
|
||||||
'variable-revalidate/revalidate-360-isr.html',
|
"partial-gen-params-no-additional-slug/en/second.html",
|
||||||
'isr-error-handling/page_client-reference-manifest.js',
|
"partial-gen-params-no-additional-slug/en/second.rsc",
|
||||||
'gen-params-dynamic-revalidate/[slug]/page.js',
|
"partial-gen-params-no-additional-slug/fr/RAND.html",
|
||||||
'hooks/use-search-params/force-static/page.js',
|
"partial-gen-params-no-additional-slug/fr/RAND.rsc",
|
||||||
'ssr-forced/page_client-reference-manifest.js',
|
"partial-gen-params-no-additional-slug/fr/first.html",
|
||||||
'variable-config-revalidate/revalidate-3.html',
|
"partial-gen-params-no-additional-slug/fr/first.rsc",
|
||||||
'variable-revalidate-edge/post-method/page.js',
|
"partial-gen-params-no-additional-slug/fr/second.html",
|
||||||
'variable-revalidate/headers-instance/page.js',
|
"partial-gen-params-no-additional-slug/fr/second.rsc",
|
||||||
'force-cache/page_client-reference-manifest.js',
|
"partial-gen-params/[lang]/[slug]/page.js",
|
||||||
'hooks/use-search-params/with-suspense/page.js',
|
"partial-gen-params/[lang]/[slug]/page_client-reference-manifest.js",
|
||||||
'variable-revalidate-edge/revalidate-3/page.js',
|
"react-fetch-deduping-edge/page.js",
|
||||||
'(new)/custom/page_client-reference-manifest.js',
|
"react-fetch-deduping-edge/page_client-reference-manifest.js",
|
||||||
'force-static/page_client-reference-manifest.js',
|
"react-fetch-deduping-node/page.js",
|
||||||
'response-url/page_client-reference-manifest.js',
|
"react-fetch-deduping-node/page_client-reference-manifest.js",
|
||||||
'variable-revalidate/revalidate-360-isr/page.js',
|
"response-url/page.js",
|
||||||
'stale-cache-serving-edge/route-handler/route.js',
|
"response-url/page_client-reference-manifest.js",
|
||||||
'blog/[author]/page_client-reference-manifest.js',
|
"route-handler-edge/revalidate-360/route.js",
|
||||||
'default-cache/page_client-reference-manifest.js',
|
"route-handler/post/route.js",
|
||||||
'variable-config-revalidate/revalidate-3/page.js',
|
"route-handler/revalidate-360-isr/route.js",
|
||||||
'variable-revalidate/post-method-request/page.js',
|
"route-handler/revalidate-360/route.js",
|
||||||
'fetch-no-cache/page_client-reference-manifest.js',
|
"route-handler/static-cookies/route.js",
|
||||||
'force-dynamic-catch-all/[slug]/[[...id]]/page.js',
|
"ssg-draft-mode.html",
|
||||||
'force-no-store/page_client-reference-manifest.js',
|
"ssg-draft-mode.rsc",
|
||||||
'partial-gen-params-no-additional-lang/en/RAND.rsc',
|
"ssg-draft-mode/[[...route]]/page.js",
|
||||||
'partial-gen-params-no-additional-lang/fr/RAND.rsc',
|
"ssg-draft-mode/[[...route]]/page_client-reference-manifest.js",
|
||||||
'partial-gen-params-no-additional-slug/en/RAND.rsc',
|
"ssg-draft-mode/test-2.html",
|
||||||
'partial-gen-params-no-additional-slug/fr/RAND.rsc',
|
"ssg-draft-mode/test-2.rsc",
|
||||||
'articles/[slug]/page_client-reference-manifest.js',
|
"ssg-draft-mode/test.html",
|
||||||
'no-store/static/page_client-reference-manifest.js',
|
"ssg-draft-mode/test.rsc",
|
||||||
'partial-gen-params-no-additional-lang/en/RAND.html',
|
"ssr-auto/cache-no-store/page.js",
|
||||||
'partial-gen-params-no-additional-lang/en/first.rsc',
|
"ssr-auto/cache-no-store/page_client-reference-manifest.js",
|
||||||
'partial-gen-params-no-additional-lang/fr/RAND.html',
|
"ssr-auto/fetch-revalidate-zero/page.js",
|
||||||
'partial-gen-params-no-additional-lang/fr/first.rsc',
|
"ssr-auto/fetch-revalidate-zero/page_client-reference-manifest.js",
|
||||||
'partial-gen-params-no-additional-slug/en/RAND.html',
|
"ssr-forced/page.js",
|
||||||
'partial-gen-params-no-additional-slug/en/first.rsc',
|
"ssr-forced/page_client-reference-manifest.js",
|
||||||
'partial-gen-params-no-additional-slug/fr/RAND.html',
|
"stale-cache-serving-edge/app-page/page.js",
|
||||||
'partial-gen-params-no-additional-slug/fr/first.rsc',
|
"stale-cache-serving-edge/app-page/page_client-reference-manifest.js",
|
||||||
'no-store/dynamic/page_client-reference-manifest.js',
|
"stale-cache-serving-edge/route-handler/route.js",
|
||||||
'partial-gen-params-no-additional-lang/en/first.html',
|
"stale-cache-serving/app-page/page.js",
|
||||||
'partial-gen-params-no-additional-lang/en/second.rsc',
|
"stale-cache-serving/app-page/page_client-reference-manifest.js",
|
||||||
'partial-gen-params-no-additional-lang/fr/first.html',
|
"stale-cache-serving/route-handler/route.js",
|
||||||
'partial-gen-params-no-additional-lang/fr/second.rsc',
|
"static-to-dynamic-error-forced/[id]/page.js",
|
||||||
'partial-gen-params-no-additional-slug/en/first.html',
|
"static-to-dynamic-error-forced/[id]/page_client-reference-manifest.js",
|
||||||
'partial-gen-params-no-additional-slug/en/second.rsc',
|
"static-to-dynamic-error/[id]/page.js",
|
||||||
'partial-gen-params-no-additional-slug/fr/first.html',
|
"static-to-dynamic-error/[id]/page_client-reference-manifest.js",
|
||||||
'partial-gen-params-no-additional-slug/fr/second.rsc',
|
"variable-config-revalidate/revalidate-3.html",
|
||||||
'dynamic-error/[id]/page_client-reference-manifest.js',
|
"variable-config-revalidate/revalidate-3.rsc",
|
||||||
'partial-gen-params-no-additional-lang/en/second.html',
|
"variable-config-revalidate/revalidate-3/page.js",
|
||||||
'partial-gen-params-no-additional-lang/fr/second.html',
|
"variable-config-revalidate/revalidate-3/page_client-reference-manifest.js",
|
||||||
'partial-gen-params-no-additional-slug/en/second.html',
|
"variable-revalidate-edge/body/page.js",
|
||||||
'partial-gen-params-no-additional-slug/fr/second.html',
|
"variable-revalidate-edge/body/page_client-reference-manifest.js",
|
||||||
'variable-revalidate-edge/post-method-request/page.js',
|
"variable-revalidate-edge/encoding/page.js",
|
||||||
'force-static/[slug]/page_client-reference-manifest.js',
|
"variable-revalidate-edge/encoding/page_client-reference-manifest.js",
|
||||||
'blog/[author]/[slug]/page_client-reference-manifest.js',
|
"variable-revalidate-edge/no-store/page.js",
|
||||||
'flight/[slug]/[slug2]/page_client-reference-manifest.js',
|
"variable-revalidate-edge/no-store/page_client-reference-manifest.js",
|
||||||
'hooks/use-search-params/static-bailout.html',
|
"variable-revalidate-edge/post-method-request/page.js",
|
||||||
'hooks/use-search-params/static-bailout.rsc',
|
"variable-revalidate-edge/post-method-request/page_client-reference-manifest.js",
|
||||||
'hooks/use-search-params/static-bailout/page.js',
|
"variable-revalidate-edge/post-method/page.js",
|
||||||
'hooks/use-search-params/static-bailout/page_client-reference-manifest.js',
|
"variable-revalidate-edge/post-method/page_client-reference-manifest.js",
|
||||||
'ssr-auto/cache-no-store/page_client-reference-manifest.js',
|
"variable-revalidate-edge/revalidate-3/page.js",
|
||||||
'gen-params-dynamic/[slug]/page_client-reference-manifest.js',
|
"variable-revalidate-edge/revalidate-3/page_client-reference-manifest.js",
|
||||||
'hooks/use-pathname/[slug]/page_client-reference-manifest.js',
|
"variable-revalidate/authorization.html",
|
||||||
'partial-gen-params-no-additional-lang/[lang]/[slug]/page.js',
|
"variable-revalidate/authorization.rsc",
|
||||||
'partial-gen-params-no-additional-slug/[lang]/[slug]/page.js',
|
"variable-revalidate/authorization/page.js",
|
||||||
'react-fetch-deduping-edge/page_client-reference-manifest.js',
|
"variable-revalidate/authorization/page_client-reference-manifest.js",
|
||||||
'react-fetch-deduping-node/page_client-reference-manifest.js',
|
"variable-revalidate/cookie.html",
|
||||||
'variable-revalidate/cookie/page_client-reference-manifest.js',
|
"variable-revalidate/cookie.rsc",
|
||||||
'ssg-draft-mode/[[...route]]/page_client-reference-manifest.js',
|
"variable-revalidate/cookie/page.js",
|
||||||
'stale-cache-serving/app-page/page_client-reference-manifest.js',
|
"variable-revalidate/cookie/page_client-reference-manifest.js",
|
||||||
'dynamic-no-gen-params/[slug]/page_client-reference-manifest.js',
|
"variable-revalidate/encoding.html",
|
||||||
'static-to-dynamic-error/[id]/page_client-reference-manifest.js',
|
"variable-revalidate/encoding.rsc",
|
||||||
'variable-revalidate/encoding/page_client-reference-manifest.js',
|
"variable-revalidate/encoding/page.js",
|
||||||
'variable-revalidate/no-store/page_client-reference-manifest.js',
|
"variable-revalidate/encoding/page_client-reference-manifest.js",
|
||||||
'variable-revalidate-edge/body/page_client-reference-manifest.js',
|
"variable-revalidate/headers-instance.html",
|
||||||
'force-dynamic-prerender/[slug]/page_client-reference-manifest.js',
|
"variable-revalidate/headers-instance.rsc",
|
||||||
'ssr-auto/fetch-revalidate-zero/page_client-reference-manifest.js',
|
"variable-revalidate/headers-instance/page.js",
|
||||||
'force-dynamic-no-prerender/[id]/page_client-reference-manifest.js',
|
"variable-revalidate/headers-instance/page_client-reference-manifest.js",
|
||||||
'variable-revalidate/post-method/page_client-reference-manifest.js',
|
"variable-revalidate/no-store/page.js",
|
||||||
'variable-revalidate/status-code/page_client-reference-manifest.js',
|
"variable-revalidate/no-store/page_client-reference-manifest.js",
|
||||||
'dynamic-no-gen-params-ssr/[slug]/page_client-reference-manifest.js',
|
"variable-revalidate/post-method-request/page.js",
|
||||||
'partial-gen-params/[lang]/[slug]/page_client-reference-manifest.js',
|
"variable-revalidate/post-method-request/page_client-reference-manifest.js",
|
||||||
'variable-revalidate/revalidate-3/page_client-reference-manifest.js',
|
"variable-revalidate/post-method.html",
|
||||||
'stale-cache-serving-edge/app-page/page_client-reference-manifest.js',
|
"variable-revalidate/post-method.rsc",
|
||||||
'variable-revalidate-edge/encoding/page_client-reference-manifest.js',
|
"variable-revalidate/post-method/page.js",
|
||||||
'variable-revalidate-edge/no-store/page_client-reference-manifest.js',
|
"variable-revalidate/post-method/page_client-reference-manifest.js",
|
||||||
'variable-revalidate/authorization/page_client-reference-manifest.js',
|
"variable-revalidate/revalidate-3.html",
|
||||||
'variable-revalidate/revalidate-360/page_client-reference-manifest.js',
|
"variable-revalidate/revalidate-3.rsc",
|
||||||
'static-to-dynamic-error-forced/[id]/page_client-reference-manifest.js',
|
"variable-revalidate/revalidate-3/page.js",
|
||||||
'gen-params-dynamic-revalidate/[slug]/page_client-reference-manifest.js',
|
"variable-revalidate/revalidate-3/page_client-reference-manifest.js",
|
||||||
'hooks/use-search-params/force-static/page_client-reference-manifest.js',
|
"variable-revalidate/revalidate-360-isr.html",
|
||||||
'variable-revalidate-edge/post-method/page_client-reference-manifest.js',
|
"variable-revalidate/revalidate-360-isr.rsc",
|
||||||
'variable-revalidate/headers-instance/page_client-reference-manifest.js',
|
"variable-revalidate/revalidate-360-isr/page.js",
|
||||||
'hooks/use-search-params/with-suspense/page_client-reference-manifest.js',
|
"variable-revalidate/revalidate-360-isr/page_client-reference-manifest.js",
|
||||||
'variable-revalidate-edge/revalidate-3/page_client-reference-manifest.js',
|
"variable-revalidate/revalidate-360/page.js",
|
||||||
'variable-revalidate/revalidate-360-isr/page_client-reference-manifest.js',
|
"variable-revalidate/revalidate-360/page_client-reference-manifest.js",
|
||||||
'variable-config-revalidate/revalidate-3/page_client-reference-manifest.js',
|
"variable-revalidate/status-code/page.js",
|
||||||
'variable-revalidate/post-method-request/page_client-reference-manifest.js',
|
"variable-revalidate/status-code/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()
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should have correct prerender-manifest entries', async () => {
|
it('should have correct prerender-manifest entries', async () => {
|
||||||
|
@ -1033,22 +1029,6 @@ createNextDescribe(
|
||||||
"initialRevalidateSeconds": false,
|
"initialRevalidateSeconds": false,
|
||||||
"srcRoute": "/hooks/use-search-params/force-static",
|
"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": {
|
"/hooks/use-search-params/with-suspense": {
|
||||||
"dataRoute": "/hooks/use-search-params/with-suspense.rsc",
|
"dataRoute": "/hooks/use-search-params/with-suspense.rsc",
|
||||||
"experimentalBypassFor": [
|
"experimentalBypassFor": [
|
||||||
|
@ -2888,26 +2868,6 @@ createNextDescribe(
|
||||||
|
|
||||||
describe('useSearchParams', () => {
|
describe('useSearchParams', () => {
|
||||||
describe('client', () => {
|
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 () => {
|
it('should bailout to client rendering - with suspense boundary', async () => {
|
||||||
const url =
|
const url =
|
||||||
'/hooks/use-search-params/with-suspense?first=value&second=other&third'
|
'/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
|
// Don't run these tests in dev mode since they won't be statically generated
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
describe('server response', () => {
|
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('<html id="__next_error__">')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should bailout to client rendering - with suspense boundary', async () => {
|
it('should bailout to client rendering - with suspense boundary', async () => {
|
||||||
const res = await next.fetch(
|
const res = await next.fetch(
|
||||||
'/hooks/use-search-params/with-suspense'
|
'/hooks/use-search-params/with-suspense'
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import UseSearchParams from '../search-params'
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p id="hooks-use-search-params" />
|
|
||||||
<UseSearchParams />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -13,7 +13,7 @@ module.exports = {
|
||||||
afterFiles: [
|
afterFiles: [
|
||||||
{
|
{
|
||||||
source: '/rewritten-use-search-params',
|
source: '/rewritten-use-search-params',
|
||||||
destination: '/hooks/use-search-params/static-bailout',
|
destination: '/hooks/use-search-params/with-suspense',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: '/rewritten-use-pathname',
|
source: '/rewritten-use-pathname',
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation'
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
|
<Component />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Component() {
|
||||||
const params = useSearchParams()
|
const params = useSearchParams()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -13,7 +13,9 @@ export default function Root({ children }) {
|
||||||
return (
|
return (
|
||||||
<html className="root-layout-html">
|
<html className="root-layout-html">
|
||||||
<body>
|
<body>
|
||||||
<NotFoundTrigger />
|
<React.Suspense fallback={<div>Loading...</div>}>
|
||||||
|
<NotFoundTrigger />
|
||||||
|
</React.Suspense>
|
||||||
<button id="trigger-not-found" onClick={() => setClicked(true)}>
|
<button id="trigger-not-found" onClick={() => setClicked(true)}>
|
||||||
Click to not found
|
Click to not found
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
export default function Layout({ children }) {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<head />
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
14
test/e2e/app-dir/use-search-params/app/layout.js
Normal file
14
test/e2e/app-dir/use-search-params/app/layout.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function Layout({ children }) {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<head />
|
||||||
|
<body>
|
||||||
|
<React.Suspense fallback={<div>Loading...</div>}>
|
||||||
|
{children}
|
||||||
|
</React.Suspense>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
8
test/e2e/app-dir/use-search-params/app/page.js
Normal file
8
test/e2e/app-dir/use-search-params/app/page.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
useSearchParams()
|
||||||
|
return <div>Page</div>
|
||||||
|
}
|
8
test/e2e/app-dir/use-search-params/next.config.js
Normal file
8
test/e2e/app-dir/use-search-params/next.config.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/** @type {import("next").NextConfig} */
|
||||||
|
const config = {
|
||||||
|
experimental: {
|
||||||
|
missingSuspenseWithCSRBailout: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config
|
35
test/e2e/app-dir/use-search-params/use-search-params.test.ts
Normal file
35
test/e2e/app-dir/use-search-params/use-search-params.test.ts
Normal file
|
@ -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')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
|
@ -1,6 +1,6 @@
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs-extra'
|
import { existsSync, promises as fs } from 'fs'
|
||||||
import treeKill from 'tree-kill'
|
import treeKill from 'tree-kill'
|
||||||
import type { NextConfig } from 'next'
|
import type { NextConfig } from 'next'
|
||||||
import { FileRef } from '../e2e-utils'
|
import { FileRef } from '../e2e-utils'
|
||||||
|
@ -100,17 +100,18 @@ export class NextInstance {
|
||||||
`FileRef passed to "files" in "createNext" is not a directory ${files.fsPath}`
|
`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 {
|
} else {
|
||||||
for (const filename of Object.keys(files)) {
|
for (const filename of Object.keys(files)) {
|
||||||
const item = files[filename]
|
const item = files[filename]
|
||||||
const outputFilename = path.join(this.testDir, filename)
|
const outputFilename = path.join(this.testDir, filename)
|
||||||
|
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
await fs.ensureDir(path.dirname(outputFilename))
|
await fs.mkdir(path.dirname(outputFilename), { recursive: true })
|
||||||
await fs.writeFile(outputFilename, item)
|
await fs.writeFile(outputFilename, item)
|
||||||
} else {
|
} 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) {
|
if (skipInstall || skipIsolatedNext) {
|
||||||
const pkgScripts = (this.packageJson['scripts'] as {}) || {}
|
const pkgScripts = (this.packageJson['scripts'] as {}) || {}
|
||||||
await fs.ensureDir(this.testDir)
|
await fs.mkdir(this.testDir, { recursive: true })
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(this.testDir, 'package.json'),
|
path.join(this.testDir, 'package.json'),
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
|
@ -193,7 +194,9 @@ export class NextInstance {
|
||||||
!this.packageJson &&
|
!this.packageJson &&
|
||||||
!(global as any).isNextDeploy
|
!(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 {
|
} else {
|
||||||
const { installDir } = await createNextInstall({
|
const { installDir } = await createNextInstall({
|
||||||
parentSpan: rootSpan,
|
parentSpan: rootSpan,
|
||||||
|
@ -218,7 +221,7 @@ export class NextInstance {
|
||||||
file.startsWith('next.config.')
|
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'
|
nextConfigFile = 'next.config.js'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,7 +333,10 @@ export class NextInstance {
|
||||||
]
|
]
|
||||||
for (const file of await fs.readdir(this.testDir)) {
|
for (const file of await fs.readdir(this.testDir)) {
|
||||||
if (!keptFiles.includes(file)) {
|
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()
|
await this.writeInitialFiles()
|
||||||
|
@ -378,7 +384,7 @@ export class NextInstance {
|
||||||
|
|
||||||
if (process.env.TRACE_PLAYWRIGHT) {
|
if (process.env.TRACE_PLAYWRIGHT) {
|
||||||
await fs
|
await fs
|
||||||
.copy(
|
.cp(
|
||||||
path.join(this.testDir, '.next/trace'),
|
path.join(this.testDir, '.next/trace'),
|
||||||
path.join(
|
path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
@ -390,7 +396,8 @@ export class NextInstance {
|
||||||
)
|
)
|
||||||
.replace(/\//g, '-')}`,
|
.replace(/\//g, '-')}`,
|
||||||
`next-trace`
|
`next-trace`
|
||||||
)
|
),
|
||||||
|
{ recursive: true }
|
||||||
)
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
require('console').error(e)
|
require('console').error(e)
|
||||||
|
@ -398,7 +405,7 @@ export class NextInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.NEXT_TEST_SKIP_CLEANUP) {
|
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`)
|
require('console').log(`destroyed next instance`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -424,13 +431,15 @@ export class NextInstance {
|
||||||
|
|
||||||
// TODO: block these in deploy mode
|
// TODO: block these in deploy mode
|
||||||
public async hasFile(filename: string) {
|
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) {
|
public async readFile(filename: string) {
|
||||||
return fs.readFile(path.join(this.testDir, filename), 'utf8')
|
return fs.readFile(path.join(this.testDir, filename), 'utf8')
|
||||||
}
|
}
|
||||||
public async readJSON(filename: string) {
|
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) {
|
private async handleDevWatchDelayBeforeChange(filename: string) {
|
||||||
// This is a temporary workaround for turbopack starting watching too late.
|
// This is a temporary workaround for turbopack starting watching too late.
|
||||||
|
@ -462,8 +471,8 @@ export class NextInstance {
|
||||||
await this.handleDevWatchDelayBeforeChange(filename)
|
await this.handleDevWatchDelayBeforeChange(filename)
|
||||||
|
|
||||||
const outputPath = path.join(this.testDir, filename)
|
const outputPath = path.join(this.testDir, filename)
|
||||||
const newFile = !(await fs.pathExists(outputPath))
|
const newFile = !existsSync(outputPath)
|
||||||
await fs.ensureDir(path.dirname(outputPath))
|
await fs.mkdir(path.dirname(outputPath), { recursive: true })
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
outputPath,
|
outputPath,
|
||||||
typeof content === 'function'
|
typeof content === 'function'
|
||||||
|
@ -486,12 +495,13 @@ export class NextInstance {
|
||||||
path.join(this.testDir, filename),
|
path.join(this.testDir, filename),
|
||||||
path.join(this.testDir, newFilename)
|
path.join(this.testDir, newFilename)
|
||||||
)
|
)
|
||||||
|
|
||||||
await this.handleDevWatchDelayAfterChange(filename)
|
await this.handleDevWatchDelayAfterChange(filename)
|
||||||
}
|
}
|
||||||
public async renameFolder(foldername: string, newFoldername: string) {
|
public async renameFolder(foldername: string, newFoldername: string) {
|
||||||
await this.handleDevWatchDelayBeforeChange(foldername)
|
await this.handleDevWatchDelayBeforeChange(foldername)
|
||||||
|
|
||||||
await fs.move(
|
await fs.rename(
|
||||||
path.join(this.testDir, foldername),
|
path.join(this.testDir, foldername),
|
||||||
path.join(this.testDir, newFoldername)
|
path.join(this.testDir, newFoldername)
|
||||||
)
|
)
|
||||||
|
@ -500,7 +510,11 @@ export class NextInstance {
|
||||||
public async deleteFile(filename: string) {
|
public async deleteFile(filename: string) {
|
||||||
await this.handleDevWatchDelayBeforeChange(filename)
|
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)
|
await this.handleDevWatchDelayAfterChange(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue