Skip to content

Commit

Permalink
Merge branch 'canary' into fix-custom-loader-prop-remove-config
Browse files Browse the repository at this point in the history
  • Loading branch information
styfle authored Apr 8, 2022
2 parents 9ecf9e1 + a0924fc commit cf72a3f
Show file tree
Hide file tree
Showing 21 changed files with 328 additions and 252 deletions.
12 changes: 9 additions & 3 deletions docs/advanced-features/react-18/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
23 changes: 8 additions & 15 deletions docs/advanced-features/react-18/streaming.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
36 changes: 36 additions & 0 deletions docs/advanced-features/react-18/switchable-runtime.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions docs/api-reference/data-fetching/get-static-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions docs/api-reference/next/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<distDir>/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.
Expand Down
4 changes: 4 additions & 0 deletions docs/api-reference/next/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
Expand Down
11 changes: 6 additions & 5 deletions packages/next/build/webpack/config/blocks/css/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -62,7 +62,7 @@ const createLazyPostCssPlugin = (
async function loadPlugin(
dir: string,
pluginName: string,
options: boolean | object
options: boolean | object | string
): Promise<import('postcss').AcceptedPlugin | false> {
if (options === false || isIgnoredPlugin(pluginName)) {
return false
Expand All @@ -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))
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions packages/next/server/node-web-streams-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ export function createBufferedTransformStream(): TransformStream<
}

export function createFlushEffectStream(
handleFlushEffect: () => Promise<string>
handleFlushEffect: () => string
): TransformStream<Uint8Array, Uint8Array> {
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)
Expand Down Expand Up @@ -154,7 +154,7 @@ export async function continueFromInitialStream({
suffix?: string
dataStream?: ReadableStream<Uint8Array>
generateStaticHTML: boolean
flushEffectHandler?: () => Promise<string>
flushEffectHandler?: () => string
renderStream: ReadableStream<Uint8Array> & {
allReady?: Promise<void>
}
Expand Down Expand Up @@ -193,7 +193,7 @@ export async function renderToStream({
suffix?: string
dataStream?: ReadableStream<Uint8Array>
generateStaticHTML: boolean
flushEffectHandler?: () => Promise<string>
flushEffectHandler?: () => string
}): Promise<ReadableStream<Uint8Array>> {
const renderStream = await renderToInitialStream({ ReactDOMServer, element })
return continueFromInitialStream({
Expand Down
23 changes: 9 additions & 14 deletions packages/next/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => (
<React.Fragment key={i}>{flushEffect()}</React.Fragment>
))}
</>
),
generateStaticHTML: true,
})
const flushed = await streamToString(flushEffectStream)
const flushed = ReactDOMServer.renderToString(
<>
{allFlushEffects.map((flushEffect, i) => (
<React.Fragment key={i}>{flushEffect()}</React.Fragment>
))}
</>
)
return flushed
}

Expand Down
35 changes: 3 additions & 32 deletions test/integration/import-assertion/test/index.test.js
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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 })
73 changes: 67 additions & 6 deletions test/integration/react-18-invalid-config/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
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)
}
Expand Down Expand Up @@ -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)
Loading

0 comments on commit cf72a3f

Please sign in to comment.