diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts
index 570684bb34baeff..82b54a3181dd8dd 100644
--- a/packages/next/build/webpack-config.ts
+++ b/packages/next/build/webpack-config.ts
@@ -769,9 +769,9 @@ export default async function getBaseWebpackConfig(
if (isLocal) {
// Makes sure dist/shared and dist/server are not bundled
- // we need to process shared/lib/router/router so that
- // the DefinePlugin can inject process.env values
- const isNextExternal = /next[/\\]dist[/\\](shared|server)[/\\](?!lib[/\\]router[/\\]router)/.test(
+ // we need to process shared `router/router` and `dynamic`,
+ // so that the DefinePlugin can inject process.env values
+ const isNextExternal = /next[/\\]dist[/\\](shared|server)[/\\](?!lib[/\\](router[/\\]router|dynamic))/.test(
res
)
@@ -1185,6 +1185,9 @@ export default async function getBaseWebpackConfig(
config.reactStrictMode
),
'process.env.__NEXT_REACT_ROOT': JSON.stringify(hasReactRoot),
+ 'process.env.__NEXT_CONCURRENT_FEATURES': JSON.stringify(
+ config.experimental.concurrentFeatures && hasReactRoot
+ ),
'process.env.__NEXT_OPTIMIZE_FONTS': JSON.stringify(
config.optimizeFonts && !dev
),
diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts
index 727037872228910..4e21021eba8761d 100644
--- a/packages/next/export/index.ts
+++ b/packages/next/export/index.ts
@@ -380,6 +380,7 @@ export default async function exportApp(
disableOptimizedLoading: nextConfig.experimental.disableOptimizedLoading,
// TODO: We should support dynamic HTML too
requireStaticHTML: true,
+ concurrentFeatures: nextConfig.experimental.concurrentFeatures,
}
const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
diff --git a/packages/next/shared/lib/dynamic.tsx b/packages/next/shared/lib/dynamic.tsx
index 0b260d19e964184..ba5871238998580 100644
--- a/packages/next/shared/lib/dynamic.tsx
+++ b/packages/next/shared/lib/dynamic.tsx
@@ -33,19 +33,24 @@ export type LoadableBaseOptions
= LoadableGeneratedOptions & {
ssr?: boolean
}
+export type LoadableSuspenseOptions
= {
+ loader: Loader
+ suspense?: boolean
+}
+
export type LoadableOptions
= LoadableBaseOptions
export type DynamicOptions
= LoadableBaseOptions
export type LoadableFn
= (
- opts: LoadableOptions
+ opts: LoadableOptions
| LoadableSuspenseOptions
) => React.ComponentType
export type LoadableComponent
= React.ComponentType
export function noSSR
(
LoadableInitializer: LoadableFn
,
- loadableOptions: LoadableOptions
+ loadableOptions: LoadableBaseOptions
): React.ComponentType
{
// Removing webpack and modules means react-loadable won't try preloading
delete loadableOptions.webpack
@@ -63,8 +68,6 @@ export function noSSR
(
)
}
-// function dynamic
(options: O):
-
export default function dynamic
(
dynamicOptions: DynamicOptions
| Loader
,
options?: DynamicOptions
@@ -110,6 +113,21 @@ export default function dynamic
(
// Support for passing options, eg: dynamic(import('../hello-world'), {loading: () =>
Loading something
})
loadableOptions = { ...loadableOptions, ...options }
+ const suspenseOptions = loadableOptions as LoadableSuspenseOptions
+ if (!process.env.__NEXT_CONCURRENT_FEATURES) {
+ // Error if react root is not enabled and `suspense` option is set to true
+ if (!process.env.__NEXT_REACT_ROOT && suspenseOptions.suspense) {
+ // TODO: add error doc when this feature is stable
+ throw new Error(
+ `Disallowed suspense option usage with next/dynamic in blocking mode`
+ )
+ }
+ suspenseOptions.suspense = false
+ }
+ if (suspenseOptions.suspense) {
+ return loadableFn(suspenseOptions)
+ }
+
// coming from build/babel/plugins/react-loadable-plugin.js
if (loadableOptions.loadableGenerated) {
loadableOptions = {
diff --git a/packages/next/shared/lib/loadable.js b/packages/next/shared/lib/loadable.js
index 73514019d8321c5..fbeaf9480f6f4e6 100644
--- a/packages/next/shared/lib/loadable.js
+++ b/packages/next/shared/lib/loadable.js
@@ -66,12 +66,16 @@ function createLoadableComponent(loadFn, options) {
timeout: null,
webpack: null,
modules: null,
+ suspense: false,
},
options
)
- let subscription = null
+ if (opts.suspense) {
+ opts.lazy = React.lazy(opts.loader)
+ }
+ let subscription = null
function init() {
if (!subscription) {
const sub = new LoadableSubscription(loadFn, opts)
@@ -86,7 +90,7 @@ function createLoadableComponent(loadFn, options) {
}
// Server only
- if (typeof window === 'undefined') {
+ if (typeof window === 'undefined' && !opts.suspense) {
ALL_INITIALIZERS.push(init)
}
@@ -95,7 +99,8 @@ function createLoadableComponent(loadFn, options) {
!initialized &&
typeof window !== 'undefined' &&
typeof opts.webpack === 'function' &&
- typeof require.resolveWeak === 'function'
+ typeof require.resolveWeak === 'function' &&
+ !opts.suspense
) {
const moduleIds = opts.webpack()
READY_INITIALIZERS.push((ids) => {
@@ -107,12 +112,11 @@ function createLoadableComponent(loadFn, options) {
})
}
- const LoadableComponent = (props, ref) => {
+ function LoadableImpl(props, ref) {
init()
const context = React.useContext(LoadableContext)
const state = useSubscription(subscription)
-
React.useImperativeHandle(
ref,
() => ({
@@ -144,7 +148,12 @@ function createLoadableComponent(loadFn, options) {
}, [props, state])
}
- LoadableComponent.preload = () => init()
+ function LazyImpl(props, ref) {
+ return React.createElement(opts.lazy, { ...props, ref })
+ }
+
+ const LoadableComponent = opts.suspense ? LazyImpl : LoadableImpl
+ LoadableComponent.preload = () => !opts.suspense && init()
LoadableComponent.displayName = 'LoadableComponent'
return React.forwardRef(LoadableComponent)
diff --git a/test/integration/chunking/test/index.test.js b/test/integration/chunking/test/index.test.js
index 24bcfd92d2d509c..a3c2bab19fafff7 100644
--- a/test/integration/chunking/test/index.test.js
+++ b/test/integration/chunking/test/index.test.js
@@ -84,7 +84,6 @@ describe('Chunking', () => {
it('should execute the build manifest', async () => {
const html = await renderViaHTTP(appPort, '/')
- console.log(html)
const $ = cheerio.load(html)
expect(
Array.from($('script'))
diff --git a/test/integration/react-18/app/components/bar.js b/test/integration/react-18/app/components/bar.js
new file mode 100644
index 000000000000000..44a8eb0864dc14f
--- /dev/null
+++ b/test/integration/react-18/app/components/bar.js
@@ -0,0 +1,24 @@
+import { Suspense } from 'react'
+import dynamic from 'next/dynamic'
+import { useCachedPromise } from './promise-cache'
+
+const Foo = dynamic(() => import('./foo'), {
+ suspense: true,
+})
+
+export default function Bar() {
+ useCachedPromise(
+ 'bar',
+ () => new Promise((resolve) => setTimeout(resolve, 300)),
+ true
+ )
+
+ return (
+
+ bar
+
+
+
+
+ )
+}
diff --git a/test/integration/react-18/app/components/dynamic-hello.js b/test/integration/react-18/app/components/dynamic-hello.js
new file mode 100644
index 000000000000000..61d0ecaea9b8804
--- /dev/null
+++ b/test/integration/react-18/app/components/dynamic-hello.js
@@ -0,0 +1,18 @@
+import { Suspense } from 'react'
+import dynamic from 'next/dynamic'
+
+let ssr
+const suspense = false
+
+const Hello = dynamic(() => import('./hello'), {
+ ssr,
+ suspense,
+})
+
+export default function DynamicHello(props) {
+ return (
+
+
+
+ )
+}
diff --git a/test/integration/react-18/app/components/foo.js b/test/integration/react-18/app/components/foo.js
new file mode 100644
index 000000000000000..3689dc33e306065
--- /dev/null
+++ b/test/integration/react-18/app/components/foo.js
@@ -0,0 +1,3 @@
+export default function Foo() {
+ return 'foo'
+}
diff --git a/test/integration/react-18/app/components/hello.js b/test/integration/react-18/app/components/hello.js
new file mode 100644
index 000000000000000..fc4d62a533fad10
--- /dev/null
+++ b/test/integration/react-18/app/components/hello.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+import { useCachedPromise } from './promise-cache'
+
+export default function Hello({ name, thrown = false }) {
+ useCachedPromise(
+ name,
+ () => new Promise((resolve) => setTimeout(resolve, 200)),
+ thrown
+ )
+
+ return hello {ReactDOM.version}
+}
diff --git a/test/integration/react-18/app/components/promise-cache.js b/test/integration/react-18/app/components/promise-cache.js
new file mode 100644
index 000000000000000..eab490fb9ae9282
--- /dev/null
+++ b/test/integration/react-18/app/components/promise-cache.js
@@ -0,0 +1,37 @@
+import React from 'react'
+
+const PromiseCacheContext = React.createContext(null)
+
+export const cache = new Map()
+export const PromiseCacheProvider = PromiseCacheContext.Provider
+
+export function useCachedPromise(key, fn, thrown = false) {
+ const cache = React.useContext(PromiseCacheContext)
+
+ if (!thrown) return undefined
+ let entry = cache.get(key)
+ if (!entry) {
+ entry = {
+ status: 'PENDING',
+ value: fn().then(
+ (value) => {
+ cache.set(key, {
+ status: 'RESOLVED',
+ value,
+ })
+ },
+ (err) => {
+ cache.set(key, {
+ status: 'REJECTED',
+ value: err,
+ })
+ }
+ ),
+ }
+ cache.set(key, entry)
+ }
+ if (['PENDING', 'REJECTED'].includes(entry.status)) {
+ throw entry.value
+ }
+ return entry.value
+}
diff --git a/test/integration/react-18/prerelease/next.config.js b/test/integration/react-18/app/next.config.js
similarity index 87%
rename from test/integration/react-18/prerelease/next.config.js
rename to test/integration/react-18/app/next.config.js
index d8f907bce9fce52..3604bd2c1ac01a7 100644
--- a/test/integration/react-18/prerelease/next.config.js
+++ b/test/integration/react-18/app/next.config.js
@@ -1,4 +1,8 @@
module.exports = {
+ experimental: {
+ reactRoot: true,
+ // concurrentFeatures: true,
+ },
webpack(config) {
const { alias } = config.resolve
// FIXME: resolving react/jsx-runtime https://github.com/facebook/react/issues/20235
diff --git a/test/integration/react-18/app/package.json b/test/integration/react-18/app/package.json
new file mode 100644
index 000000000000000..f9dafc993a79cae
--- /dev/null
+++ b/test/integration/react-18/app/package.json
@@ -0,0 +1,12 @@
+{
+ "scripts": {
+ "next": "node -r ../test/require-hook.js ../../../../packages/next/dist/bin/next",
+ "dev": "yarn next dev",
+ "build": "yarn next build",
+ "start": "yarn next start"
+ },
+ "dependencies": {
+ "react": "*",
+ "react-dom": "*"
+ }
+}
diff --git a/test/integration/react-18/app/pages/_app.js b/test/integration/react-18/app/pages/_app.js
new file mode 100644
index 000000000000000..db9e691d37e0254
--- /dev/null
+++ b/test/integration/react-18/app/pages/_app.js
@@ -0,0 +1,13 @@
+import { PromiseCacheProvider } from '../components/promise-cache'
+
+const cache = new Map()
+
+function MyApp({ Component, pageProps }) {
+ return (
+
+
+
+ )
+}
+
+export default MyApp
diff --git a/test/integration/react-18/app/pages/index.js b/test/integration/react-18/app/pages/index.js
new file mode 100644
index 000000000000000..ebca78cecee9e7f
--- /dev/null
+++ b/test/integration/react-18/app/pages/index.js
@@ -0,0 +1,12 @@
+import ReactDOM from 'react-dom'
+
+export default function Index() {
+ if (typeof window !== 'undefined') {
+ window.didHydrate = true
+ }
+ return (
+
+ )
+}
diff --git a/test/integration/react-18/app/pages/suspense/no-preload.js b/test/integration/react-18/app/pages/suspense/no-preload.js
new file mode 100644
index 000000000000000..aaf54e244118fff
--- /dev/null
+++ b/test/integration/react-18/app/pages/suspense/no-preload.js
@@ -0,0 +1,21 @@
+import { Suspense } from 'react'
+import dynamic from 'next/dynamic'
+
+const Bar = dynamic(() => import('../../components/bar'), {
+ suspense: true,
+ // Explicitly declare loaded modules.
+ // For suspense cases, they'll be ignored.
+ // For loadable component cases, they'll be handled
+ loadableGenerated: {
+ modules: ['../../components/bar'],
+ webpack: [require.resolveWeak('../../components/bar')],
+ },
+})
+
+export default function NoPreload() {
+ return (
+
+
+
+ )
+}
diff --git a/test/integration/react-18/app/pages/suspense/no-thrown.js b/test/integration/react-18/app/pages/suspense/no-thrown.js
new file mode 100644
index 000000000000000..123ab2d39b0fefd
--- /dev/null
+++ b/test/integration/react-18/app/pages/suspense/no-thrown.js
@@ -0,0 +1,5 @@
+import DynamicHello from '../../components/dynamic-hello'
+
+export default function NoThrown() {
+ return
+}
diff --git a/test/integration/react-18/app/pages/suspense/thrown.js b/test/integration/react-18/app/pages/suspense/thrown.js
new file mode 100644
index 000000000000000..3f8d40a49ac4fce
--- /dev/null
+++ b/test/integration/react-18/app/pages/suspense/thrown.js
@@ -0,0 +1,5 @@
+import DynamicHello from '../../components/dynamic-hello'
+
+export default function Thrown() {
+ return
+}
diff --git a/test/integration/react-18/app/pages/suspense/unwrapped.js b/test/integration/react-18/app/pages/suspense/unwrapped.js
new file mode 100644
index 000000000000000..3909e80679a6a9d
--- /dev/null
+++ b/test/integration/react-18/app/pages/suspense/unwrapped.js
@@ -0,0 +1,10 @@
+import React from 'react'
+import dynamic from 'next/dynamic'
+
+const Hello = dynamic(() => import('../../components/hello'), {
+ suspense: true,
+})
+
+export default function Unwrapped() {
+ return
+}
diff --git a/test/integration/react-18/prerelease/.gitignore b/test/integration/react-18/prerelease/.gitignore
deleted file mode 100644
index 736e8ae58ad87f0..000000000000000
--- a/test/integration/react-18/prerelease/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-!node_modules
\ No newline at end of file
diff --git a/test/integration/react-18/prerelease/node_modules/react-dom/package.json b/test/integration/react-18/prerelease/node_modules/react-dom/package.json
deleted file mode 100644
index 8986c6dd8a0748e..000000000000000
--- a/test/integration/react-18/prerelease/node_modules/react-dom/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "react-dom",
- "version": "18.0.0-alpha-c76e4dbbc-20210722"
-}
diff --git a/test/integration/react-18/prerelease/package.json b/test/integration/react-18/prerelease/package.json
deleted file mode 100644
index be823a58af7d766..000000000000000
--- a/test/integration/react-18/prerelease/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "dependencies": {
- "react": "*",
- "react-dom": "*"
- }
-}
diff --git a/test/integration/react-18/prerelease/pages/bar.js b/test/integration/react-18/prerelease/pages/bar.js
deleted file mode 100644
index 3b3b63d19aee596..000000000000000
--- a/test/integration/react-18/prerelease/pages/bar.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Bar() {
- return bar
-}
diff --git a/test/integration/react-18/prerelease/pages/index.js b/test/integration/react-18/prerelease/pages/index.js
deleted file mode 100644
index 4381bead3f6409b..000000000000000
--- a/test/integration/react-18/prerelease/pages/index.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Suspense } from 'react'
-import Bar from './bar'
-
-export default function Index() {
- if (typeof window !== 'undefined') {
- window.didHydrate = true
- }
- return (
-
- )
-}
diff --git a/test/integration/react-18/supported/pages/index.js b/test/integration/react-18/supported/pages/index.js
deleted file mode 100644
index fb077e8078c9e51..000000000000000
--- a/test/integration/react-18/supported/pages/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Index() {
- return Hello
-}
diff --git a/test/integration/react-18/test/basics.js b/test/integration/react-18/test/basics.js
new file mode 100644
index 000000000000000..bb3c4ad99ced4f7
--- /dev/null
+++ b/test/integration/react-18/test/basics.js
@@ -0,0 +1,31 @@
+/* eslint-env jest */
+
+import webdriver from 'next-webdriver'
+import cheerio from 'cheerio'
+import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils'
+
+export default (context) => {
+ it('hydrates correctly for normal page', async () => {
+ const browser = await webdriver(context.appPort, '/')
+ expect(await browser.eval('window.didHydrate')).toBe(true)
+ expect(await browser.elementById('react-dom-version').text()).toMatch(/18/)
+ })
+
+ it('should works with suspense in ssg', async () => {
+ const res1 = await fetchViaHTTP(context.appPort, '/suspense/thrown')
+ const res2 = await fetchViaHTTP(context.appPort, '/suspense/no-thrown')
+
+ expect(res1.status).toBe(200)
+ expect(res2.status).toBe(200)
+ })
+
+ it('should render fallback without preloads on server side', async () => {
+ const html = await renderViaHTTP(context.appPort, '/suspense/no-preload')
+ const $ = cheerio.load(html)
+ const nextData = JSON.parse($('#__NEXT_DATA__').text())
+ const content = $('#__next').text()
+ // is suspended
+ expect(content).toBe('rab')
+ expect(nextData.dynamicIds).toBeUndefined()
+ })
+}
diff --git a/test/integration/react-18/test/blocking.js b/test/integration/react-18/test/blocking.js
new file mode 100644
index 000000000000000..3adb476c5a018b2
--- /dev/null
+++ b/test/integration/react-18/test/blocking.js
@@ -0,0 +1,27 @@
+/* eslint-env jest */
+
+import cheerio from 'cheerio'
+
+export default (context, render) => {
+ async function get$(path, query) {
+ const html = await render(path, query)
+ return cheerio.load(html)
+ }
+
+ it('should render fallback on server side if suspense without preload', async () => {
+ const $ = await get$('/suspense/no-preload')
+ const nextData = JSON.parse($('#__NEXT_DATA__').text())
+ const content = $('#__next').text()
+ expect(content).toBe('rab')
+ expect(nextData.dynamicIds).toBeUndefined()
+ })
+
+ it('should render fallback on server side if suspended on server with preload', async () => {
+ const $ = await get$('/suspense/thrown')
+ const html = $('body').html()
+ expect(html).toContain('loading')
+ expect(
+ JSON.parse($('#__NEXT_DATA__').text()).dynamicIds
+ ).not.toBeUndefined()
+ })
+}
diff --git a/test/integration/react-18/test/concurrent.js b/test/integration/react-18/test/concurrent.js
new file mode 100644
index 000000000000000..e2aec94e452bd4b
--- /dev/null
+++ b/test/integration/react-18/test/concurrent.js
@@ -0,0 +1,47 @@
+/* eslint-env jest */
+
+import webdriver from 'next-webdriver'
+import cheerio from 'cheerio'
+import { check } from 'next-test-utils'
+
+export default (context, render) => {
+ async function get$(path, query) {
+ const html = await render(path, query)
+ return cheerio.load(html)
+ }
+
+ it('should resolve suspense modules on server side if suspense', async () => {
+ const $ = await get$('/suspense/no-preload')
+ const nextData = JSON.parse($('#__NEXT_DATA__').text())
+ const content = $('#__next').text()
+ expect(content).toBe('barfoo')
+ expect(nextData.dynamicIds).toBeUndefined()
+ })
+
+ it('should resolve suspense on server side if not suspended on server', async () => {
+ const $ = await get$('/suspense/no-thrown')
+ const html = $('body').html()
+ // there might be html comments between text, test hello only
+ expect(html).toContain('hello')
+ expect(JSON.parse($('#__NEXT_DATA__').text()).dynamicIds).toBeUndefined()
+ })
+
+ it('should resolve suspense on server side if suspended on server', async () => {
+ const $ = await get$('/suspense/thrown')
+ const html = $('body').html()
+ expect(html).toContain('hello')
+ expect(JSON.parse($('#__NEXT_DATA__').text()).dynamicIds).toBeUndefined()
+ })
+
+ it('should hydrate suspenses on client side if suspended on server', async () => {
+ let browser
+ try {
+ browser = await webdriver(context.appPort, '/suspense/thrown')
+ await check(() => browser.elementByCss('body').text(), /hello/)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+}
diff --git a/test/integration/react-18/test/index.test.js b/test/integration/react-18/test/index.test.js
index 34d7f9067880fc5..d0f889adcccd1bc 100644
--- a/test/integration/react-18/test/index.test.js
+++ b/test/integration/react-18/test/index.test.js
@@ -2,22 +2,30 @@
import { join } from 'path'
import fs from 'fs-extra'
-import webdriver from 'next-webdriver'
+
import {
+ File,
findPort,
killApp,
launchApp,
nextBuild,
nextStart,
+ renderViaHTTP,
} from 'next-test-utils'
+import blocking from './blocking'
+import concurrent from './concurrent'
+import basics from './basics'
jest.setTimeout(1000 * 60 * 5)
// overrides react and react-dom to v18
const nodeArgs = ['-r', join(__dirname, 'require-hook.js')]
-const dirSupported = join(__dirname, '../supported')
-const dirPrerelease = join(__dirname, '../prerelease')
+const appDir = join(__dirname, '../app')
+const nextConfig = new File(join(appDir, 'next.config.js'))
+const dynamicHello = new File(join(appDir, 'components/dynamic-hello.js'))
+const SUSPENSE_ERROR_MESSAGE =
+ 'Disallowed suspense option usage with next/dynamic'
const UNSUPPORTED_PRERELEASE =
"You are using an unsupported prerelease of 'react-dom'"
const USING_CREATE_ROOT = 'Using the createRoot API for React'
@@ -52,54 +60,129 @@ async function getDevOutput(dir) {
}
describe('React 18 Support', () => {
- describe('build', () => {
- test('supported version of React', async () => {
- const output = await getBuildOutput(dirSupported)
+ describe('no warns with stable supported version of react-dom', () => {
+ beforeAll(async () => {
+ await fs.remove(join(appDir, 'node_modules'))
+ nextConfig.replace('reactRoot: true', '// reactRoot: true')
+ })
+ afterAll(() => {
+ nextConfig.replace('// reactRoot: true', 'reactRoot: true')
+ })
+
+ test('supported version of react in dev', async () => {
+ const output = await getDevOutput(appDir)
expect(output).not.toMatch(USING_CREATE_ROOT)
expect(output).not.toMatch(UNSUPPORTED_PRERELEASE)
})
- test('prerelease version of React', async () => {
- const output = await getBuildOutput(dirPrerelease)
- expect(output).toMatch(USING_CREATE_ROOT)
- expect(output).toMatch(UNSUPPORTED_PRERELEASE)
+ test('supported version of react in build', async () => {
+ const output = await getBuildOutput(appDir)
+ expect(output).not.toMatch(USING_CREATE_ROOT)
+ expect(output).not.toMatch(UNSUPPORTED_PRERELEASE)
+ })
+
+ it('suspense is not allowed in blocking rendering mode', async () => {
+ const appPort = await findPort()
+ const app = await launchApp(appDir, appPort)
+ const html = await renderViaHTTP(appPort, '/suspense/unwrapped')
+ await killApp(app)
+ expect(html).toContain(SUSPENSE_ERROR_MESSAGE)
})
})
- describe('dev', () => {
- test('supported version of React', async () => {
- let output = await getDevOutput(dirSupported)
- expect(output).not.toMatch(USING_CREATE_ROOT)
- expect(output).not.toMatch(UNSUPPORTED_PRERELEASE)
+ describe('warns with stable supported version of react-dom', () => {
+ beforeAll(async () => {
+ const reactDomPkgPath = join(
+ appDir,
+ 'node_modules/react-dom/package.json'
+ )
+ await fs.outputJson(reactDomPkgPath, {
+ name: 'react-dom',
+ version: '18.0.0-alpha-c76e4dbbc-20210722',
+ })
+ })
+ afterAll(async () => await fs.remove(join(appDir, 'node_modules')))
+
+ test('prerelease version of react in dev', async () => {
+ const output = await getDevOutput(appDir)
+ expect(output).toMatch(USING_CREATE_ROOT)
+ expect(output).toMatch(UNSUPPORTED_PRERELEASE)
})
- test('prerelease version of React', async () => {
- let output = await getDevOutput(dirPrerelease)
+ test('prerelease version of react in build', async () => {
+ const output = await getBuildOutput(appDir)
expect(output).toMatch(USING_CREATE_ROOT)
expect(output).toMatch(UNSUPPORTED_PRERELEASE)
})
})
+})
+
+describe('Basics', () => {
+ runTests('default setting with react 18', 'dev', (context) => basics(context))
+ runTests('default setting with react 18', 'prod', (context) =>
+ basics(context)
+ )
+})
+
+describe('Blocking mode', () => {
+ beforeAll(() => {
+ dynamicHello.replace('suspense = false', `suspense = true`)
+ })
+ afterAll(() => {
+ dynamicHello.restore()
+ })
+
+ runTests('concurrentFeatures is disabled', 'dev', (context) =>
+ blocking(context, (p, q) => renderViaHTTP(context.appPort, p, q))
+ )
- describe('hydration', () => {
- const appDir = join(__dirname, '../prerelease')
- let app
- let appPort
+ runTests('concurrentFeatures is disabled', 'prod', (context) =>
+ blocking(context, (p, q) => renderViaHTTP(context.appPort, p, q))
+ )
+})
+
+describe('Concurrent mode', () => {
+ beforeAll(async () => {
+ nextConfig.replace(
+ '// concurrentFeatures: true',
+ 'concurrentFeatures: true'
+ )
+ dynamicHello.replace('suspense = false', `suspense = true`)
+ // `noSSR` mode will be ignored by suspense
+ dynamicHello.replace('let ssr', `let ssr = false`)
+ })
+ afterAll(async () => {
+ nextConfig.restore()
+ dynamicHello.restore()
+ })
+
+ runTests('concurrentFeatures is enabled', 'dev', (context) =>
+ concurrent(context, (p, q) => renderViaHTTP(context.appPort, p, q))
+ )
+ runTests('concurrentFeatures is enabled', 'prod', (context) =>
+ concurrent(context, (p, q) => renderViaHTTP(context.appPort, p, q))
+ )
+})
+
+function runTests(name, mode, fn) {
+ const context = { appDir }
+ describe(`${name} (${mode})`, () => {
beforeAll(async () => {
- await fs.remove(join(appDir, '.next'))
- await nextBuild(appDir, [dirPrerelease], {
- nodeArgs,
- stdout: true,
- stderr: true,
- })
- appPort = await findPort()
- app = await nextStart(appDir, appPort, { nodeArgs })
+ context.appPort = await findPort()
+ if (mode === 'dev') {
+ context.server = await launchApp(context.appDir, context.appPort, {
+ nodeArgs,
+ })
+ } else {
+ await nextBuild(context.appDir, [], { nodeArgs })
+ context.server = await nextStart(context.appDir, context.appPort, {
+ nodeArgs,
+ })
+ }
})
afterAll(async () => {
- await killApp(app)
- })
- it('hydrates correctly for normal page', async () => {
- const browser = await webdriver(appPort, '/')
- expect(await browser.eval('window.didHydrate')).toBe(true)
+ await killApp(context.server)
})
+ fn(context)
})
-})
+}
diff --git a/test/unit/next-dynamic.test.js b/test/unit/next-dynamic.test.js
index 31477e93f00ef82..e78e15d6ff0efa7 100644
--- a/test/unit/next-dynamic.test.js
+++ b/test/unit/next-dynamic.test.js
@@ -6,7 +6,7 @@ import { act, render } from '@testing-library/react'
import dynamic from 'next/dynamic'
describe('next/dynamic', () => {
- it('test link with unmount', () => {
+ it('test dynamic with jest', () => {
const App = dynamic(() => import('./fixtures/stub-components/hello'))
act(() => {
const { unmount } = render()