diff --git a/docs/advanced-features/react-18/streaming.md b/docs/advanced-features/react-18/streaming.md
index 544759b3daa66..984264190dc56 100644
--- a/docs/advanced-features/react-18/streaming.md
+++ b/docs/advanced-features/react-18/streaming.md
@@ -1,7 +1,7 @@
# Streaming SSR (Alpha)
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.
+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.
## Enable Streaming SSR
diff --git a/docs/api-reference/next.config.js/ignoring-typescript-errors.md b/docs/api-reference/next.config.js/ignoring-typescript-errors.md
index 57335b30cf8ce..a26cbc26cbc8f 100644
--- a/docs/api-reference/next.config.js/ignoring-typescript-errors.md
+++ b/docs/api-reference/next.config.js/ignoring-typescript-errors.md
@@ -8,7 +8,7 @@ Next.js fails your **production build** (`next build`) when TypeScript errors ar
If you'd like Next.js to dangerously produce production code even when your application has errors, you can disable the built-in type checking step.
-> Be sure you are running type checks as part of your build or deploy process, otherwise this can be very dangerous.
+If disabled, be sure you are running type checks as part of your build or deploy process, otherwise this can be very dangerous.
Open `next.config.js` and enable the `ignoreBuildErrors` option in the `typescript` config:
diff --git a/docs/basic-features/typescript.md b/docs/basic-features/typescript.md
index db23d29dad073..9869dc850e431 100644
--- a/docs/basic-features/typescript.md
+++ b/docs/basic-features/typescript.md
@@ -5,14 +5,19 @@ description: Next.js supports TypeScript by default and has built-in types for p
# TypeScript
- Examples
-
+ Version History
+
+| Version | Changes |
+| --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
+| `v12.0.0` | [SWC](https://nextjs.org/docs/advanced-features/compiler) is now used by default to compile TypeScript and TSX for faster builds. |
+| `v10.2.1` | [Incremental type checking](https://www.typescriptlang.org/tsconfig#incremental) support added when enabled in your `tsconfig.json`. |
+
-Next.js provides an integrated [TypeScript](https://www.typescriptlang.org/)
-experience out of the box, similar to an IDE.
+Next.js provides an integrated [TypeScript](https://www.typescriptlang.org/) experience, including zero-configuration set up and built-in types for Pages, APIs, and more.
+
+- [Clone and deploy the TypeScript starter](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-typescript&project-name=with-typescript&repository-name=with-typescript)
+- [View an example application](https://github.com/vercel/next.js/tree/canary/examples/with-typescript)
## `create-next-app` support
@@ -120,26 +125,11 @@ export default (req: NextApiRequest, res: NextApiResponse) => {
If you have a [custom `App`](/docs/advanced-features/custom-app.md), you can use the built-in type `AppProps` and change file name to `./pages/_app.tsx` like so:
```ts
-// import App from "next/app";
-import type { AppProps /*, AppContext */ } from 'next/app'
+import type { AppProps } from 'next/app'
-function MyApp({ Component, pageProps }: AppProps) {
+export default function MyApp({ Component, pageProps }: AppProps) {
return
}
-
-// Only uncomment this method if you have blocking data requirements for
-// every single page in your application. This disables the ability to
-// perform automatic static optimization, causing every page in your app to
-// be server-side rendered.
-//
-// MyApp.getInitialProps = async (appContext: AppContext) => {
-// // calls page's `getInitialProps` and fills `appProps.pageProps`
-// const appProps = await App.getInitialProps(appContext);
-
-// return { ...appProps }
-// }
-
-export default MyApp
```
## Path aliases and baseUrl
@@ -170,3 +160,25 @@ module.exports = nextConfig
Since `v10.2.1` Next.js supports [incremental type checking](https://www.typescriptlang.org/tsconfig#incremental) when enabled in your `tsconfig.json`, this can help speed up type checking in larger applications.
It is highly recommended to be on at least `v4.3.2` of TypeScript to experience the [best performance](https://devblogs.microsoft.com/typescript/announcing-typescript-4-3/#lazier-incremental) when leveraging this feature.
+
+## Ignoring TypeScript Errors
+
+Next.js fails your **production build** (`next build`) when TypeScript errors are present in your project.
+
+If you'd like Next.js to dangerously produce production code even when your application has errors, you can disable the built-in type checking step.
+
+If disabled, be sure you are running type checks as part of your build or deploy process, otherwise this can be very dangerous.
+
+Open `next.config.js` and enable the `ignoreBuildErrors` option in the `typescript` config:
+
+```js
+module.exports = {
+ typescript: {
+ // !! WARN !!
+ // Dangerously allow production builds to successfully complete even if
+ // your project has type errors.
+ // !! WARN !!
+ ignoreBuildErrors: true,
+ },
+}
+```
diff --git a/packages/next/bin/next.ts b/packages/next/bin/next.ts
index bba1a9c9a0099..dead0052077ff 100755
--- a/packages/next/bin/next.ts
+++ b/packages/next/bin/next.ts
@@ -15,7 +15,7 @@ import { NON_STANDARD_NODE_ENV } from '../lib/constants'
const defaultCommand = 'dev'
export type cliCommand = (argv?: string[]) => void
-const commands: { [command: string]: () => Promise } = {
+export const commands: { [command: string]: () => Promise } = {
build: () => Promise.resolve(require('../cli/next-build').nextBuild),
start: () => Promise.resolve(require('../cli/next-start').nextStart),
export: () => Promise.resolve(require('../cli/next-export').nextExport),
diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts
index 4dc5b5b3e7a8a..3b280aba97378 100644
--- a/packages/next/build/webpack-config.ts
+++ b/packages/next/build/webpack-config.ts
@@ -1,7 +1,6 @@
import ReactRefreshWebpackPlugin from 'next/dist/compiled/@next/react-refresh-utils/ReactRefreshWebpackPlugin'
import chalk from 'next/dist/compiled/chalk'
import crypto from 'crypto'
-import { stringify } from 'querystring'
import { webpack } from 'next/dist/compiled/webpack/webpack'
import type { webpack5 } from 'next/dist/compiled/webpack/webpack'
import path, { join as pathJoin, relative as relativePath } from 'path'
@@ -1180,10 +1179,11 @@ export default async function getBaseWebpackConfig(
...codeCondition,
test: serverComponentsRegex,
use: {
- loader: `next-flight-server-loader?${stringify({
+ loader: 'next-flight-server-loader',
+ options: {
client: 1,
- pageExtensions: JSON.stringify(rawPageExtensions),
- })}`,
+ pageExtensions: rawPageExtensions,
+ },
},
},
]
@@ -1195,22 +1195,16 @@ export default async function getBaseWebpackConfig(
? [
{
...codeCondition,
- test: serverComponentsRegex,
use: {
- loader: `next-flight-server-loader?${stringify({
- pageExtensions: JSON.stringify(rawPageExtensions),
- })}`,
- },
- },
- {
- ...codeCondition,
- test: clientComponentsRegex,
- use: {
- loader: 'next-flight-client-loader',
+ loader: 'next-flight-server-loader',
+ options: {
+ pageExtensions: rawPageExtensions,
+ },
},
},
{
- test: /next[\\/](dist[\\/]client[\\/])?(link|image)/,
+ test: codeCondition.test,
+ resourceQuery: /__sc_client__/,
use: {
loader: 'next-flight-client-loader',
},
diff --git a/packages/next/build/webpack/loaders/next-flight-client-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-loader.ts
index 65af111e6dcac..c7cc6c4badeac 100644
--- a/packages/next/build/webpack/loaders/next-flight-client-loader.ts
+++ b/packages/next/build/webpack/loaders/next-flight-client-loader.ts
@@ -94,11 +94,8 @@ export default async function transformSource(
this: any,
source: string
): Promise {
- const { resourcePath, resourceQuery } = this
+ const { resourcePath } = this
- if (resourceQuery !== '?flight') return source
-
- let url = resourcePath
const transformedSource = source
if (typeof transformedSource !== 'string') {
throw new Error('Expected source to have been transformed to a string.')
@@ -108,7 +105,7 @@ export default async function transformSource(
await parseExportNamesInto(resourcePath, transformedSource, names)
// next.js/packages/next/.js
- if (/[\\/]next[\\/](link|image)\.js$/.test(url)) {
+ if (/[\\/]next[\\/](link|image)\.js$/.test(resourcePath)) {
names.push('default')
}
@@ -122,7 +119,7 @@ export default async function transformSource(
newSrc += 'export const ' + name + ' = '
}
newSrc += '{ $$typeof: MODULE_REFERENCE, filepath: '
- newSrc += JSON.stringify(url)
+ newSrc += JSON.stringify(resourcePath)
newSrc += ', name: '
newSrc += JSON.stringify(name)
newSrc += '};\n'
diff --git a/packages/next/build/webpack/loaders/next-flight-server-loader.ts b/packages/next/build/webpack/loaders/next-flight-server-loader.ts
index f125d562da505..2555a72795bcf 100644
--- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts
+++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts
@@ -5,17 +5,19 @@ import { parse } from '../../swc'
import { getBaseSWCOptions } from '../../swc/options'
import { getRawPageExtensions } from '../../utils'
-function isClientComponent(importSource: string, pageExtensions: string[]) {
- return new RegExp(`\\.client(\\.(${pageExtensions.join('|')}))?`).test(
- importSource
- )
-}
+const getIsClientComponent =
+ (pageExtensions: string[]) => (importSource: string) => {
+ return new RegExp(`\\.client(\\.(${pageExtensions.join('|')}))?`).test(
+ importSource
+ )
+ }
-function isServerComponent(importSource: string, pageExtensions: string[]) {
- return new RegExp(`\\.server(\\.(${pageExtensions.join('|')}))?`).test(
- importSource
- )
-}
+const getIsServerComponent =
+ (pageExtensions: string[]) => (importSource: string) => {
+ return new RegExp(`\\.server(\\.(${pageExtensions.join('|')}))?`).test(
+ importSource
+ )
+ }
function isNextComponent(importSource: string) {
return (
@@ -31,13 +33,21 @@ export function isImageImport(importSource: string) {
)
}
-async function parseImportsInfo(
- resourcePath: string,
- source: string,
- imports: Array,
- isClientCompilation: boolean,
- pageExtensions: string[]
-): Promise<{
+async function parseImportsInfo({
+ resourcePath,
+ source,
+ imports,
+ isClientCompilation,
+ isServerComponent,
+ isClientComponent,
+}: {
+ resourcePath: string
+ source: string
+ imports: Array
+ isClientCompilation: boolean
+ isServerComponent: (name: string) => boolean
+ isClientComponent: (name: string) => boolean
+}): Promise<{
source: string
defaultExportName: string
}> {
@@ -45,7 +55,6 @@ async function parseImportsInfo(
filename: resourcePath,
globalWindow: isClientCompilation,
})
-
const ast = await parse(source, { ...opts.jsc.parser, isModule: true })
const { body } = ast
const beginPos = ast.span.start
@@ -58,29 +67,49 @@ async function parseImportsInfo(
case 'ImportDeclaration': {
const importSource = node.source.value
if (!isClientCompilation) {
+ // Server compilation for .server.js.
+ if (isServerComponent(importSource)) {
+ continue
+ }
+
+ const importDeclarations = source.substring(
+ lastIndex,
+ node.source.span.start - beginPos
+ )
+
if (
!(
- isClientComponent(importSource, pageExtensions) ||
+ isClientComponent(importSource) ||
isNextComponent(importSource) ||
isImageImport(importSource)
)
) {
- continue
+ if (
+ ['react/jsx-runtime', 'react/jsx-dev-runtime'].includes(
+ importSource
+ )
+ ) {
+ continue
+ }
+
+ // A shared component. It should be handled as a server
+ // component.
+ transformedSource += importDeclarations
+ transformedSource += JSON.stringify(`${importSource}?__sc_server__`)
+ } else {
+ // A client component. It should be loaded as module reference.
+ transformedSource += importDeclarations
+ transformedSource += JSON.stringify(`${importSource}?__sc_client__`)
+ imports.push(`require(${JSON.stringify(importSource)})`)
}
- const importDeclarations = source.substring(
- lastIndex,
- node.source.span.start - beginPos
- )
- transformedSource += importDeclarations
- transformedSource += JSON.stringify(`${node.source.value}?flight`)
} else {
// For the client compilation, we skip all modules imports but
// always keep client components in the bundle. All client components
// have to be imported from either server or client components.
if (
!(
- isClientComponent(importSource, pageExtensions) ||
- isServerComponent(importSource, pageExtensions) ||
+ isClientComponent(importSource) ||
+ isServerComponent(importSource) ||
// Special cases for Next.js APIs that are considered as client
// components:
isNextComponent(importSource) ||
@@ -89,11 +118,12 @@ async function parseImportsInfo(
) {
continue
}
+
+ imports.push(`require(${JSON.stringify(importSource)})`)
}
lastIndex = node.source.span.end - beginPos
- imports.push(`require(${JSON.stringify(importSource)})`)
- continue
+ break
}
case 'ExportDefaultDeclaration': {
const def = node.decl
@@ -126,28 +156,44 @@ export default async function transformSource(
this: any,
source: string
): Promise {
- const { client: isClientCompilation, pageExtensions: pageExtensionsJson } =
- this.getOptions()
- const { resourcePath } = this
- const pageExtensions = JSON.parse(pageExtensionsJson)
+ const { client: isClientCompilation, pageExtensions } = this.getOptions()
+ const { resourcePath, resourceQuery } = this
if (typeof source !== 'string') {
throw new Error('Expected source to have been transformed to a string.')
}
+ // We currently assume that all components are shared components (unsuffixed)
+ // from node_modules.
if (resourcePath.includes('/node_modules/')) {
return source
}
+ const rawRawPageExtensions = getRawPageExtensions(pageExtensions)
+ const isServerComponent = getIsServerComponent(rawRawPageExtensions)
+ const isClientComponent = getIsClientComponent(rawRawPageExtensions)
+
+ if (!isClientCompilation) {
+ // We only apply the loader to server components, or shared components that
+ // are imported by a server component.
+ if (
+ !isServerComponent(resourcePath) &&
+ resourceQuery !== '?__sc_server__'
+ ) {
+ return source
+ }
+ }
+
const imports: string[] = []
const { source: transformedSource, defaultExportName } =
- await parseImportsInfo(
+ await parseImportsInfo({
resourcePath,
source,
imports,
isClientCompilation,
- getRawPageExtensions(pageExtensions)
- )
+ isServerComponent,
+ isClientComponent,
+ })
/**
* For .server.js files, we handle this loader differently.
@@ -177,6 +223,5 @@ export default async function transformSource(
}
const transformed = transformedSource + '\n' + noop + '\n' + defaultExportNoop
-
return transformed
}
diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts
index ef01915b4b350..29cbd09aac86b 100644
--- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts
+++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts
@@ -69,7 +69,7 @@ export class FlightManifestPlugin {
const { clientComponentsRegex } = this
compilation.chunkGroups.forEach((chunkGroup: any) => {
function recordModule(id: string, _chunk: any, mod: any) {
- const resource = mod.resource?.replace(/\?flight$/, '')
+ const resource = mod.resource?.replace(/\?__sc_client__$/, '')
// TODO: Hook into deps instead of the target module.
// That way we know by the type of dep whether to include.
diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx
index 03f5438f93612..6fad0b58d355c 100644
--- a/packages/next/client/image.tsx
+++ b/packages/next/client/image.tsx
@@ -8,6 +8,7 @@ import {
} from '../shared/lib/image-config'
import { useIntersection } from './use-intersection'
import { ImageConfigContext } from '../shared/lib/image-config-context'
+import { warnOnce } from '../shared/lib/utils'
const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
const loadedImageURLs = new Set()
@@ -23,17 +24,6 @@ if (typeof window === 'undefined') {
;(global as any).__NEXT_IMAGE_IMPORTED = true
}
-let warnOnce = (_: string) => {}
-if (process.env.NODE_ENV !== 'production') {
- const warnings = new Set()
- warnOnce = (msg: string) => {
- if (!warnings.has(msg)) {
- console.warn(msg)
- }
- warnings.add(msg)
- }
-}
-
const VALID_LOADING_VALUES = ['lazy', 'eager', undefined] as const
type LoadingValue = typeof VALID_LOADING_VALUES[number]
type ImageConfig = ImageConfigComplete & { allSizes: number[] }
diff --git a/packages/next/lib/detect-typo.ts b/packages/next/lib/detect-typo.ts
new file mode 100644
index 0000000000000..b94200482dbc9
--- /dev/null
+++ b/packages/next/lib/detect-typo.ts
@@ -0,0 +1,44 @@
+// the minimum number of operations required to convert string a to string b.
+function minDistance(a: string, b: string, threshold: number): number {
+ const m = a.length
+ const n = b.length
+
+ if (m < n) {
+ return minDistance(b, a, threshold)
+ }
+
+ if (n === 0) {
+ return m
+ }
+
+ let previousRow = Array.from({ length: n + 1 }, (_, i) => i)
+
+ for (let i = 0; i < m; i++) {
+ const s1 = a[i]
+ let currentRow = [i + 1]
+ for (let j = 0; j < n; j++) {
+ const s2 = b[j]
+ const insertions = previousRow[j + 1] + 1
+ const deletions = currentRow[j] + 1
+ const substitutions = previousRow[j] + Number(s1 !== s2)
+ currentRow.push(Math.min(insertions, deletions, substitutions))
+ }
+ previousRow = currentRow
+ }
+ return previousRow[previousRow.length - 1]
+}
+
+export function detectTypo(input: string, options: string[], threshold = 2) {
+ const potentialTypos = options
+ .map((o) => ({
+ option: o,
+ distance: minDistance(o, input, threshold),
+ }))
+ .filter(({ distance }) => distance <= threshold && distance > 0)
+ .sort((a, b) => a.distance - b.distance)
+
+ if (potentialTypos.length) {
+ return potentialTypos[0].option
+ }
+ return null
+}
diff --git a/packages/next/lib/get-project-dir.ts b/packages/next/lib/get-project-dir.ts
index 24b4c32070ab3..f132c846d0ce5 100644
--- a/packages/next/lib/get-project-dir.ts
+++ b/packages/next/lib/get-project-dir.ts
@@ -1,6 +1,8 @@
import fs from 'fs'
import path from 'path'
+import { commands } from '../bin/next'
import * as Log from '../build/output/log'
+import { detectTypo } from './detect-typo'
export function getProjectDir(dir?: string) {
try {
@@ -19,6 +21,17 @@ export function getProjectDir(dir?: string) {
return realDir
} catch (err: any) {
if (err.code === 'ENOENT') {
+ if (typeof dir === 'string') {
+ const detectedTypo = detectTypo(dir, Object.keys(commands))
+
+ if (detectedTypo) {
+ Log.error(
+ `"next ${dir}" does not exist. Did you mean "next ${detectedTypo}"?`
+ )
+ process.exit(1)
+ }
+ }
+
Log.error(
`Invalid project directory provided, no such directory: ${path.resolve(
dir || '.'
diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx
index 014b6c104a12c..2dd64eb1b402e 100644
--- a/packages/next/server/render.tsx
+++ b/packages/next/server/render.tsx
@@ -304,17 +304,15 @@ const rscCache = new Map()
function createRSCHook() {
return (
- writable: WritableStream,
+ writable: WritableStream,
id: string,
- req: ReadableStream,
+ req: ReadableStream,
bootstrap: boolean
) => {
let entry = rscCache.get(id)
if (!entry) {
const [renderStream, forwardStream] = readableStreamTee(req)
- entry = createFromReadableStream(
- pipeThrough(renderStream, createTextEncoderStream())
- )
+ entry = createFromReadableStream(renderStream)
rscCache.set(id, entry)
let bootstrapped = false
@@ -325,10 +323,11 @@ function createRSCHook() {
if (bootstrap && !bootstrapped) {
bootstrapped = true
writer.write(
- ``
+ encodeText(
+ ``
+ )
)
}
if (done) {
@@ -336,11 +335,11 @@ function createRSCHook() {
writer.close()
} else {
writer.write(
- ``
+ encodeText(
+ ``
+ )
)
process()
}
@@ -365,7 +364,7 @@ function createServerComponentRenderer(
runtime,
}: {
cachePrefix: string
- transformStream: TransformStream
+ transformStream: TransformStream
serverComponentManifest: NonNullable
runtime: 'nodejs' | 'edge'
}
@@ -381,12 +380,9 @@ function createServerComponentRenderer(
const writable = transformStream.writable
const ServerComponentWrapper = (props: any) => {
const id = (React as any).useId()
- const reqStream: ReadableStream = pipeThrough(
- renderToReadableStream(
- renderFlight(App, OriginalComponent, props),
- serverComponentManifest
- ),
- createTextDecoderStream()
+ const reqStream: ReadableStream = renderToReadableStream(
+ renderFlight(App, OriginalComponent, props),
+ serverComponentManifest
)
const response = useRSCResponse(
@@ -482,8 +478,8 @@ export async function renderToHTML(
let Component: React.ComponentType<{}> | ((props: any) => JSX.Element) =
renderOpts.Component
let serverComponentsInlinedTransformStream: TransformStream<
- string,
- string
+ Uint8Array,
+ Uint8Array
> | null = null
if (isServerComponent) {
@@ -1181,21 +1177,16 @@ export async function renderToHTML(
if (isResSent(res) && !isSSG) return null
if (renderServerComponentData) {
- const stream: ReadableStream = pipeThrough(
- renderToReadableStream(
- renderFlight(App, OriginalComponent, {
- ...props.pageProps,
- ...serverComponentProps,
- }),
- serverComponentManifest
- ),
- createTextDecoderStream()
+ const stream: ReadableStream = renderToReadableStream(
+ renderFlight(App, OriginalComponent, {
+ ...props.pageProps,
+ ...serverComponentProps,
+ }),
+ serverComponentManifest
)
+
return new RenderResult(
- pipeThrough(
- pipeThrough(stream, createBufferedTransformStream()),
- createTextEncoderStream()
- )
+ pipeThrough(stream, createBufferedTransformStream())
)
}
@@ -1360,7 +1351,8 @@ export async function renderToHTML(
generateStaticHTML: true,
})
- return await streamToString(flushEffectStream)
+ const flushed = await streamToString(flushEffectStream)
+ return flushed
}
return await renderToStream({
@@ -1607,9 +1599,7 @@ export async function renderToHTML(
return new RenderResult(html)
}
- return new RenderResult(
- pipeThrough(chainStreams(streams), createTextEncoderStream())
- )
+ return new RenderResult(chainStreams(streams))
}
function errorToJSON(err: Error) {
@@ -1707,27 +1697,10 @@ function createTransformStream({
}
}
-function createTextDecoderStream(): TransformStream {
- const decoder = new TextDecoder()
- return createTransformStream({
- transform(chunk, controller) {
- controller.enqueue(
- typeof chunk === 'string' ? chunk : decoder.decode(chunk)
- )
- },
- })
-}
-
-function createTextEncoderStream(): TransformStream {
- const encoder = new TextEncoder()
- return createTransformStream({
- transform(chunk, controller) {
- controller.enqueue(encoder.encode(chunk))
- },
- })
-}
-
-function createBufferedTransformStream(): TransformStream {
+function createBufferedTransformStream(): TransformStream<
+ Uint8Array,
+ Uint8Array
+> {
let bufferedString = ''
let pendingFlush: Promise | null = null
@@ -1735,7 +1708,7 @@ function createBufferedTransformStream(): TransformStream {
if (!pendingFlush) {
pendingFlush = new Promise((resolve) => {
setTimeout(() => {
- controller.enqueue(bufferedString)
+ controller.enqueue(encodeText(bufferedString))
bufferedString = ''
pendingFlush = null
resolve()
@@ -1747,7 +1720,7 @@ function createBufferedTransformStream(): TransformStream {
return createTransformStream({
transform(chunk, controller) {
- bufferedString += chunk
+ bufferedString += decodeText(chunk)
flushBuffer(controller)
},
@@ -1761,11 +1734,11 @@ function createBufferedTransformStream(): TransformStream {
function createFlushEffectStream(
handleFlushEffect: () => Promise
-): TransformStream {
+): TransformStream {
return createTransformStream({
async transform(chunk, controller) {
const extraChunk = await handleFlushEffect()
- controller.enqueue(extraChunk + chunk)
+ controller.enqueue(encodeText(extraChunk + decodeText(chunk)))
},
})
}
@@ -1781,10 +1754,10 @@ function renderToStream({
ReactDOMServer: typeof import('react-dom/server')
element: React.ReactElement
suffix?: string
- dataStream?: ReadableStream
+ dataStream?: ReadableStream
generateStaticHTML: boolean
flushEffectHandler?: () => Promise
-}): Promise> {
+}): Promise> {
return new Promise((resolve, reject) => {
let resolved = false
@@ -1799,7 +1772,7 @@ function renderToStream({
// defer to a microtask to ensure `stream` is set.
resolve(
Promise.resolve().then(() => {
- const transforms: Array> = [
+ const transforms: Array> = [
createBufferedTransformStream(),
flushEffectHandler
? createFlushEffectStream(flushEffectHandler)
@@ -1820,45 +1793,57 @@ function renderToStream({
}
}
- const renderStream = pipeThrough(
- (ReactDOMServer as any).renderToReadableStream(element, {
- onError(err: Error) {
- if (!resolved) {
- resolved = true
- reject(err)
- }
- },
- onCompleteShell() {
- if (!generateStaticHTML) {
- doResolve()
- }
- },
- onCompleteAll() {
+ const renderStream: ReadableStream = (
+ ReactDOMServer as any
+ ).renderToReadableStream(element, {
+ onError(err: Error) {
+ if (!resolved) {
+ resolved = true
+ reject(err)
+ }
+ },
+ onCompleteShell() {
+ if (!generateStaticHTML) {
doResolve()
- },
- }),
- createTextDecoderStream()
- )
+ }
+ },
+ onCompleteAll() {
+ doResolve()
+ },
+ })
})
}
-function createSuffixStream(suffix: string): TransformStream {
+function encodeText(input: string) {
+ return new TextEncoder().encode(input)
+}
+
+function decodeText(input?: Uint8Array) {
+ return new TextDecoder().decode(input)
+}
+
+function createSuffixStream(
+ suffix: string
+): TransformStream {
return createTransformStream({
flush(controller) {
if (suffix) {
- controller.enqueue(suffix)
+ controller.enqueue(encodeText(suffix))
}
},
})
}
-function createPrefixStream(prefix: string): TransformStream {
+function createPrefixStream(
+ prefix: string
+): TransformStream {
let prefixFlushed = false
return createTransformStream({
transform(chunk, controller) {
if (!prefixFlushed && prefix) {
prefixFlushed = true
- controller.enqueue(chunk + prefix)
+ controller.enqueue(chunk)
+ controller.enqueue(encodeText(prefix))
} else {
controller.enqueue(chunk)
}
@@ -1866,15 +1851,15 @@ function createPrefixStream(prefix: string): TransformStream {
flush(controller) {
if (!prefixFlushed && prefix) {
prefixFlushed = true
- controller.enqueue(prefix)
+ controller.enqueue(encodeText(prefix))
}
},
})
}
function createInlineDataStream(
- dataStream: ReadableStream
-): TransformStream {
+ dataStream: ReadableStream
+): TransformStream {
let dataStreamFinished: Promise | null = null
return createTransformStream({
transform(chunk, controller) {
@@ -1966,19 +1951,21 @@ function chainStreams(streams: ReadableStream[]): ReadableStream {
return readable
}
-function streamFromArray(strings: string[]): ReadableStream {
+function streamFromArray(strings: string[]): ReadableStream {
// Note: we use a TransformStream here instead of instantiating a ReadableStream
// because the built-in ReadableStream polyfill runs strings through TextEncoder.
const { readable, writable } = new TransformStream()
const writer = writable.getWriter()
- strings.forEach((str) => writer.write(str))
+ strings.forEach((str) => writer.write(encodeText(str)))
writer.close()
return readable
}
-async function streamToString(stream: ReadableStream): Promise {
+async function streamToString(
+ stream: ReadableStream
+): Promise {
const reader = stream.getReader()
let bufferedString = ''
@@ -1989,6 +1976,6 @@ async function streamToString(stream: ReadableStream): Promise {
return bufferedString
}
- bufferedString += value
+ bufferedString += decodeText(value)
}
}
diff --git a/packages/next/shared/lib/head.tsx b/packages/next/shared/lib/head.tsx
index 81d339e21a9f3..d4bbcc64bed22 100644
--- a/packages/next/shared/lib/head.tsx
+++ b/packages/next/shared/lib/head.tsx
@@ -3,6 +3,7 @@ import Effect from './side-effect'
import { AmpStateContext } from './amp-context'
import { HeadManagerContext } from './head-manager-context'
import { isInAmpMode } from './amp'
+import { warnOnce } from './utils'
type WithInAmpMode = {
inAmpMode?: boolean
@@ -161,17 +162,20 @@ function reduceComponents(
return React.cloneElement(c, newProps)
}
}
- if (process.env.NODE_ENV === 'development') {
+ if (
+ process.env.NODE_ENV === 'development' &&
+ process.env.__NEXT_CONCURRENT_FEATURES
+ ) {
// omit JSON-LD structured data snippets from the warning
if (c.type === 'script' && c.props['type'] !== 'application/ld+json') {
const srcMessage = c.props['src']
? `
+
+
+
Streaming Head
+
+)
diff --git a/test/integration/react-streaming-and-server-components/app/pages/shared.server.js b/test/integration/react-streaming-and-server-components/app/pages/shared.server.js
new file mode 100644
index 0000000000000..4bb1e2c534ffa
--- /dev/null
+++ b/test/integration/react-streaming-and-server-components/app/pages/shared.server.js
@@ -0,0 +1,22 @@
+import ClientFromDirect from '../components/client.client'
+import ClientFromShared from '../components/shared'
+import SharedFromClient from '../components/shared.client'
+
+export default function Page() {
+ // All three client components should be rendered correctly, but only
+ // shared component is a server component, and another is a client component.
+ // These two shared components should be created as two module instances.
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
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 d93f233a1aab0..527a17c875179 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
@@ -151,7 +151,7 @@ describe('Edge runtime - prod', () => {
})
basic(context, { env: 'prod' })
- streaming(context)
+ streaming(context, { env: 'prod' })
rsc(context, { runtime: 'edge', env: 'prod' })
})
@@ -184,14 +184,14 @@ describe('Edge runtime - dev', () => {
})
basic(context, { env: 'dev' })
- streaming(context)
+ streaming(context, { env: 'dev' })
rsc(context, { runtime: 'edge', env: 'dev' })
})
const nodejsRuntimeBasicSuite = {
runTests: (context, env) => {
basic(context, { env })
- streaming(context)
+ streaming(context, { env })
rsc(context, { runtime: 'nodejs' })
if (env === 'prod') {
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 a01fefe5eb1d6..6e8b76d8c2a40 100644
--- a/test/integration/react-streaming-and-server-components/test/rsc.js
+++ b/test/integration/react-streaming-and-server-components/test/rsc.js
@@ -55,6 +55,26 @@ export default function (context, { runtime, env }) {
expect(html).toContain('foo.client')
})
+ it('should resolve different kinds of components correctly', async () => {
+ const html = await renderViaHTTP(context.appPort, '/shared')
+ const main = getNodeBySelector(html, '#main').html()
+
+ // Should have 5 occurrences of "client_component".
+ expect([...main.matchAll(/client_component/g)].length).toBe(5)
+
+ // Should have 2 occurrences of "shared:server", and 2 occurrences of
+ // "shared:client".
+ const sharedServerModule = [...main.matchAll(/shared:server:(\d+)/g)]
+ const sharedClientModule = [...main.matchAll(/shared:client:(\d+)/g)]
+ expect(sharedServerModule.length).toBe(2)
+ expect(sharedClientModule.length).toBe(2)
+
+ // Should have 2 modules created for the shared component.
+ expect(sharedServerModule[0][1]).toBe(sharedServerModule[1][1])
+ expect(sharedClientModule[0][1]).toBe(sharedClientModule[1][1])
+ expect(sharedServerModule[0][1]).not.toBe(sharedClientModule[0][1])
+ })
+
it('should support next/link in server components', async () => {
const linkHTML = await renderViaHTTP(context.appPort, '/next-api/link')
const linkText = getNodeBySelector(
diff --git a/test/integration/react-streaming-and-server-components/test/streaming.js b/test/integration/react-streaming-and-server-components/test/streaming.js
index 31b6e62591492..edfeccebdcd87 100644
--- a/test/integration/react-streaming-and-server-components/test/streaming.js
+++ b/test/integration/react-streaming-and-server-components/test/streaming.js
@@ -1,6 +1,6 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
-import { fetchViaHTTP } from 'next-test-utils'
+import { fetchViaHTTP, waitFor } from 'next-test-utils'
async function resolveStreamResponse(response, onData) {
let result = ''
@@ -16,7 +16,7 @@ async function resolveStreamResponse(response, onData) {
return result
}
-export default function (context) {
+export default function (context, { env }) {
it('should support streaming for fizz response', async () => {
await fetchViaHTTP(context.appPort, '/streaming', null, {}).then(
async (response) => {
@@ -99,4 +99,82 @@ export default function (context) {
expect(result).toMatch(/<\/body><\/html>/)
})
})
+
+ if (env === 'dev') {
+ it('should warn when stylesheets or scripts are in head', async () => {
+ let browser
+ try {
+ browser = await webdriver(context.appPort, '/head')
+
+ await browser.waitForElementByCss('h1')
+ await waitFor(1000)
+ const browserLogs = await browser.log('browser')
+ let foundStyles = false
+ let foundScripts = false
+ const logs = []
+ browserLogs.forEach(({ message }) => {
+ if (message.includes('Do not add stylesheets using next/head')) {
+ foundStyles = true
+ logs.push(message)
+ }
+ if (message.includes('Do not add