diff --git a/docs/advanced-features/react-18/overview.md b/docs/advanced-features/react-18/overview.md index 83124c62fcdbf..f43b46baa05f3 100644 --- a/docs/advanced-features/react-18/overview.md +++ b/docs/advanced-features/react-18/overview.md @@ -15,14 +15,20 @@ npm install next@latest react@latest react-dom@latest You can now start using React 18's new APIs like `startTransition` and `Suspense` in Next.js. -## Streaming SSR (Alpha) +## Streaming SSR -Streaming server-rendering (SSR) is an experimental feature in Next.js 12. When enabled, SSR will use the same [Edge Runtime](/docs/api-reference/edge-runtime.md) as [Middleware](/docs/middleware.md). +Next.js supports React 18 streaming server-rendering (SSR) out of the box. -[Learn how to enable streaming in Next.js.](/docs/advanced-features/react-18/streaming.md) +[Learn more about streaming in Next.js](/docs/advanced-features/react-18/streaming.md). ## React Server Components (Alpha) Server Components are a new feature in React that let you reduce your JavaScript bundle size by separating server and client-side code. Server Components allow developers to build apps that span the server and client, combining the rich interactivity of client-side apps with the improved performance of traditional server rendering. Server Components are still in research and development. [Learn how to try Server Components](/docs/advanced-features/react-18/server-components.md) as an experimental feature in Next.js. + +## Switchable Runtime (Alpha) + +Next.js supports changing the runtime of your application between Node.js and the [Edge Runtime](/docs/api-reference/edge-runtime.md) at the page level. For example, you can selectively configure specific pages to be server-side rendered in the Edge Runtime. + +This feature is still experimental. [Learn more about the switchable runtime](/docs/advanced-features/react-18/switchable-runtime.md). diff --git a/docs/advanced-features/react-18/streaming.md b/docs/advanced-features/react-18/streaming.md index 0734d0983464d..c5cfa6532d5e7 100644 --- a/docs/advanced-features/react-18/streaming.md +++ b/docs/advanced-features/react-18/streaming.md @@ -1,24 +1,17 @@ -# Streaming SSR (Alpha) +# Streaming SSR -React 18 will include architectural improvements to React server-side rendering (SSR) performance. This means you can use `Suspense` in your React components in streaming SSR mode and React will render them on the server and send them through HTTP streams. -It's worth noting that another experimental feature, React Server Components, is based on streaming. You can read more about server components related streaming APIs in [`next/streaming`](/docs/api-reference/next/streaming.md). However, this guide focuses on basic React 18 streaming. +React 18 includes architectural improvements to React server-side rendering (SSR) performance. This means you can use `Suspense` in your React components in streaming SSR mode and React will render content on the server and send updates through HTTP streams. +React Server Components, an experimental feature, is based on streaming. You can read more about Server Components related streaming APIs in [`next/streaming`](/docs/api-reference/next/streaming.md). However, this guide focuses on streaming with React 18. -## Enable Streaming SSR +## Using Streaming Server-Rendering -Enabling streaming SSR means React renders your components into streams and the client continues receiving updates from these streams even after the initial SSR response is sent. In other words, when any suspended components resolve down the line, they are rendered on the server and streamed to the client. With this strategy, the app can start emitting HTML even before all the data is ready, improving your app's loading performance. As an added bonus, in streaming SSR mode, the client will also use selective hydration strategy to prioritize component hydration which based on user interaction. +When you use Suspense in a server-rendered page, there is no extra configuration required to use streaming SSR. When deployed, streaming can be utilized through infrastructure like [Edge Functions](https://vercel.com/edge) on Vercel (with the Edge Runtime) or with a Node.js server (with the Node.js runtime). AWS Lambda Functions do not currently support streaming responses. -To enable streaming SSR, set the experimental option `runtime` to either `'nodejs'` or `'edge'`: +All SSR pages have the ability to render components into streams and the client continues receiving updates from these streams even after the initial SSR response is sent. When any suspended components resolve down the line, they are rendered on the server and streamed to the client. This means applications can start emitting HTML even _before_ all the data is ready, improving your app's loading performance. -```jsx -// next.config.js -module.exports = { - experimental: { - runtime: 'nodejs', - }, -} -``` +As an added bonus, in streaming SSR mode the client will also use selective hydration to prioritize component hydration based on user interactions, further improving performance. -This option determines the environment in which streaming SSR will be happening. When setting to `'edge'`, the server will be running entirely in the [Edge Runtime](https://nextjs.org/docs/api-reference/edge-runtime). +For non-SSR pages, all Suspense boundaries will still be [statically optimized](/docs/advanced-features/automatic-static-optimization.md). ## Streaming Features diff --git a/docs/advanced-features/react-18/switchable-runtime.md b/docs/advanced-features/react-18/switchable-runtime.md new file mode 100644 index 0000000000000..458ad86664e74 --- /dev/null +++ b/docs/advanced-features/react-18/switchable-runtime.md @@ -0,0 +1,36 @@ +# Switchable Runtime (Alpha) + +By default, Next.js uses Node.js as the runtime for page rendering, including pre-rendering and server-side rendering. + +If you have [React 18](/docs/advanced-features/react-18/overview) installed, there is a new experimental feature that lets you switch the page runtime between Node.js and the [Edge Runtime](/docs/api-reference/edge-runtime). Changing the runtime affects [SSR streaming](/docs/advanced-features/react-18/streaming) and [Server Components](/docs/advanced-features/react-18/server-components) features, as well. + +## Global Runtime Option + +You can set the experimental option `runtime` to either `'nodejs'` or `'edge'` in your `next.config.js` file: + +```jsx +// next.config.js +module.exports = { + experimental: { + runtime: 'nodejs', + }, +} +``` + +This option determines which runtime should be used as the default rendering runtime for all pages. + +## Page Runtime Option + +On each page, you can optionally export a `runtime` config set to either `'nodejs'` or `'edge'`: + +```jsx +export const config = { + runtime: 'nodejs', +} +``` + +When both the per-page runtime and global runtime are set, the per-page runtime overrides the global runtime. If the per-page runtime is _not_ set, the global runtime option will be used. + +You can refer to the [Switchable Next.js Runtime RFC](https://github.com/vercel/next.js/discussions/34179) for more information. + +**Note:** The page runtime option is not supported in [API Routes](/docs/api-routes/introduction.md) currently. diff --git a/docs/api-reference/data-fetching/get-static-props.md b/docs/api-reference/data-fetching/get-static-props.md index 66d734a6f9fc5..ec547fe0e30db 100644 --- a/docs/api-reference/data-fetching/get-static-props.md +++ b/docs/api-reference/data-fetching/get-static-props.md @@ -83,6 +83,12 @@ export async function getStaticProps() { Learn more about [Incremental Static Regeneration](/docs/basic-features/data-fetching/incremental-static-regeneration.md) +The cache status of a page leveraging ISR can be determined by reading the value of the `x-nextjs-cache` response header. The possible values are the following: + +- `MISS` - the path is not in the cache (occurs at most once, on the first visit) +- `STALE` - the path is in the cache but exceeded the revalidate time so it will be updated in the background +- `HIT` - the path is in the cache and has not exceeded the revalidate time + ### `notFound` The `notFound` boolean allows the page to return a `404` status and [404 Page](/docs/advanced-features/custom-error-page.md#404-page). With `notFound: true`, the page will return a `404` even if there was a successfully generated page before. This is meant to support use cases like user-generated content getting removed by its author. Note, `notFound` follows the same `revalidate` behavior [described here](/docs/api-reference/data-fetching/get-static-props.md#revalidate) diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index 2e75c38e14e72..9f6db8cd92824 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -422,6 +422,12 @@ The following describes the caching algorithm for the default [loader](#loader). Images are optimized dynamically upon request and stored in the `/cache/images` directory. The optimized image file will be served for subsequent requests until the expiration is reached. When a request is made that matches a cached but expired file, the expired image is served stale immediately. Then the image is optimized again in the background (also called revalidation) and saved to the cache with the new expiration date. +The cache status of an image can be determined by reading the value of the `x-nextjs-cache` response header. The possible values are the following: + +- `MISS` - the path is not in the cache (occurs at most once, on the first visit) +- `STALE` - the path is in the cache but exceeded the revalidate time so it will be updated in the background +- `HIT` - the path is in the cache and has not exceeded the revalidate time + The expiration (or rather Max Age) is defined by either the [`minimumCacheTTL`](#minimum-cache-ttl) configuration or the upstream server's `Cache-Control` header, whichever is larger. Specifically, the `max-age` value of the `Cache-Control` header is used. If both `s-maxage` and `max-age` are found, then `s-maxage` is preferred. - You can configure [`minimumCacheTTL`](#minimum-cache-ttl) to increase the cache duration when the upstream image does not include `Cache-Control` header or the value is very low. diff --git a/docs/api-reference/next/server.md b/docs/api-reference/next/server.md index d9139858801cd..601be07c0d4db 100644 --- a/docs/api-reference/next/server.md +++ b/docs/api-reference/next/server.md @@ -164,6 +164,10 @@ The introduction of the `307` status code means that the request method is prese The `redirect()` method uses a `307` by default, instead of a `302` temporary redirect, meaning your requests will _always_ be preserved as `POST` requests. +If you want to cause a `GET` response to a `POST` request, use `303`. + +[Learn more](https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections) about HTTP Redirects. + ### How do I access Environment Variables? `process.env` can be used to access [Environment Variables](/docs/basic-features/environment-variables.md) from Middleware. These are evaluated at build time, so only environment variables _actually_ used will be included. diff --git a/docs/manifest.json b/docs/manifest.json index d2f8c5cca5921..ce6622372baf3 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -311,6 +311,10 @@ { "title": "React Server Components", "path": "/docs/advanced-features/react-18/server-components.md" + }, + { + "title": "Switchable Runtime", + "path": "/docs/advanced-features/react-18/switchable-runtime.md" } ] } diff --git a/packages/next/build/webpack/config/blocks/css/plugins.ts b/packages/next/build/webpack/config/blocks/css/plugins.ts index 890c31edbcd24..9d24c4aabbd37 100644 --- a/packages/next/build/webpack/config/blocks/css/plugins.ts +++ b/packages/next/build/webpack/config/blocks/css/plugins.ts @@ -9,7 +9,7 @@ type CssPluginCollection = | CssPluginCollection_Array | CssPluginCollection_Object -type CssPluginShape = [string, object | boolean] +type CssPluginShape = [string, object | boolean | string] const genericErrorText = 'Malformed PostCSS Configuration' @@ -62,7 +62,7 @@ const createLazyPostCssPlugin = ( async function loadPlugin( dir: string, pluginName: string, - options: boolean | object + options: boolean | object | string ): Promise { if (options === false || isIgnoredPlugin(pluginName)) { return false @@ -79,8 +79,7 @@ async function loadPlugin( } else if (options === true) { return createLazyPostCssPlugin(() => require(pluginPath)) } else { - const keys = Object.keys(options) - if (keys.length === 0) { + if (typeof options === 'object' && Object.keys(options).length === 0) { return createLazyPostCssPlugin(() => require(pluginPath)) } return createLazyPostCssPlugin(() => require(pluginPath)(options)) @@ -187,7 +186,9 @@ export async function getPostCssPlugins( const pluginConfig = plugin[1] if ( typeof pluginName === 'string' && - (typeof pluginConfig === 'boolean' || typeof pluginConfig === 'object') + (typeof pluginConfig === 'boolean' || + typeof pluginConfig === 'object' || + typeof pluginConfig === 'string') ) { parsed.push([pluginName, pluginConfig]) } else { diff --git a/packages/next/server/node-web-streams-helper.ts b/packages/next/server/node-web-streams-helper.ts index 9ef5e747d6813..9512d9cbffa65 100644 --- a/packages/next/server/node-web-streams-helper.ts +++ b/packages/next/server/node-web-streams-helper.ts @@ -118,11 +118,11 @@ export function createBufferedTransformStream(): TransformStream< } export function createFlushEffectStream( - handleFlushEffect: () => Promise + handleFlushEffect: () => string ): TransformStream { return new TransformStream({ - async transform(chunk, controller) { - const flushedChunk = encodeText(await handleFlushEffect()) + transform(chunk, controller) { + const flushedChunk = encodeText(handleFlushEffect()) controller.enqueue(flushedChunk) controller.enqueue(chunk) @@ -154,7 +154,7 @@ export async function continueFromInitialStream({ suffix?: string dataStream?: ReadableStream generateStaticHTML: boolean - flushEffectHandler?: () => Promise + flushEffectHandler?: () => string renderStream: ReadableStream & { allReady?: Promise } @@ -193,7 +193,7 @@ export async function renderToStream({ suffix?: string dataStream?: ReadableStream generateStaticHTML: boolean - flushEffectHandler?: () => Promise + flushEffectHandler?: () => string }): Promise> { const renderStream = await renderToInitialStream({ ReactDOMServer, element }) return continueFromInitialStream({ diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 6b2b8648257a9..3f611c42405fc 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -1430,25 +1430,20 @@ export async function renderToHTML( const bodyResult = async (suffix: string) => { // this must be called inside bodyResult so appWrappers is - // up to date when getWrappedApp is called + // up to date when `wrapApp` is called - const flushEffectHandler = async () => { + const flushEffectHandler = (): string => { const allFlushEffects = [ styledJsxFlushEffect, ...(flushEffects || []), ] - const flushEffectStream = await renderToStream({ - ReactDOMServer, - element: ( - <> - {allFlushEffects.map((flushEffect, i) => ( - {flushEffect()} - ))} - - ), - generateStaticHTML: true, - }) - const flushed = await streamToString(flushEffectStream) + const flushed = ReactDOMServer.renderToString( + <> + {allFlushEffects.map((flushEffect, i) => ( + {flushEffect()} + ))} + + ) return flushed } diff --git a/test/integration/import-assertion/test/index.test.js b/test/integration/import-assertion/test/index.test.js index 9bcfe028754ce..b080b485ddb5b 100644 --- a/test/integration/import-assertion/test/index.test.js +++ b/test/integration/import-assertion/test/index.test.js @@ -1,37 +1,8 @@ import { join } from 'path' -import { - nextBuild, - nextStart, - launchApp, - killApp, - findPort, - renderViaHTTP, -} from 'next-test-utils' +import { renderViaHTTP, runDevSuite, runProdSuite } from 'next-test-utils' const appDir = join(__dirname, '../') -function runSuite(suiteName, env, runTests) { - const context = { appDir } - describe(`${suiteName} ${env}`, () => { - if (env === 'prod') { - beforeAll(async () => { - context.appPort = await findPort() - await nextBuild(context.appDir) - context.server = await nextStart(context.appDir, context.appPort) - }) - } - if (env === 'dev') { - beforeAll(async () => { - context.appPort = await findPort() - context.server = await launchApp(context.appDir, context.appPort) - }) - } - afterAll(async () => await killApp(context.server)) - - runTests(context, env) - }) -} - function basic(context) { it('should handle json assertions', async () => { const esHtml = await renderViaHTTP(context.appPort, '/es') @@ -41,5 +12,5 @@ function basic(context) { }) } -runSuite('import-assertion', 'dev', basic) -runSuite('import-assertion', 'prod', basic) +runDevSuite('import-assertion', appDir, { runTests: basic }) +runProdSuite('import-assertion', appDir, { runTests: basic }) diff --git a/test/integration/react-18-invalid-config/index.test.js b/test/integration/react-18-invalid-config/index.test.js index 9cde36de3148d..dc2f1e2c930cc 100644 --- a/test/integration/react-18-invalid-config/index.test.js +++ b/test/integration/react-18-invalid-config/index.test.js @@ -2,19 +2,47 @@ import fs from 'fs-extra' import { join } from 'path' -import { File, nextBuild } from 'next-test-utils' +import { + File, + nextBuild, + runDevSuite, + runProdSuite, + fetchViaHTTP, +} from 'next-test-utils' const appDir = __dirname const nodeArgs = ['-r', join(appDir, '../../lib/react-17-require-hook.js')] -const nextConfig = new File(join(appDir, 'next.config.js')) const reactDomPackagePah = join(appDir, 'node_modules/react-dom') +const nextConfig = new File(join(appDir, 'next.config.js')) +const documentPage = new File(join(appDir, 'pages/_document.js')) +const indexPage = join(appDir, 'pages/index.js') +const indexServerPage = join(appDir, 'pages/index.server.js') + +const documentWithGip = ` +import { Html, Head, Main, NextScript } from 'next/document' + +export default function Document() { + return ( + + + +
+ + + + ) +} + +Document.getInitialProps = (ctx) => { + return ctx.defaultGetInitialProps(ctx) +} +` -function writeNextConfig(config) { +function writeNextConfig(config, reactVersion = 17) { const content = ` const path = require('path') - module.exports = require(path.join(__dirname, '../../lib/with-react-17.js'))({ experimental: ${JSON.stringify( - config - )} }) + const withReact = ${reactVersion} === 18 ? v => v : require(path.join(__dirname, '../../lib/with-react-17.js')) + module.exports = withReact({ experimental: ${JSON.stringify(config)} }) ` nextConfig.write(content) } @@ -65,3 +93,36 @@ describe('React 17 with React 18 config', () => { expect(code).toBe(1) }) }) + +const documentSuite = { + runTests: (context, env) => { + if (env === 'dev') { + it('should error when custom _document has getInitialProps method', async () => { + const res = await fetchViaHTTP(context.appPort, '/') + expect(res.status).toBe(500) + }) + } else { + it('should failed building', async () => { + expect(context.code).toBe(1) + }) + } + }, + beforeAll: async () => { + writeNextConfig( + { + serverComponents: true, + }, + 18 + ) + documentPage.write(documentWithGip) + await fs.rename(indexPage, indexServerPage) + }, + afterAll: async () => { + documentPage.delete() + nextConfig.restore() + await fs.rename(indexServerPage, indexPage) + }, +} + +runDevSuite('Invalid custom document with gip', appDir, documentSuite) +runProdSuite('Invalid custom document with gip', appDir, documentSuite) diff --git a/test/integration/react-18/test/index.test.js b/test/integration/react-18/test/index.test.js index 7155004b6d191..43e2287ac11b2 100644 --- a/test/integration/react-18/test/index.test.js +++ b/test/integration/react-18/test/index.test.js @@ -7,27 +7,23 @@ import { findPort, killApp, launchApp, - nextBuild, - nextStart, renderViaHTTP, hasRedbox, getRedboxHeader, + runDevSuite, + runProdSuite, } from 'next-test-utils' import concurrent from './concurrent' import basics from './basics' import strictMode from './strict-mode' import webdriver from 'next-webdriver' -// overrides react and react-dom to v18 -const nodeArgs = [] const appDir = join(__dirname, '../app') const nextConfig = new File(join(appDir, 'next.config.js')) const invalidPage = new File(join(appDir, 'pages/invalid.js')) describe('Basics', () => { - runTests('default setting with react 18', (context, env) => - basics(context, env) - ) + runTests('default setting with react 18', basics) }) // React 18 with Strict Mode enabled might cause double invocation of lifecycle methods. @@ -37,9 +33,7 @@ describe('Strict mode - dev', () => { beforeAll(async () => { nextConfig.replace('// reactStrictMode: true,', 'reactStrictMode: true,') context.appPort = await findPort() - context.server = await launchApp(context.appDir, context.appPort, { - nodeArgs, - }) + context.server = await launchApp(context.appDir, context.appPort) }) afterAll(() => { @@ -84,42 +78,11 @@ function runTestsAgainstRuntime(runtime) { ) } -function runTest(env, name, fn, options) { - const context = { appDir } - describe(`${name} (${env})`, () => { - beforeAll(async () => { - context.appPort = await findPort() - context.stderr = '' - options?.beforeAll(env) - if (env === 'dev') { - context.server = await launchApp(context.appDir, context.appPort, { - nodeArgs, - onStderr(msg) { - context.stderr += msg - }, - }) - } else { - await nextBuild(context.appDir, [], { nodeArgs }) - context.server = await nextStart(context.appDir, context.appPort, { - nodeArgs, - onStderr(msg) { - context.stderr += msg - }, - }) - } - }) - afterAll(async () => { - options?.afterAll(env) - await killApp(context.server) - }) - fn(context, env) - }) -} - runTestsAgainstRuntime('edge') runTestsAgainstRuntime('nodejs') -function runTests(name, fn, options) { - runTest('dev', name, fn, options) - runTest('prod', name, fn, options) +function runTests(name, fn, opts) { + const suiteOptions = { ...opts, runTests: fn } + runDevSuite(name, appDir, suiteOptions) + runProdSuite(name, appDir, suiteOptions) } diff --git a/test/integration/react-streaming-and-server-components/test/functions.js b/test/integration/react-streaming-and-server-components/test/functions.js index 965190486c2fa..31f98fcca124f 100644 --- a/test/integration/react-streaming-and-server-components/test/functions.js +++ b/test/integration/react-streaming-and-server-components/test/functions.js @@ -10,20 +10,23 @@ import { nextBuild } from './utils' export default function (context) { it('should not generate functions manifest when filesystem API is not enabled', async () => { // Make sure there is no existing functions manifest (caused by failed tests etc). - await fs.remove(join(context.appDir, '.next')) + const distDir = join(context.appDir, '.next') + await fs.remove(distDir) await nextBuild(context.appDir) const functionsManifestPath = join( - context.distDir, + distDir, 'server', 'functions-manifest.json' ) expect(fs.existsSync(functionsManifestPath)).toBe(false) await fs.remove(join(context.appDir, '.next')) }) + it('should contain rsc paths in functions manifest', async () => { + const distDir = join(context.appDir, '.next') await nextBuild(context.appDir, { env: { ENABLE_FILE_SYSTEM_API: '1' } }) const functionsManifestPath = join( - context.distDir, + distDir, 'server', 'functions-manifest.json' ) diff --git a/test/integration/react-streaming-and-server-components/test/index.test.js b/test/integration/react-streaming-and-server-components/test/index.test.js index 6ac2a84e5cb42..4e526e8ede82d 100644 --- a/test/integration/react-streaming-and-server-components/test/index.test.js +++ b/test/integration/react-streaming-and-server-components/test/index.test.js @@ -3,16 +3,17 @@ import { join } from 'path' import fs from 'fs-extra' -import { fetchViaHTTP, findPort, killApp, renderViaHTTP } from 'next-test-utils' - import { + fetchViaHTTP, + renderViaHTTP, nextBuild, - nextStart, - nextDev, + runDevSuite, + runProdSuite, +} from 'next-test-utils' + +import { appDir, nativeModuleTestAppDir, - distDir, - documentPage, appPage, appServerPage, error500Page, @@ -25,26 +26,6 @@ import streaming from './streaming' import basic from './basic' import runtime from './runtime' -const documentWithGip = ` -import { Html, Head, Main, NextScript } from 'next/document' - -export default function Document() { - return ( - - - -
- - - - ) -} - -Document.getInitialProps = (ctx) => { - return ctx.defaultGetInitialProps(ctx) -} -` - const rscAppPage = ` import Container from '../components/container.server' export default function App({children}) { @@ -72,7 +53,9 @@ describe('Edge runtime - errors', () => { it('should warn user that native node APIs are not supported', async () => { const fsImportedErrorMessage = 'Native Node.js APIs are not supported in the Edge Runtime. Found `dns` imported.' - const { stderr } = await nextBuild(nativeModuleTestAppDir) + const { stderr } = await nextBuild(nativeModuleTestAppDir, [], { + stderr: true, + }) expect(stderr).toContain(fsImportedErrorMessage) }) }) @@ -80,6 +63,7 @@ describe('Edge runtime - errors', () => { const edgeRuntimeBasicSuite = { runTests: (context, env) => { const options = { runtime: 'edge', env } + const distDir = join(appDir, '.next') basic(context, options) streaming(context, options) rsc(context, options) @@ -158,6 +142,7 @@ const edgeRuntimeBasicSuite = { const nodejsRuntimeBasicSuite = { runTests: (context, env) => { const options = { runtime: 'nodejs', env } + const distDir = join(appDir, '.next') basic(context, options) streaming(context, options) rsc(context, options) @@ -217,61 +202,13 @@ const cssSuite = { afterAll: () => appPage.delete(), } -const documentSuite = { - runTests: (context, env) => { - if (env === 'dev') { - it('should error when custom _document has getInitialProps method', async () => { - const res = await fetchViaHTTP(context.appPort, '/') - expect(res.status).toBe(500) - }) - } else { - it('should failed building', async () => { - expect(context.code).toBe(1) - }) - } - }, - beforeAll: () => documentPage.write(documentWithGip), - afterAll: () => documentPage.delete(), -} - -runSuite('Node.js runtime', 'dev', nodejsRuntimeBasicSuite) -runSuite('Node.js runtime', 'prod', nodejsRuntimeBasicSuite) -runSuite('Edge runtime', 'dev', edgeRuntimeBasicSuite) -runSuite('Edge runtime', 'prod', edgeRuntimeBasicSuite) +runDevSuite('Node.js runtime', appDir, nodejsRuntimeBasicSuite) +runProdSuite('Node.js runtime', appDir, nodejsRuntimeBasicSuite) +runDevSuite('Edge runtime', appDir, edgeRuntimeBasicSuite) +runProdSuite('Edge runtime', appDir, edgeRuntimeBasicSuite) -runSuite('Custom App', 'dev', customAppPageSuite) -runSuite('Custom App', 'prod', customAppPageSuite) +runDevSuite('Custom App', appDir, customAppPageSuite) +runProdSuite('Custom App', appDir, customAppPageSuite) -runSuite('CSS', 'dev', cssSuite) -runSuite('CSS', 'prod', cssSuite) - -runSuite('Custom Document', 'dev', documentSuite) -runSuite('Custom Document', 'prod', documentSuite) - -function runSuite(suiteName, env, options) { - const context = { appDir, distDir } - describe(`${suiteName} ${env}`, () => { - beforeAll(async () => { - options.beforeAll?.() - if (env === 'prod') { - context.appPort = await findPort() - const { stdout, stderr, code } = await nextBuild(context.appDir) - context.stdout = stdout - context.stderr = stderr - context.code = code - context.server = await nextStart(context.appDir, context.appPort) - } - if (env === 'dev') { - context.appPort = await findPort() - context.server = await nextDev(context.appDir, context.appPort) - } - }) - afterAll(async () => { - options.afterAll?.() - if (context.server) { - await killApp(context.server) - } - }) - options.runTests(context, env) - }) -} +runDevSuite('CSS', appDir, cssSuite) +runProdSuite('CSS', appDir, cssSuite) diff --git a/test/integration/react-streaming-and-server-components/test/rsc.js b/test/integration/react-streaming-and-server-components/test/rsc.js index a7cc18df86b7f..cb5e0537d1510 100644 --- a/test/integration/react-streaming-and-server-components/test/rsc.js +++ b/test/integration/react-streaming-and-server-components/test/rsc.js @@ -3,9 +3,10 @@ import webdriver from 'next-webdriver' import { renderViaHTTP, check } from 'next-test-utils' import { join } from 'path' import fs from 'fs-extra' -import { distDir, getNodeBySelector } from './utils' +import { getNodeBySelector } from './utils' export default function (context, { runtime, env }) { + const distDir = join(context.appDir, '.next') it('should render server components correctly', async () => { const homeHTML = await renderViaHTTP(context.appPort, '/', null, { headers: { diff --git a/test/integration/react-streaming-and-server-components/test/runtime.js b/test/integration/react-streaming-and-server-components/test/runtime.js index b166fdb4ded35..236510214b625 100644 --- a/test/integration/react-streaming-and-server-components/test/runtime.js +++ b/test/integration/react-streaming-and-server-components/test/runtime.js @@ -2,9 +2,9 @@ import { renderViaHTTP } from 'next-test-utils' import { join } from 'path' import fs from 'fs-extra' -import { distDir } from './utils' - export default async function runtime(context, { runtime, env }) { + const distDir = join(context.appDir, '.next') + if (runtime === 'edge') { it('should support per-page runtime configuration', async () => { const html1 = await renderViaHTTP(context.appPort, '/runtime') diff --git a/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js b/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js index d261108040492..c3a2c276769df 100644 --- a/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js +++ b/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js @@ -5,10 +5,12 @@ import { check, findPort, killApp, + launchApp, + nextBuild, + nextStart, renderViaHTTP, waitFor, } from 'next-test-utils' -import { nextBuild, nextDev, nextStart } from './utils' const appDir = join(__dirname, '../switchable-runtime') @@ -48,7 +50,10 @@ describe('Switchable runtime (prod)', () => { beforeAll(async () => { context.appPort = await findPort() - const { stdout, stderr } = await nextBuild(context.appDir) + const { stdout, stderr } = await nextBuild(context.appDir, [], { + stderr: true, + stdout: true, + }) context.stdout = stdout context.stderr = stderr context.server = await nextStart(context.appDir, context.appPort) @@ -252,7 +257,7 @@ describe('Switchable runtime (dev)', () => { beforeAll(async () => { context.appPort = await findPort() - context.server = await nextDev(context.appDir, context.appPort) + context.server = await launchApp(context.appDir, context.appPort) }) afterAll(async () => { await killApp(context.server) diff --git a/test/integration/react-streaming-and-server-components/test/utils.js b/test/integration/react-streaming-and-server-components/test/utils.js index 65bae002ce679..d1f19ed8e80fd 100644 --- a/test/integration/react-streaming-and-server-components/test/utils.js +++ b/test/integration/react-streaming-and-server-components/test/utils.js @@ -1,51 +1,17 @@ import { join } from 'path' -import { - File, - launchApp, - nextBuild as _nextBuild, - nextStart as _nextStart, -} from 'next-test-utils' +import { File } from 'next-test-utils' import cheerio from 'cheerio' -const nodeArgs = [] - export const appDir = join(__dirname, '../app') export const nativeModuleTestAppDir = join( __dirname, '../unsupported-native-module' ) -export const distDir = join(__dirname, '../app/.next') -export const documentPage = new File(join(appDir, 'pages/_document.jsx')) export const appPage = new File(join(appDir, 'pages/_app.js')) export const appServerPage = new File(join(appDir, 'pages/_app.server.js')) export const error500Page = new File(join(appDir, 'pages/500.js')) export const nextConfig = new File(join(appDir, 'next.config.js')) -export async function nextBuild(dir, options) { - return await _nextBuild(dir, [], { - ...options, - stdout: true, - stderr: true, - nodeArgs, - }) -} - -export async function nextStart(dir, port) { - return await _nextStart(dir, port, { - stdout: true, - stderr: true, - nodeArgs, - }) -} - -export async function nextDev(dir, port) { - return await launchApp(dir, port, { - stdout: true, - stderr: true, - nodeArgs, - }) -} - export function getNodeBySelector(html, selector) { const $ = cheerio.load(html) return $(selector) diff --git a/test/lib/next-test-utils.js b/test/lib/next-test-utils.js index c90aaa433e25d..a33c5970a128b 100644 --- a/test/lib/next-test-utils.js +++ b/test/lib/next-test-utils.js @@ -722,3 +722,67 @@ export function readNextBuildServerPageFile(appDir, page) { const pageFile = getPageFileFromPagesManifest(appDir, page) return readFileSync(path.join(appDir, '.next', 'server', pageFile), 'utf8') } + +/** + * + * @param {string} suiteName + * @param {{env: 'prod' | 'dev', appDir: string}} context + * @param {{beforeAll?: Function; afterAll?: Function; runTests: Function}} options + */ +function runSuite(suiteName, context, options) { + const { appDir, env } = context + describe(`${suiteName} ${env}`, () => { + beforeAll(async () => { + options.beforeAll?.(env) + context.stderr = '' + const onStderr = (msg) => { + context.stderr += msg + } + if (env === 'prod') { + context.appPort = await findPort() + const { stdout, stderr, code } = await nextBuild(appDir, [], { + stderr: true, + stdout: true, + }) + context.stdout = stdout + context.stderr = stderr + context.code = code + context.server = await nextStart(context.appDir, context.appPort, { + onStderr, + }) + } else if (env === 'dev') { + context.appPort = await findPort() + context.server = await launchApp(context.appDir, context.appPort, { + onStderr, + }) + } + }) + afterAll(async () => { + options.afterAll?.(env) + if (context.server) { + await killApp(context.server) + } + }) + options.runTests(context, env) + }) +} + +/** + * + * @param {string} suiteName + * @param {string} appDir + * @param {{beforeAll?: Function; afterAll?: Function; runTests: Function}} options + */ +export function runDevSuite(suiteName, appDir, options) { + return runSuite(suiteName, { appDir, env: 'dev' }, options) +} + +/** + * + * @param {string} suiteName + * @param {string} appDir + * @param {{beforeAll?: Function; afterAll?: Function; runTests: Function}} options + */ +export function runProdSuite(suiteName, appDir, options) { + return runSuite(suiteName, { appDir, env: 'prod' }, options) +} diff --git a/test/production/postcss-plugin-config-as-string/index.test.ts b/test/production/postcss-plugin-config-as-string/index.test.ts new file mode 100644 index 0000000000000..e8f75b5f10352 --- /dev/null +++ b/test/production/postcss-plugin-config-as-string/index.test.ts @@ -0,0 +1,54 @@ +import { createNext } from 'e2e-utils' +import { renderViaHTTP } from 'next-test-utils' +import { NextInstance } from 'test/lib/next-modes/base' + +describe('PostCSS plugin config as string', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: { + 'pages/index.js': ` + export default function Page() { + return

hello world

+ } + `, + 'global.css': ` + @import "tailwindcss/base"; + @import "tailwindcss/components"; + @import "tailwindcss/utilities"; + `, + 'pages/_app.js': ` + import "../global.css" + + export default function MyApp({ Component, pageProps }) { + return + } + `, + 'postcss.config.js': ` + module.exports = { + plugins: { + 'tailwindcss/nesting': 'postcss-nesting', + tailwindcss: {}, + }, + } + `, + 'tailwind.config.js': ` + module.exports = { + content: ['./pages/**/*'], + } + `, + }, + dependencies: { + 'postcss-nesting': '10.1.3', + tailwindcss: '3.0.23', + }, + }) + }) + afterAll(() => next.destroy()) + + it('should work', async () => { + const html = await renderViaHTTP(next.url, '/') + expect(html).toContain('hello world') + }) +})