From 24f450e8aa08cf648eaac86522e7e6237c0b703e Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 17 Mar 2022 17:31:46 +0100 Subject: [PATCH 1/9] Revert "Make concurrent features independent from the global runtime option" (#35405) Reverts vercel/next.js#35245 --- packages/next/build/entries.ts | 3 + packages/next/build/index.ts | 35 ++--- packages/next/build/webpack-config.ts | 13 +- packages/next/export/index.ts | 1 - packages/next/export/worker.ts | 11 +- packages/next/pages/_document.tsx | 4 +- packages/next/server/dev/hot-reloader.ts | 13 +- packages/next/server/load-components.ts | 18 +-- packages/next/server/next-server.ts | 2 +- packages/next/server/render.tsx | 15 +-- packages/next/shared/lib/html-context.ts | 1 - .../react-18-invalid-config/index.test.js | 13 ++ .../react-18/app/pages/suspense/no-preload.js | 3 +- test/integration/react-18/test/basics.js | 2 +- test/integration/react-18/test/blocking.js | 7 +- .../app/pages/err/render.js | 4 - .../app/pages/err/suspense.js | 4 - .../app/pages/next-api/image.server.js | 4 - .../app/pages/next-api/link.server.js | 4 - .../app/pages/partial-hydration.server.js | 4 - .../app/pages/routes/[dynamic].server.js | 4 - .../app/pages/streaming-rsc.server.js | 4 - .../app/pages/streaming.js | 4 - .../switchable-runtime/next.config.js | 9 -- .../switchable-runtime/package.json | 9 -- .../switchable-runtime/pages-manifest.json | 9 -- .../pages/edge-rsc.server.js | 18 --- .../switchable-runtime/pages/edge.js | 18 --- .../pages/node-rsc-ssg.server.js | 26 ---- .../pages/node-rsc-ssr.server.js | 26 ---- .../pages/node-rsc.server.js | 18 --- .../switchable-runtime/pages/node-ssg.js | 26 ---- .../switchable-runtime/pages/node-ssr.js | 26 ---- .../switchable-runtime/pages/node.js | 18 --- .../switchable-runtime/pages/static.js | 14 -- .../switchable-runtime/utils/runtime.js | 3 - .../switchable-runtime/utils/time.js | 3 - .../test/switchable-runtime.test.js | 127 ------------------ .../unsupported-native-module/next.config.js | 1 + .../unsupported-native-module/pages/index.js | 4 - 40 files changed, 60 insertions(+), 468 deletions(-) delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/next.config.js delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/package.json delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages-manifest.json delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge-rsc.server.js delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge.js delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssg.server.js delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssr.server.js delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc.server.js delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssg.js delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssr.js delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/node.js delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/static.js delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/utils/runtime.js delete mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/utils/time.js delete mode 100644 test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index c6a57df897a06d8..304b7047b1490ad 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -187,6 +187,9 @@ export async function getPageRuntime( if (!pageRuntime) { if (isRuntimeRequired) { pageRuntime = globalRuntimeFallback + } else { + // @TODO: Remove this branch to fully implement the RFC. + pageRuntime = globalRuntimeFallback } } diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 3e0584d68b43948..e1176656fe08d14 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -76,11 +76,7 @@ import { } from '../telemetry/events' import { Telemetry } from '../telemetry/storage' import { CompilerResult, runCompiler } from './compiler' -import { - createEntrypoints, - createPagesMapping, - getPageRuntime, -} from './entries' +import { createEntrypoints, createPagesMapping } from './entries' import { generateBuildId } from './generate-build-id' import { isWriteable } from './is-writeable' import * as Log from './output/log' @@ -157,10 +153,11 @@ export default async function build( setGlobal('phase', PHASE_PRODUCTION_BUILD) setGlobal('distDir', distDir) - // We enable concurrent features (Fizz-related rendering architecture) when - // using React 18 or experimental. + // Currently, when the runtime option is set (either `nodejs` or `edge`), + // we enable concurrent features (Fizz-related rendering architecture). + const runtime = config.experimental.runtime const hasReactRoot = shouldUseReactRoot() - const hasConcurrentFeatures = hasReactRoot + const hasConcurrentFeatures = !!runtime const hasServerComponents = hasReactRoot && !!config.experimental.serverComponents @@ -625,7 +622,6 @@ export default async function build( entrypoints: entrypoints.client, rewrites, runWebpackSpan, - hasReactRoot, }), getBaseWebpackConfig(dir, { buildId, @@ -637,7 +633,6 @@ export default async function build( entrypoints: entrypoints.server, rewrites, runWebpackSpan, - hasReactRoot, }), hasReactRoot ? getBaseWebpackConfig(dir, { @@ -651,7 +646,6 @@ export default async function build( entrypoints: entrypoints.edgeServer, rewrites, runWebpackSpan, - hasReactRoot, }) : null, ]) @@ -960,22 +954,10 @@ export default async function build( let ssgPageRoutes: string[] | null = null let isMiddlewareRoute = !!page.match(MIDDLEWARE_ROUTE) - const pagePath = pagePaths.find((_path) => - _path.startsWith(actualPage + '.') - ) - const pageRuntime = - hasConcurrentFeatures && pagePath - ? await getPageRuntime( - join(pagesDir, pagePath), - config.experimental.runtime - ) - : null - if ( !isMiddlewareRoute && !isReservedPage(page) && - // We currently don't support staic optimization in the Edge runtime. - pageRuntime !== 'edge' + !hasConcurrentFeatures ) { try { let isPageStaticSpan = @@ -1501,7 +1483,10 @@ export default async function build( const combinedPages = [...staticPages, ...ssgPages] - if (combinedPages.length > 0 || useStatic404 || useDefaultStatic500) { + if ( + !hasConcurrentFeatures && + (combinedPages.length > 0 || useStatic404 || useDefaultStatic500) + ) { const staticGenerationSpan = nextBuildSpan.traceChild('static-generation') await staticGenerationSpan.traceAsyncFn(async () => { detectConflictingPaths( diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index a42abcae003e0a5..b07c42e17557b83 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -48,6 +48,7 @@ import type { Span } from '../trace' import { getRawPageExtensions } from './utils' import browserslist from 'next/dist/compiled/browserslist' import loadJsConfig from './load-jsconfig' +import { shouldUseReactRoot } from '../server/config' import { getMiddlewareSourceMapPlugins } from './webpack/plugins/middleware-source-maps-plugin' const watchOptions = Object.freeze({ @@ -309,7 +310,6 @@ export default async function getBaseWebpackConfig( rewrites, isDevFallback = false, runWebpackSpan, - hasReactRoot, }: { buildId: string config: NextConfigComplete @@ -323,7 +323,6 @@ export default async function getBaseWebpackConfig( rewrites: CustomRoutes['rewrites'] isDevFallback?: boolean runWebpackSpan: Span - hasReactRoot: boolean } ): Promise { const { useTypeScript, jsConfig, resolvedBaseUrl } = await loadJsConfig( @@ -336,10 +335,10 @@ export default async function getBaseWebpackConfig( rewrites.afterFiles.length > 0 || rewrites.fallback.length > 0 const hasReactRefresh: boolean = dev && !isServer - + const hasReactRoot = shouldUseReactRoot() const runtime = config.experimental.runtime - // Make sure `reactRoot` is enabled when React 18 or experimental is detected. + // Make sure reactRoot is enabled when react 18 is detected if (hasReactRoot) { config.experimental.reactRoot = true } @@ -354,14 +353,14 @@ export default async function getBaseWebpackConfig( '`experimental.runtime` requires `experimental.reactRoot` to be enabled along with React 18.' ) } - if (config.experimental.serverComponents && !hasReactRoot) { + if (config.experimental.serverComponents && !runtime) { throw new Error( - '`experimental.serverComponents` requires React 18 to be installed.' + '`experimental.runtime` is required to be set along with `experimental.serverComponents`.' ) } const targetWeb = isEdgeRuntime || !isServer - const hasConcurrentFeatures = hasReactRoot + const hasConcurrentFeatures = !!runtime && hasReactRoot const hasServerComponents = hasConcurrentFeatures && !!config.experimental.serverComponents const disableOptimizedLoading = hasConcurrentFeatures diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index df995262048eeee..937c01ab089cfc3 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -588,7 +588,6 @@ export default async function exportApp( nextConfig.experimental.disableOptimizedLoading, parentSpanId: pageExportSpan.id, httpAgentOptions: nextConfig.httpAgentOptions, - serverComponents: nextConfig.experimental.serverComponents, }) for (const validation of result.ampValidations || []) { diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index 3679b28eebd1d4a..f2e6dbb64f133f0 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -59,7 +59,6 @@ interface ExportPageInput { disableOptimizedLoading: any parentSpanId: any httpAgentOptions: NextConfigComplete['httpAgentOptions'] - serverComponents?: boolean } interface ExportPageResults { @@ -107,7 +106,6 @@ export default async function exportPage({ optimizeCss, disableOptimizedLoading, httpAgentOptions, - serverComponents, }: ExportPageInput): Promise { setHttpAgentOptions(httpAgentOptions) const exportPageSpan = trace('export-page-worker', parentSpanId) @@ -262,7 +260,7 @@ export default async function exportPage({ getServerSideProps, getStaticProps, pageConfig, - } = await loadComponents(distDir, page, serverless, serverComponents) + } = await loadComponents(distDir, page, serverless) const ampState = { ampFirst: pageConfig?.amp === true, hasQuery: Boolean(query.amp), @@ -323,12 +321,7 @@ export default async function exportPage({ throw new Error(`Failed to render serverless page`) } } else { - const components = await loadComponents( - distDir, - page, - serverless, - serverComponents - ) + const components = await loadComponents(distDir, page, serverless) const ampState = { ampFirst: components.pageConfig?.amp === true, hasQuery: Boolean(query.amp), diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index 72868292fa6aeb6..8ac37914b0026e1 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -585,9 +585,11 @@ export class Head extends Component< disableOptimizedLoading, optimizeCss, optimizeFonts, - hasConcurrentFeatures, + runtime, } = this.context + const hasConcurrentFeatures = !!runtime + const disableRuntimeJS = unstable_runtimeJS === false const disableJsPreload = unstable_JsPreload === false || !disableOptimizedLoading diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index b30f365b54ffcf5..bcd58454ab578cf 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -154,7 +154,6 @@ export default class HotReloader { private config: NextConfigComplete private runtime?: 'nodejs' | 'edge' private hasServerComponents: boolean - private hasReactRoot: boolean public clientStats: webpack5.Stats | null public serverStats: webpack5.Stats | null private clientError: Error | null = null @@ -198,9 +197,7 @@ export default class HotReloader { this.config = config this.runtime = config.experimental.runtime - this.hasReactRoot = shouldUseReactRoot() - this.hasServerComponents = - this.hasReactRoot && !!config.experimental.serverComponents + this.hasServerComponents = !!config.experimental.serverComponents this.previewProps = previewProps this.rewrites = rewrites this.hotReloaderSpan = trace('hot-reloader', undefined, { @@ -343,6 +340,8 @@ export default class HotReloader { ) ) + const hasReactRoot = shouldUseReactRoot() + return webpackConfigSpan .traceChild('generate-webpack-config') .traceAsyncFn(() => @@ -357,7 +356,6 @@ export default class HotReloader { rewrites: this.rewrites, entrypoints: entrypoints.client, runWebpackSpan: this.hotReloaderSpan, - hasReactRoot: this.hasReactRoot, }), getBaseWebpackConfig(this.dir, { dev: true, @@ -368,10 +366,9 @@ export default class HotReloader { rewrites: this.rewrites, entrypoints: entrypoints.server, runWebpackSpan: this.hotReloaderSpan, - hasReactRoot: this.hasReactRoot, }), // The edge runtime is only supported with React root. - this.hasReactRoot + hasReactRoot ? getBaseWebpackConfig(this.dir, { dev: true, isServer: true, @@ -382,7 +379,6 @@ export default class HotReloader { rewrites: this.rewrites, entrypoints: entrypoints.edgeServer, runWebpackSpan: this.hotReloaderSpan, - hasReactRoot: this.hasReactRoot, }) : null, ].filter(Boolean) as webpack.Configuration[] @@ -421,7 +417,6 @@ export default class HotReloader { this.pagesDir ) ).client, - hasReactRoot: this.hasReactRoot, }) const fallbackCompiler = webpack(fallbackConfig) diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index 361f0183899d550..ae247d9ea66fdbe 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -6,7 +6,6 @@ import type { import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, - MIDDLEWARE_FLIGHT_MANIFEST, } from '../shared/lib/constants' import { join } from 'path' import { requirePage } from './require' @@ -31,7 +30,6 @@ export type LoadComponentsReturnType = { pageConfig: PageConfig buildManifest: BuildManifest reactLoadableManifest: ReactLoadableManifest - serverComponentManifest?: any | null Document: DocumentType App: AppType getStaticProps?: GetStaticProps @@ -63,8 +61,7 @@ export async function loadDefaultErrorComponents(distDir: string) { export async function loadComponents( distDir: string, pathname: string, - serverless: boolean, - serverComponents?: boolean + serverless: boolean ): Promise { if (serverless) { const ComponentMod = await requirePage(pathname, distDir, serverless) @@ -105,14 +102,10 @@ export async function loadComponents( requirePage(pathname, distDir, serverless), ]) - const [buildManifest, reactLoadableManifest, serverComponentManifest] = - await Promise.all([ - require(join(distDir, BUILD_MANIFEST)), - require(join(distDir, REACT_LOADABLE_MANIFEST)), - serverComponents - ? require(join(distDir, 'server', MIDDLEWARE_FLIGHT_MANIFEST + '.json')) - : null, - ]) + const [buildManifest, reactLoadableManifest] = await Promise.all([ + require(join(distDir, BUILD_MANIFEST)), + require(join(distDir, REACT_LOADABLE_MANIFEST)), + ]) const Component = interopDefault(ComponentMod) const Document = interopDefault(DocumentMod) @@ -132,6 +125,5 @@ export async function loadComponents( getServerSideProps, getStaticProps, getStaticPaths, - serverComponentManifest, } } diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 2455112a313d79b..83a8d7e007875e3 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -693,7 +693,7 @@ export default class NextNodeServer extends BaseServer { } protected getServerComponentManifest() { - if (!this.nextConfig.experimental.serverComponents) return undefined + if (!this.nextConfig.experimental.runtime) return undefined return require(join( this.distDir, 'server', diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 190cfadc6b27987..7797d1d654047ae 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -450,12 +450,12 @@ export async function renderToHTML( supportsDynamicHTML, images, reactRoot, - runtime: globalRuntime, + runtime, ComponentMod, AppMod, } = renderOpts - const hasConcurrentFeatures = reactRoot + const hasConcurrentFeatures = !!runtime let Document = renderOpts.Document const OriginalComponent = renderOpts.Component @@ -464,7 +464,7 @@ export async function renderToHTML( const isServerComponent = !!serverComponentManifest && hasConcurrentFeatures && - !!ComponentMod.__next_rsc__ + ComponentMod.__next_rsc__ let Component: React.ComponentType<{}> | ((props: any) => JSX.Element) = renderOpts.Component @@ -1243,7 +1243,7 @@ export async function renderToHTML( | typeof Document | undefined - if (process.browser && Document.getInitialProps) { + if (runtime === 'edge' && Document.getInitialProps) { // In the Edge runtime, `Document.getInitialProps` isn't supported. // We throw an error here if it's customized. if (!builtinDocument) { @@ -1329,8 +1329,7 @@ export async function renderToHTML( ) : ( - {isServerComponent && AppMod.__next_rsc__ ? ( - // _app.server.js is used. + {renderOpts.serverComponents && AppMod.__next_rsc__ ? ( ) : ( @@ -1362,6 +1361,7 @@ export async function renderToHTML( ), generateStaticHTML: true, }) + const flushed = await streamToString(flushEffectStream) return flushed } @@ -1489,8 +1489,7 @@ export async function renderToHTML( optimizeCss: renderOpts.optimizeCss, optimizeFonts: renderOpts.optimizeFonts, nextScriptWorkers: renderOpts.nextScriptWorkers, - runtime: globalRuntime, - hasConcurrentFeatures, + runtime, } const document = ( diff --git a/packages/next/shared/lib/html-context.ts b/packages/next/shared/lib/html-context.ts index b069fca0f49f9d1..0a4455715361e95 100644 --- a/packages/next/shared/lib/html-context.ts +++ b/packages/next/shared/lib/html-context.ts @@ -38,7 +38,6 @@ export type HtmlProps = { optimizeFonts?: boolean nextScriptWorkers?: boolean runtime?: 'edge' | 'nodejs' - hasConcurrentFeatures?: boolean } export const HtmlContext = createContext(null as any) diff --git a/test/integration/react-18-invalid-config/index.test.js b/test/integration/react-18-invalid-config/index.test.js index 9372f0f199f4d52..e0096a0b3502361 100644 --- a/test/integration/react-18-invalid-config/index.test.js +++ b/test/integration/react-18-invalid-config/index.test.js @@ -25,6 +25,19 @@ describe('Invalid react 18 webpack config', () => { ) }) + it('should require `experimental.runtime` for server components', async () => { + writeNextConfig({ + reactRoot: true, + serverComponents: true, + }) + const { stderr } = await nextBuild(appDir, [], { stderr: true }) + nextConfig.restore() + + expect(stderr).toContain( + '`experimental.runtime` is required to be set along with `experimental.serverComponents`.' + ) + }) + it('should warn user when not using react 18 and `experimental.reactRoot` is enabled', async () => { const reactDomPackagePah = join(appDir, 'node_modules/react-dom') await fs.mkdirp(reactDomPackagePah) diff --git a/test/integration/react-18/app/pages/suspense/no-preload.js b/test/integration/react-18/app/pages/suspense/no-preload.js index 8ae8c7c599f2eef..aaf54e244118fff 100644 --- a/test/integration/react-18/app/pages/suspense/no-preload.js +++ b/test/integration/react-18/app/pages/suspense/no-preload.js @@ -2,7 +2,6 @@ import { Suspense } from 'react' import dynamic from 'next/dynamic' const Bar = dynamic(() => import('../../components/bar'), { - ssr: false, suspense: true, // Explicitly declare loaded modules. // For suspense cases, they'll be ignored. @@ -15,7 +14,7 @@ const Bar = dynamic(() => import('../../components/bar'), { export default function NoPreload() { return ( - + ) diff --git a/test/integration/react-18/test/basics.js b/test/integration/react-18/test/basics.js index bc63d2ad4f0f5d1..debbad85a56b520 100644 --- a/test/integration/react-18/test/basics.js +++ b/test/integration/react-18/test/basics.js @@ -31,7 +31,7 @@ export default (context) => { const nextData = JSON.parse($('#__NEXT_DATA__').text()) const content = $('#__next').text() // is suspended - expect(content).toBe('fallback') + 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 index 0db4aacd6cf971c..c4feeb2e8a301f6 100644 --- a/test/integration/react-18/test/blocking.js +++ b/test/integration/react-18/test/blocking.js @@ -8,16 +8,15 @@ export default (context, render) => { return cheerio.load(html) } - it('should render fallback on server side if suspense without ssr', async () => { + 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('fallback') + expect(content).toBe('rab') expect(nextData.dynamicIds).toBeUndefined() }) - // Testing the same thing as above. - it.skip('should render import fallback on server side if suspended without ssr', async () => { + 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') diff --git a/test/integration/react-streaming-and-server-components/app/pages/err/render.js b/test/integration/react-streaming-and-server-components/app/pages/err/render.js index 7d9d321f7cb0a87..0c6ec0bd1baedbb 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/err/render.js +++ b/test/integration/react-streaming-and-server-components/app/pages/err/render.js @@ -5,7 +5,3 @@ export default function MyError() { throw new Error('oops') } } - -export const config = { - runtime: 'edge', -} diff --git a/test/integration/react-streaming-and-server-components/app/pages/err/suspense.js b/test/integration/react-streaming-and-server-components/app/pages/err/suspense.js index 05f85b9f4b16fd8..e23ccd094bbabe3 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/err/suspense.js +++ b/test/integration/react-streaming-and-server-components/app/pages/err/suspense.js @@ -18,7 +18,3 @@ export default function page() { ) } - -export const config = { - runtime: 'edge', -} diff --git a/test/integration/react-streaming-and-server-components/app/pages/next-api/image.server.js b/test/integration/react-streaming-and-server-components/app/pages/next-api/image.server.js index 96c50ace36a55e6..0083c3b9d4ccbc6 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/next-api/image.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/next-api/image.server.js @@ -7,7 +7,3 @@ const Page = () => { } export default Page - -export const config = { - runtime: 'edge', -} diff --git a/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js b/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js index 1162cea6f999ca3..a4d5cb765082c42 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js @@ -16,7 +16,3 @@ export default function LinkPage({ router }) { ) } - -export const config = { - runtime: 'edge', -} diff --git a/test/integration/react-streaming-and-server-components/app/pages/partial-hydration.server.js b/test/integration/react-streaming-and-server-components/app/pages/partial-hydration.server.js index 8604f3bdcb72673..c3740b8719feda0 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/partial-hydration.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/partial-hydration.server.js @@ -40,7 +40,3 @@ export default function () { ) } - -export const config = { - runtime: 'edge', -} diff --git a/test/integration/react-streaming-and-server-components/app/pages/routes/[dynamic].server.js b/test/integration/react-streaming-and-server-components/app/pages/routes/[dynamic].server.js index 27c7c0bddf02830..288d6165f9990c9 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/routes/[dynamic].server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/routes/[dynamic].server.js @@ -1,7 +1,3 @@ export default function Pid({ router }) { return
{`query: ${router.query.dynamic}`}
} - -export const config = { - runtime: 'edge', -} diff --git a/test/integration/react-streaming-and-server-components/app/pages/streaming-rsc.server.js b/test/integration/react-streaming-and-server-components/app/pages/streaming-rsc.server.js index 7d7c0cacce37be5..c6653c9e71f816d 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/streaming-rsc.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/streaming-rsc.server.js @@ -29,7 +29,3 @@ export default function Page() { ) } - -export const config = { - runtime: 'edge', -} diff --git a/test/integration/react-streaming-and-server-components/app/pages/streaming.js b/test/integration/react-streaming-and-server-components/app/pages/streaming.js index 70aaf4ced70ef0b..ac58f5e9e2c382c 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/streaming.js +++ b/test/integration/react-streaming-and-server-components/app/pages/streaming.js @@ -21,7 +21,3 @@ export default function Page() {
) } - -export const config = { - runtime: 'edge', -} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/next.config.js b/test/integration/react-streaming-and-server-components/switchable-runtime/next.config.js deleted file mode 100644 index 7b4ccb839cd641c..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/next.config.js +++ /dev/null @@ -1,9 +0,0 @@ -const withReact18 = require('../../react-18/test/with-react-18') - -module.exports = withReact18({ - reactStrictMode: true, - experimental: { - serverComponents: true, - // runtime: 'edge', - }, -}) diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/package.json b/test/integration/react-streaming-and-server-components/switchable-runtime/package.json deleted file mode 100644 index 90af0ce830c998e..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "private": true, - "scripts": { - "lnext": "node -r ../../react-18/test/require-hook.js ../../../../packages/next/dist/bin/next", - "dev": "yarn lnext dev", - "build": "yarn lnext build", - "start": "yarn lnext start" - } -} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages-manifest.json b/test/integration/react-streaming-and-server-components/switchable-runtime/pages-manifest.json deleted file mode 100644 index 4aa797aeaee3080..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/pages-manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "/_app": "pages/_app.js", - "/_error": "pages/_error.js", - "/edge-rsc": "pages/edge-rsc.js", - "/static": "pages/static.js", - "/node-rsc": "pages/node-rsc.js", - "/node": "pages/node.js", - "/edge": "pages/edge.js" -} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge-rsc.server.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge-rsc.server.js deleted file mode 100644 index 83dc8c219e84ef0..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge-rsc.server.js +++ /dev/null @@ -1,18 +0,0 @@ -import getRuntime from '../utils/runtime' -import getTime from '../utils/time' - -export default function Page() { - return ( -
- This is a SSR RSC page. -
- {'Runtime: ' + getRuntime()} -
- {'Time: ' + getTime()} -
- ) -} - -export const config = { - runtime: 'edge', -} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge.js deleted file mode 100644 index c3425c7e64af7cd..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge.js +++ /dev/null @@ -1,18 +0,0 @@ -import getRuntime from '../utils/runtime' -import getTime from '../utils/time' - -export default function Page() { - return ( -
- This is a SSR page. -
- {'Runtime: ' + getRuntime()} -
- {'Time: ' + getTime()} -
- ) -} - -export const config = { - runtime: 'edge', -} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssg.server.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssg.server.js deleted file mode 100644 index 362e634644eb2ba..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssg.server.js +++ /dev/null @@ -1,26 +0,0 @@ -import getRuntime from '../utils/runtime' -import getTime from '../utils/time' - -export default function Page({ type }) { - return ( -
- This is a {type} RSC page. -
- {'Runtime: ' + getRuntime()} -
- {'Time: ' + getTime()} -
- ) -} - -export function getStaticProps() { - return { - props: { - type: 'SSG', - }, - } -} - -export const config = { - runtime: 'nodejs', -} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssr.server.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssr.server.js deleted file mode 100644 index 1b8b01526a3cede..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssr.server.js +++ /dev/null @@ -1,26 +0,0 @@ -import getRuntime from '../utils/runtime' -import getTime from '../utils/time' - -export default function Page({ type }) { - return ( -
- This is a {type} RSC page. -
- {'Runtime: ' + getRuntime()} -
- {'Time: ' + getTime()} -
- ) -} - -export function getServerSideProps() { - return { - props: { - type: 'SSR', - }, - } -} - -export const config = { - runtime: 'nodejs', -} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc.server.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc.server.js deleted file mode 100644 index f3563039b63bdfe..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc.server.js +++ /dev/null @@ -1,18 +0,0 @@ -import getRuntime from '../utils/runtime' -import getTime from '../utils/time' - -export default function Page() { - return ( -
- This is a static RSC page. -
- {'Runtime: ' + getRuntime()} -
- {'Time: ' + getTime()} -
- ) -} - -export const config = { - runtime: 'nodejs', -} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssg.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssg.js deleted file mode 100644 index d555009acfcdb83..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssg.js +++ /dev/null @@ -1,26 +0,0 @@ -import getRuntime from '../utils/runtime' -import getTime from '../utils/time' - -export default function Page({ type }) { - return ( -
- This is a {type} page. -
- {'Runtime: ' + getRuntime()} -
- {'Time: ' + getTime()} -
- ) -} - -export function getStaticProps() { - return { - props: { - type: 'SSG', - }, - } -} - -export const config = { - runtime: 'nodejs', -} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssr.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssr.js deleted file mode 100644 index e58276b47a76386..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssr.js +++ /dev/null @@ -1,26 +0,0 @@ -import getRuntime from '../utils/runtime' -import getTime from '../utils/time' - -export default function Page({ type }) { - return ( -
- This is a {type} page. -
- {'Runtime: ' + getRuntime()} -
- {'Time: ' + getTime()} -
- ) -} - -export function getServerSideProps() { - return { - props: { - type: 'SSR', - }, - } -} - -export const config = { - runtime: 'nodejs', -} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node.js deleted file mode 100644 index bf065da478ba866..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node.js +++ /dev/null @@ -1,18 +0,0 @@ -import getRuntime from '../utils/runtime' -import getTime from '../utils/time' - -export default function Page() { - return ( -
- This is a static page. -
- {'Runtime: ' + getRuntime()} -
- {'Time: ' + getTime()} -
- ) -} - -export const config = { - runtime: 'nodejs', -} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/static.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/static.js deleted file mode 100644 index e44edfd7952325d..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/static.js +++ /dev/null @@ -1,14 +0,0 @@ -import getRuntime from '../utils/runtime' -import getTime from '../utils/time' - -export default function Page() { - return ( -
- This is a static page. -
- {'Runtime: ' + getRuntime()} -
- {'Time: ' + getTime()} -
- ) -} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/utils/runtime.js b/test/integration/react-streaming-and-server-components/switchable-runtime/utils/runtime.js deleted file mode 100644 index 444f1ee8b498bbb..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/utils/runtime.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function getRuntime() { - return process.version ? `Node.js ${process.version}` : 'Edge/Browser' -} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/utils/time.js b/test/integration/react-streaming-and-server-components/switchable-runtime/utils/time.js deleted file mode 100644 index cf78549b9a7c124..000000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/utils/time.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function getTime() { - return Date.now() -} 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 deleted file mode 100644 index 62ebdcadf4264fc..000000000000000 --- a/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js +++ /dev/null @@ -1,127 +0,0 @@ -/* eslint-env jest */ - -import { join } from 'path' -import { - // File, - nextBuild as _nextBuild, - nextStart as _nextStart, -} from 'next-test-utils' - -import { findPort, killApp, renderViaHTTP } from 'next-test-utils' - -const nodeArgs = ['-r', join(__dirname, '../../react-18/test/require-hook.js')] - -const appDir = join(__dirname, '../switchable-runtime') -// const nextConfig = new File(join(appDir, 'next.config.js')) - -async function nextBuild(dir, options) { - return await _nextBuild(dir, [], { - ...options, - stdout: true, - stderr: true, - nodeArgs, - }) -} - -async function nextStart(dir, port) { - return await _nextStart(dir, port, { - stdout: true, - stderr: true, - nodeArgs, - }) -} - -async function testRoute(appPort, url, { isStatic, isEdge }) { - const html1 = await renderViaHTTP(appPort, url) - const renderedAt1 = +html1.match(/Time: (\d+)/)[1] - expect(html1).toContain(`Runtime: ${isEdge ? 'Edge' : 'Node.js'}`) - - const html2 = await renderViaHTTP(appPort, url) - const renderedAt2 = +html2.match(/Time: (\d+)/)[1] - expect(html2).toContain(`Runtime: ${isEdge ? 'Edge' : 'Node.js'}`) - - if (isStatic) { - // Should not be re-rendered, some timestamp should be returned. - expect(renderedAt1).toBe(renderedAt2) - } else { - // Should be re-rendered. - expect(renderedAt1).toBeLessThan(renderedAt2) - } -} - -describe('Without global runtime configuration', () => { - const context = { appDir } - - beforeAll(async () => { - context.appPort = await findPort() - const { stderr } = await nextBuild(context.appDir) - context.stderr = stderr - context.server = await nextStart(context.appDir, context.appPort) - }) - afterAll(async () => { - await killApp(context.server) - }) - - it('should build /static as a static page with the nodejs runtime', async () => { - await testRoute(context.appPort, '/static', { - isStatic: true, - isEdge: false, - }) - }) - - it('should build /node as a static page with the nodejs runtime', async () => { - await testRoute(context.appPort, '/node', { - isStatic: true, - isEdge: false, - }) - }) - - it('should build /node-ssr as a dynamic page with the nodejs runtime', async () => { - await testRoute(context.appPort, '/node-ssr', { - isStatic: false, - isEdge: false, - }) - }) - - it('should build /node-ssg as a static page with the nodejs runtime', async () => { - await testRoute(context.appPort, '/node-ssg', { - isStatic: true, - isEdge: false, - }) - }) - - it('should build /node-rsc as a static page with the nodejs runtime', async () => { - await testRoute(context.appPort, '/node-rsc', { - isStatic: true, - isEdge: false, - }) - }) - - it('should build /node-rsc-ssr as a dynamic page with the nodejs runtime', async () => { - await testRoute(context.appPort, '/node-rsc-ssr', { - isStatic: false, - isEdge: false, - }) - }) - - it('should build /node-rsc-ssg as a static page with the nodejs runtime', async () => { - await testRoute(context.appPort, '/node-rsc-ssg', { - isStatic: true, - isEdge: false, - }) - }) - - it('should build /edge as a dynamic page with the edge runtime', async () => { - await testRoute(context.appPort, '/edge', { - isStatic: false, - isEdge: true, - }) - }) - - it('should build /edge-rsc as a dynamic page with the edge runtime', async () => { - await testRoute(context.appPort, '/edge-rsc', { - isStatic: false, - isEdge: true, - }) - }) -}) diff --git a/test/integration/react-streaming-and-server-components/unsupported-native-module/next.config.js b/test/integration/react-streaming-and-server-components/unsupported-native-module/next.config.js index deb87bcba88d7c6..4783ccbdadb7868 100644 --- a/test/integration/react-streaming-and-server-components/unsupported-native-module/next.config.js +++ b/test/integration/react-streaming-and-server-components/unsupported-native-module/next.config.js @@ -3,6 +3,7 @@ const withReact18 = require('../../react-18/test/with-react-18') module.exports = withReact18({ experimental: { reactRoot: true, + runtime: 'edge', serverComponents: true, }, }) diff --git a/test/integration/react-streaming-and-server-components/unsupported-native-module/pages/index.js b/test/integration/react-streaming-and-server-components/unsupported-native-module/pages/index.js index 8371b4c194f7d0b..31f0f204a9d3bd4 100644 --- a/test/integration/react-streaming-and-server-components/unsupported-native-module/pages/index.js +++ b/test/integration/react-streaming-and-server-components/unsupported-native-module/pages/index.js @@ -8,7 +8,3 @@ export default function Index() { console.log(EOF) return 'Access Node.js native module dns' } - -export const config = { - runtime: 'edge', -} From d3a53a6f018d572371f3ca41d0148c8329b59b33 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 17 Mar 2022 12:06:44 -0500 Subject: [PATCH 2/9] Update on-demand ISR to skip fetch locally (#35386) --- packages/next/server/api-utils/node.ts | 94 +++++++++++++----------- packages/next/server/image-optimizer.ts | 62 ++-------------- packages/next/server/lib/mock-request.ts | 70 ++++++++++++++++++ packages/next/server/next-server.ts | 7 +- 4 files changed, 132 insertions(+), 101 deletions(-) create mode 100644 packages/next/server/lib/mock-request.ts diff --git a/packages/next/server/api-utils/node.ts b/packages/next/server/api-utils/node.ts index 73f4d6d8cd77641..68f928a23755259 100644 --- a/packages/next/server/api-utils/node.ts +++ b/packages/next/server/api-utils/node.ts @@ -31,6 +31,7 @@ import { SYMBOL_PREVIEW_DATA, RESPONSE_LIMIT_DEFAULT, } from './index' +import { mockRequest } from '../lib/mock-request' export function tryGetPreviewData( req: IncomingMessage | BaseNextRequest, @@ -149,16 +150,17 @@ export async function parseBody( } } +type ApiContext = __ApiPreviewProps & { + trustHostHeader?: boolean + revalidate?: (_req: IncomingMessage, _res: ServerResponse) => Promise +} + export async function apiResolver( req: IncomingMessage, res: ServerResponse, query: any, resolverModule: any, - apiContext: __ApiPreviewProps & { - trustHostHeader?: boolean - hostname?: string - port?: number - }, + apiContext: ApiContext, propagateError: boolean, dev?: boolean, page?: string @@ -277,55 +279,59 @@ export async function apiResolver( async function unstable_revalidate( urlPath: string, - req: IncomingMessage | BaseNextRequest, - context: { - hostname?: string - port?: number - previewModeId: string - trustHostHeader?: boolean - } + req: IncomingMessage, + context: ApiContext ) { - if (!context.trustHostHeader && (!context.hostname || !context.port)) { - throw new Error( - `"hostname" and "port" must be provided when starting next to use "unstable_revalidate". See more here https://nextjs.org/docs/advanced-features/custom-server` - ) - } - if (typeof urlPath !== 'string' || !urlPath.startsWith('/')) { throw new Error( `Invalid urlPath provided to revalidate(), must be a path e.g. /blog/post-1, received ${urlPath}` ) } - const baseUrl = context.trustHostHeader - ? `https://${req.headers.host}` - : `http://${context.hostname}:${context.port}` - - const extraHeaders: Record = {} - - if (context.trustHostHeader) { - extraHeaders.cookie = req.headers.cookie - } - try { - const res = await fetch(`${baseUrl}${urlPath}`, { - headers: { - [PRERENDER_REVALIDATE_HEADER]: context.previewModeId, - ...extraHeaders, - }, - }) - - // we use the cache header to determine successful revalidate as - // a non-200 status code can be returned from a successful revalidate - // e.g. notFound: true returns 404 status code but is successful - const cacheHeader = - res.headers.get('x-vercel-cache') || res.headers.get('x-nextjs-cache') + if (context.trustHostHeader) { + const res = await fetch(`https://${req.headers.host}${urlPath}`, { + headers: { + [PRERENDER_REVALIDATE_HEADER]: context.previewModeId, + cookie: req.headers.cookie || '', + }, + }) + // we use the cache header to determine successful revalidate as + // a non-200 status code can be returned from a successful revalidate + // e.g. notFound: true returns 404 status code but is successful + const cacheHeader = + res.headers.get('x-vercel-cache') || res.headers.get('x-nextjs-cache') + + if (cacheHeader?.toUpperCase() !== 'REVALIDATED') { + throw new Error(`Invalid response ${res.status}`) + } + } else if (context.revalidate) { + const { + req: mockReq, + res: mockRes, + streamPromise, + } = mockRequest( + urlPath, + { + [PRERENDER_REVALIDATE_HEADER]: context.previewModeId, + }, + 'GET' + ) + await context.revalidate(mockReq, mockRes) + await streamPromise - if (cacheHeader?.toUpperCase() !== 'REVALIDATED') { - throw new Error(`Invalid response ${res.status}`) + if (mockRes.getHeader('x-nextjs-cache') !== 'REVALIDATED') { + throw new Error(`Invalid response ${mockRes.status}`) + } + } else { + throw new Error( + `Invariant: required internal revalidate method not passed to api-utils` + ) } - } catch (err) { - throw new Error(`Failed to revalidate ${urlPath}`) + } catch (err: unknown) { + throw new Error( + `Failed to revalidate ${urlPath}: ${isError(err) ? err.message : err}` + ) } } diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 9adec2dd8a1d3bd..8a84ce7a8bb5838 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -8,7 +8,6 @@ import { IncomingMessage, ServerResponse } from 'http' import isAnimated from 'next/dist/compiled/is-animated' import contentDisposition from 'next/dist/compiled/content-disposition' import { join } from 'path' -import Stream from 'stream' import nodeUrl, { UrlWithParsedQuery } from 'url' import { NextConfigComplete } from './config-shared' import { processBuffer, decodeBuffer, Operation } from './lib/squoosh/main' @@ -17,6 +16,7 @@ import { getContentType, getExtension } from './serve-static' import chalk from 'next/dist/compiled/chalk' import { NextUrlWithParsedQuery } from './request-meta' import { IncrementalCacheEntry, IncrementalCacheValue } from './response-cache' +import { mockRequest } from './lib/mock-request' type XCacheHeader = 'MISS' | 'HIT' | 'STALE' @@ -307,60 +307,12 @@ export async function imageOptimizer( maxAge = getMaxAge(upstreamRes.headers.get('Cache-Control')) } else { try { - const resBuffers: Buffer[] = [] - const mockRes: any = new Stream.Writable() - - const isStreamFinished = new Promise(function (resolve, reject) { - mockRes.on('finish', () => resolve(true)) - mockRes.on('end', () => resolve(true)) - mockRes.on('error', (err: any) => reject(err)) - }) - - mockRes.write = (chunk: Buffer | string) => { - resBuffers.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) - } - mockRes._write = ( - chunk: Buffer | string, - _encoding: string, - callback: () => void - ) => { - mockRes.write(chunk) - // According to Node.js documentation, the callback MUST be invoked to signal that - // the write completed successfully. If this callback is not invoked, the 'finish' event - // will not be emitted. - // https://nodejs.org/docs/latest-v16.x/api/stream.html#writable_writechunk-encoding-callback - callback() - } - - const mockHeaders: Record = {} - - mockRes.writeHead = (_status: any, _headers: any) => - Object.assign(mockHeaders, _headers) - mockRes.getHeader = (name: string) => mockHeaders[name.toLowerCase()] - mockRes.getHeaders = () => mockHeaders - mockRes.getHeaderNames = () => Object.keys(mockHeaders) - mockRes.setHeader = (name: string, value: string | string[]) => - (mockHeaders[name.toLowerCase()] = value) - mockRes.removeHeader = (name: string) => { - delete mockHeaders[name.toLowerCase()] - } - mockRes._implicitHeader = () => {} - mockRes.connection = _res.connection - mockRes.finished = false - mockRes.statusCode = 200 - - const mockReq: any = new Stream.Readable() - - mockReq._read = () => { - mockReq.emit('end') - mockReq.emit('close') - return Buffer.from('') - } - - mockReq.headers = _req.headers - mockReq.method = _req.method - mockReq.url = href - mockReq.connection = _req.connection + const { + resBuffers, + req: mockReq, + res: mockRes, + streamPromise: isStreamFinished, + } = mockRequest(href, _req.headers, _req.method || 'GET', _req.connection) await handleRequest(mockReq, mockRes, nodeUrl.parse(href, true)) await isStreamFinished diff --git a/packages/next/server/lib/mock-request.ts b/packages/next/server/lib/mock-request.ts new file mode 100644 index 000000000000000..5d25ba0ddd105af --- /dev/null +++ b/packages/next/server/lib/mock-request.ts @@ -0,0 +1,70 @@ +import Stream from 'stream' + +export function mockRequest( + requestUrl: string, + requestHeaders: Record, + requestMethod: string, + requestConnection?: any +) { + const resBuffers: Buffer[] = [] + const mockRes: any = new Stream.Writable() + + const isStreamFinished = new Promise(function (resolve, reject) { + mockRes.on('finish', () => resolve(true)) + mockRes.on('end', () => resolve(true)) + mockRes.on('error', (err: any) => reject(err)) + }) + + mockRes.write = (chunk: Buffer | string) => { + resBuffers.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + } + mockRes._write = ( + chunk: Buffer | string, + _encoding: string, + callback: () => void + ) => { + mockRes.write(chunk) + // According to Node.js documentation, the callback MUST be invoked to signal that + // the write completed successfully. If this callback is not invoked, the 'finish' event + // will not be emitted. + // https://nodejs.org/docs/latest-v16.x/api/stream.html#writable_writechunk-encoding-callback + callback() + } + + const mockHeaders: Record = {} + + mockRes.writeHead = (_status: any, _headers: any) => + Object.assign(mockHeaders, _headers) + mockRes.getHeader = (name: string) => mockHeaders[name.toLowerCase()] + mockRes.getHeaders = () => mockHeaders + mockRes.getHeaderNames = () => Object.keys(mockHeaders) + mockRes.setHeader = (name: string, value: string | string[]) => + (mockHeaders[name.toLowerCase()] = value) + mockRes.removeHeader = (name: string) => { + delete mockHeaders[name.toLowerCase()] + } + mockRes._implicitHeader = () => {} + mockRes.connection = requestConnection + mockRes.finished = false + mockRes.statusCode = 200 + + const mockReq: any = new Stream.Readable() + + mockReq._read = () => { + mockReq.emit('end') + mockReq.emit('close') + return Buffer.from('') + } + + mockReq.headers = requestHeaders + mockReq.method = requestMethod + mockReq.url = requestUrl + mockReq.connection = requestConnection + + return { + resBuffers, + req: mockReq, + res: mockRes, + streamPromise: isStreamFinished, + } +} diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 83a8d7e007875e3..d7fa9a4d5f8c74a 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -554,8 +554,11 @@ export default class NextNodeServer extends BaseServer { pageModule, { ...this.renderOpts.previewProps, - port: this.port, - hostname: this.hostname, + revalidate: (newReq: IncomingMessage, newRes: ServerResponse) => + this.getRequestHandler()( + new NodeNextRequest(newReq), + new NodeNextResponse(newRes) + ), // internal config so is not typed trustHostHeader: (this.nextConfig.experimental as any).trustHostHeader, }, From 62b46772a07ea7c4bd2017bec6a49622a6e0f181 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 17 Mar 2022 18:40:13 +0100 Subject: [PATCH 3/9] Use hasConcurrentFeatures instead of reactRoot as the condition of Fizz (#35407) We should use the `hasConcurrentFeatures` as the condition to enable Fizz, instead of reactRoot, otherwise just by installing React 18 will break stuff such as CSS imports. Currently `hasConcurrentFeatures` still needs to opt-in via the global `runtime` option. Once we fixed all the CSS bugs and add back #35245, we can flip the condition here again. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint` --- packages/next/server/render.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 7797d1d654047ae..e80cf62991cc77d 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -449,7 +449,7 @@ export async function renderToHTML( devOnlyCacheBusterQueryString, supportsDynamicHTML, images, - reactRoot, + // reactRoot, runtime, ComponentMod, AppMod, @@ -1216,7 +1216,7 @@ export async function renderToHTML( return inAmpMode ? children :
{children}
} - const ReactDOMServer = reactRoot + const ReactDOMServer = hasConcurrentFeatures ? require('react-dom/server.browser') : require('react-dom/server') @@ -1339,7 +1339,7 @@ export async function renderToHTML( ) } - if (reactRoot) { + if (hasConcurrentFeatures) { bodyResult = async (suffix: string) => { // this must be called inside bodyResult so appWrappers is // up to date when getWrappedApp is called From 9129bb9a054f36890d3138fd751c358b57534a07 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 17 Mar 2022 12:51:26 -0500 Subject: [PATCH 4/9] v12.1.1-canary.14 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-next/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-dev-overlay/package.json | 2 +- packages/react-refresh-utils/package.json | 2 +- 15 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lerna.json b/lerna.json index 4415b75844cbced..5e8cd0b1971db5c 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "12.1.1-canary.13" + "version": "12.1.1-canary.14" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 2d57e6a8e2baa0e..d0e4f0d7d2a671f 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index e68969e8aa4275c..5b3017880013fa0 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "description": "ESLint configuration used by NextJS.", "main": "index.js", "license": "MIT", @@ -9,7 +9,7 @@ "directory": "packages/eslint-config-next" }, "dependencies": { - "@next/eslint-plugin-next": "12.1.1-canary.13", + "@next/eslint-plugin-next": "12.1.1-canary.14", "@rushstack/eslint-patch": "1.0.8", "@typescript-eslint/parser": "5.10.1", "eslint-import-resolver-node": "0.3.4", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 3fe50fb6b560104..690f94fef4168f6 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "description": "ESLint plugin for NextJS.", "main": "lib/index.js", "license": "MIT", diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index fb7f83556de056e..a9a59a5c10672cb 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index e709967e7c402cc..e3565e5c5371c03 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 2cc25fd36d5d215..3047335bfe99bd4 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 868e2c86ced2bbd..1e157d63d80d576 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 74a1e3a4e04e40e..66f42c4541a2199 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 70fc47de314b664..01186936731e9f8 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index e3a31329d734cc9..07ce96320c474a4 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index dbe4fe548158304..4e36d27fbd4c8a8 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "private": true, "scripts": { "build-native": "napi build --platform --cargo-name next_swc_napi native", diff --git a/packages/next/package.json b/packages/next/package.json index 355a8dc0cd55a0f..91ce61279a02b5a 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -69,7 +69,7 @@ ] }, "dependencies": { - "@next/env": "12.1.1-canary.13", + "@next/env": "12.1.1-canary.14", "caniuse-lite": "^1.0.30001283", "postcss": "8.4.5", "styled-jsx": "5.0.1", @@ -118,11 +118,11 @@ "@hapi/accept": "5.0.2", "@napi-rs/cli": "2.4.4", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "12.1.1-canary.13", - "@next/polyfill-nomodule": "12.1.1-canary.13", - "@next/react-dev-overlay": "12.1.1-canary.13", - "@next/react-refresh-utils": "12.1.1-canary.13", - "@next/swc": "12.1.1-canary.13", + "@next/polyfill-module": "12.1.1-canary.14", + "@next/polyfill-nomodule": "12.1.1-canary.14", + "@next/react-dev-overlay": "12.1.1-canary.14", + "@next/react-refresh-utils": "12.1.1-canary.14", + "@next/swc": "12.1.1-canary.14", "@peculiar/webcrypto": "1.3.1", "@taskr/clear": "1.1.0", "@taskr/esnext": "1.1.0", diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 507779634a8ee20..19af781d08e37ce 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-dev-overlay", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 8c6b47de2f60a3c..5ce3467fc42e0b6 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "12.1.1-canary.13", + "version": "12.1.1-canary.14", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", From bc460229ab5dc0aa7589c9e25b4581b60c014e9d Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 17 Mar 2022 15:16:15 -0400 Subject: [PATCH 5/9] Update `next/image` docs for width & height (#35188) - Fixes #30219 Co-authored-by: Rich Haines --- docs/api-reference/next/image.md | 16 ++++++++++++---- docs/basic-features/image-optimization.md | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index 4035fa8c65ec71a..2e75c38e14e7249 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -48,15 +48,23 @@ When using an external URL, you must add it to ### width -The width of the image, in pixels. Must be an integer without a unit. +The `width` property can represent either the _rendered_ width or _original_ width in pixels, depending on the [`layout`](#layout) and [`sizes`](#sizes) properties. -Required, except for statically imported images, or those with [`layout="fill"`](#layout). +When using `layout="intrinsic"`, `layout="fixed"`, or `layout="raw"` without `sizes`, the `width` property represents the _rendered_ width in pixels, so it will affect how large the image appears. + +When using `layout="responsive"`, `layout="fill"`, or `layout="raw"` with `sizes`, the `width` property represents the _original_ width in pixels, so it will only affect the aspect ratio. + +The `width` property is required, except for [statically imported images](#local-images), or those with `layout="fill"`. ### height -The height of the image, in pixels. Must be an integer without a unit. +The `height` property can represent either the _rendered_ height or _original_ height in pixels, depending on the [`layout`](#layout) and [`sizes`](#sizes) properties. + +When using `layout="intrinsic"`, `layout="fixed"`, or `layout="raw"` without `sizes`, the `height` property represents the _rendered_ height in pixels, so it will affect how large the image appears. + +When using `layout="responsive"`, `layout="fill"`, or `layout="raw"` with `sizes`, the `height` property represents the _original_ height in pixels, so it will only affect the aspect ratio. -Required, except for statically imported images, or those with [`layout="fill"`](#layout). +The `height` property is required, except for [statically imported images](#local-images), or those with `layout="fill"`. ## Optional Props diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index 9de528f1910b175..0c55837410ec0fa 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -150,8 +150,8 @@ One of the ways that images most commonly hurt performance is through _layout sh Because `next/image` is designed to guarantee good performance results, it cannot be used in a way that will contribute to layout shift, and **must** be sized in one of three ways: 1. Automatically, using a [static import](#local-images) -2. Explicitly, by including a `height` **and** `width` property -3. Implicitly, by using `layout="fill"` which causes the image to expand to fill its parent element. +2. Explicitly, by including a [`width`](/docs/api-reference/next/image.md#width) and [`height`](/docs/api-reference/next/image.md#height) property +3. Implicitly, by using [`layout="fill"`](/docs/api-reference/next/image.md#layout) which causes the image to expand to fill its parent element. > ### What if I don't know the size of my images? > From 4365463fd92c7d17a4f112a8590e2fbc3d57b54a Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 17 Mar 2022 17:47:59 -0500 Subject: [PATCH 6/9] Run tests against node LTS (v16) and current (v17) (#35414) * Run tests against node LTS (v16) and current (v17) * update pnp tests --- .github/workflows/build_test_deploy.yml | 24 +++++-- test/e2e/yarn-pnp/test/utils.ts | 83 +++++++++++++------------ 2 files changed, 64 insertions(+), 43 deletions(-) diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index bf9b28cf3a04fae..7937f7a0ec667f5 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -208,12 +208,16 @@ jobs: env: NEXT_TELEMETRY_DISABLED: 1 NEXT_TEST_JOB: 1 + strategy: + fail-fast: false + matrix: + node: [16, 17] steps: - name: Setup node uses: actions/setup-node@v2 if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: - node-version: 14 + node-version: ${{ matrix.node }} - run: echo ${{needs.build.outputs.docsChange}} @@ -258,12 +262,16 @@ jobs: env: NEXT_TELEMETRY_DISABLED: 1 NEXT_TEST_JOB: 1 + strategy: + fail-fast: false + matrix: + node: [16, 17] steps: - name: Setup node uses: actions/setup-node@v2 if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: - node-version: 14 + node-version: ${{ matrix.node }} - run: echo ${{needs.build.outputs.docsChange}} @@ -308,12 +316,16 @@ jobs: env: NEXT_TELEMETRY_DISABLED: 1 NEXT_TEST_JOB: 1 + strategy: + fail-fast: false + matrix: + node: [16, 17] steps: - name: Setup node uses: actions/setup-node@v2 if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: - node-version: 14 + node-version: ${{ matrix.node }} - run: echo ${{needs.build.outputs.docsChange}} @@ -348,12 +360,16 @@ jobs: env: NEXT_TELEMETRY_DISABLED: 1 NEXT_TEST_JOB: 1 + strategy: + fail-fast: false + matrix: + node: [16, 17] steps: - name: Setup node uses: actions/setup-node@v2 if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: - node-version: 14 + node-version: ${{ matrix.node }} - run: echo ${{needs.build.outputs.docsChange}} diff --git a/test/e2e/yarn-pnp/test/utils.ts b/test/e2e/yarn-pnp/test/utils.ts index 57ceb10c050e6be..976a339e5fb080c 100644 --- a/test/e2e/yarn-pnp/test/utils.ts +++ b/test/e2e/yarn-pnp/test/utils.ts @@ -7,49 +7,54 @@ import { NextInstance } from 'test/lib/next-modes/base' jest.setTimeout(2 * 60 * 1000) export function runTests(example = '') { - let next: NextInstance + const versionParts = process.versions.node.split('.').map((i) => Number(i)) - beforeAll(async () => { - const srcDir = join(__dirname, '../../../../examples', example) - const srcFiles = await fs.readdir(srcDir) + if ( + versionParts[0] > 16 || + (versionParts[0] === 16 && versionParts[1] >= 14) + ) { + let next: NextInstance - const packageJson = await fs.readJson(join(srcDir, 'package.json')) + beforeAll(async () => { + const srcDir = join(__dirname, '../../../../examples', example) + const srcFiles = await fs.readdir(srcDir) - next = await createNext({ - files: srcFiles.reduce((prev, file) => { - if (file !== 'package.json') { - prev[file] = new FileRef(join(srcDir, file)) - } - return prev - }, {} as { [key: string]: FileRef }), - dependencies: { - ...packageJson.dependencies, - ...packageJson.devDependencies, - }, - installCommand: ({ dependencies }) => { - const pkgs = Object.keys(dependencies).reduce((prev, cur) => { - prev.push(`${cur}@${dependencies[cur]}`) + const packageJson = await fs.readJson(join(srcDir, 'package.json')) + + next = await createNext({ + files: srcFiles.reduce((prev, file) => { + if (file !== 'package.json') { + prev[file] = new FileRef(join(srcDir, file)) + } return prev - }, [] as string[]) - return `yarn set version 3.1.1 && yarn config set enableGlobalCache true && yarn config set compressionLevel 0 && yarn add ${pkgs.join( - ' ' - )}` - }, - buildCommand: `yarn next build --no-lint`, - startCommand: (global as any).isNextDev ? `yarn next` : `yarn next start`, + }, {} as { [key: string]: FileRef }), + dependencies: { + ...packageJson.dependencies, + ...packageJson.devDependencies, + }, + installCommand: ({ dependencies }) => { + const pkgs = Object.keys(dependencies).reduce((prev, cur) => { + prev.push(`${cur}@${dependencies[cur]}`) + return prev + }, [] as string[]) + return `yarn set version berry && yarn config set enableGlobalCache true && yarn config set compressionLevel 0 && yarn add ${pkgs.join( + ' ' + )}` + }, + buildCommand: `yarn next build --no-lint`, + startCommand: (global as any).isNextDev + ? `yarn next` + : `yarn next start`, + }) }) - }) - afterAll(() => next?.destroy()) - - it('should warn on not fully supported node versions', async () => { - expect(next.cliOutput).toContain( - 'Node.js 16.14+ is required for Yarn PnP 3.20+. More info' - ) - }) + afterAll(() => next?.destroy()) - it(`should compile and serve the index page correctly ${example}`, async () => { - const res = await fetchViaHTTP(next.url, '/') - expect(res.status).toBe(200) - expect(await res.text()).toContain(' { + const res = await fetchViaHTTP(next.url, '/') + expect(res.status).toBe(200) + expect(await res.text()).toContain(' {}) + } } From 757a0c708221fe28eca11811a10c81b2b78901f4 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Fri, 18 Mar 2022 00:21:16 +0100 Subject: [PATCH 7/9] Abstract out stream related utils from render and web/utils (#35372) The `readableStreamTee` util is only used by server/render.tsx, but ended up in the middleware runtime bundle. So it's better to add all the utils in one place, and we can remove them once upstream implementation of ReadableStream is ready. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint` --- .../next/server/node-web-streams-helper.ts | 359 ++++++++++++++++++ packages/next/server/render.tsx | 344 +---------------- packages/next/server/web/utils.ts | 26 -- 3 files changed, 370 insertions(+), 359 deletions(-) create mode 100644 packages/next/server/node-web-streams-helper.ts diff --git a/packages/next/server/node-web-streams-helper.ts b/packages/next/server/node-web-streams-helper.ts new file mode 100644 index 000000000000000..b8b1cc6ff98da83 --- /dev/null +++ b/packages/next/server/node-web-streams-helper.ts @@ -0,0 +1,359 @@ +export function readableStreamTee( + readable: ReadableStream +): [ReadableStream, ReadableStream] { + const transformStream = new TransformStream() + const transformStream2 = new TransformStream() + const writer = transformStream.writable.getWriter() + const writer2 = transformStream2.writable.getWriter() + + const reader = readable.getReader() + function read() { + reader.read().then(({ done, value }) => { + if (done) { + writer.close() + writer2.close() + return + } + writer.write(value) + writer2.write(value) + read() + }) + } + read() + + return [transformStream.readable, transformStream2.readable] +} + +export function pipeTo( + readable: ReadableStream, + writable: WritableStream, + options?: { preventClose: boolean } +) { + let resolver: () => void + const promise = new Promise((resolve) => (resolver = resolve)) + + const reader = readable.getReader() + const writer = writable.getWriter() + function process() { + reader.read().then(({ done, value }) => { + if (done) { + if (options?.preventClose) { + writer.releaseLock() + } else { + writer.close() + } + resolver() + } else { + writer.write(value) + process() + } + }) + } + process() + return promise +} + +export function pipeThrough( + readable: ReadableStream, + transformStream: TransformStream +) { + pipeTo(readable, transformStream.writable) + return transformStream.readable +} + +export function chainStreams( + streams: ReadableStream[] +): ReadableStream { + const { readable, writable } = new TransformStream() + + let promise = Promise.resolve() + for (let i = 0; i < streams.length; ++i) { + promise = promise.then(() => + pipeTo(streams[i], writable, { + preventClose: i + 1 < streams.length, + }) + ) + } + + return readable +} + +export 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(encodeText(str))) + writer.close() + + return readable +} + +export async function streamToString( + stream: ReadableStream +): Promise { + const reader = stream.getReader() + let bufferedString = '' + + while (true) { + const { done, value } = await reader.read() + + if (done) { + return bufferedString + } + + bufferedString += decodeText(value) + } +} + +export function encodeText(input: string) { + return new TextEncoder().encode(input) +} + +export function decodeText(input?: Uint8Array) { + return new TextDecoder().decode(input) +} + +export function createTransformStream({ + flush, + transform, +}: { + flush?: ( + controller: TransformStreamDefaultController + ) => Promise | void + transform?: ( + chunk: Input, + controller: TransformStreamDefaultController + ) => Promise | void +}): TransformStream { + const source = new TransformStream() + const sink = new TransformStream() + const reader = source.readable.getReader() + const writer = sink.writable.getWriter() + + const controller = { + enqueue(chunk: Output) { + writer.write(chunk) + }, + + error(reason: Error) { + writer.abort(reason) + reader.cancel() + }, + + terminate() { + writer.close() + reader.cancel() + }, + + get desiredSize() { + return writer.desiredSize + }, + } + + ;(async () => { + try { + while (true) { + const { done, value } = await reader.read() + + if (done) { + const maybePromise = flush?.(controller) + if (maybePromise) { + await maybePromise + } + writer.close() + return + } + + if (transform) { + const maybePromise = transform(value, controller) + if (maybePromise) { + await maybePromise + } + } else { + controller.enqueue(value) + } + } + } catch (err) { + writer.abort(err) + } + })() + + return { + readable: sink.readable, + writable: source.writable, + } +} + +export function createBufferedTransformStream(): TransformStream< + Uint8Array, + Uint8Array +> { + let bufferedString = '' + let pendingFlush: Promise | null = null + + const flushBuffer = (controller: TransformStreamDefaultController) => { + if (!pendingFlush) { + pendingFlush = new Promise((resolve) => { + setTimeout(() => { + controller.enqueue(encodeText(bufferedString)) + bufferedString = '' + pendingFlush = null + resolve() + }, 0) + }) + } + return pendingFlush + } + + return createTransformStream({ + transform(chunk, controller) { + bufferedString += decodeText(chunk) + flushBuffer(controller) + }, + + flush() { + if (pendingFlush) { + return pendingFlush + } + }, + }) +} + +export function createFlushEffectStream( + handleFlushEffect: () => Promise +): TransformStream { + return createTransformStream({ + async transform(chunk, controller) { + const extraChunk = await handleFlushEffect() + // those should flush together at once + controller.enqueue(encodeText(extraChunk + decodeText(chunk))) + }, + }) +} + +export async function renderToStream({ + ReactDOMServer, + element, + suffix, + dataStream, + generateStaticHTML, + flushEffectHandler, +}: { + ReactDOMServer: typeof import('react-dom/server') + element: React.ReactElement + suffix?: string + dataStream?: ReadableStream + generateStaticHTML: boolean + flushEffectHandler?: () => Promise +}): Promise> { + const closeTag = '' + const suffixUnclosed = suffix ? suffix.split(closeTag)[0] : null + const renderStream: ReadableStream & { + allReady?: Promise + } = await (ReactDOMServer as any).renderToReadableStream(element) + + if (generateStaticHTML) { + await renderStream.allReady + } + + const transforms: Array> = [ + createBufferedTransformStream(), + flushEffectHandler ? createFlushEffectStream(flushEffectHandler) : null, + suffixUnclosed != null ? createPrefixStream(suffixUnclosed) : null, + dataStream ? createInlineDataStream(dataStream) : null, + suffixUnclosed != null ? createSuffixStream(closeTag) : null, + ].filter(Boolean) as any + + return transforms.reduce( + (readable, transform) => pipeThrough(readable, transform), + renderStream + ) +} + +export function createSuffixStream( + suffix: string +): TransformStream { + return createTransformStream({ + flush(controller) { + if (suffix) { + controller.enqueue(encodeText(suffix)) + } + }, + }) +} + +export function createPrefixStream( + prefix: string +): TransformStream { + let prefixFlushed = false + let prefixPrefixFlushFinished: Promise | null = null + return createTransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk) + if (!prefixFlushed && prefix) { + prefixFlushed = true + prefixPrefixFlushFinished = new Promise((res) => { + // NOTE: streaming flush + // Enqueue prefix part before the major chunks are enqueued so that + // prefix won't be flushed too early to interrupt the data stream + setTimeout(() => { + controller.enqueue(encodeText(prefix)) + res() + }) + }) + } + }, + flush(controller) { + if (prefixPrefixFlushFinished) return prefixPrefixFlushFinished + if (!prefixFlushed && prefix) { + prefixFlushed = true + controller.enqueue(encodeText(prefix)) + } + }, + }) +} + +export function createInlineDataStream( + dataStream: ReadableStream +): TransformStream { + let dataStreamFinished: Promise | null = null + return createTransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk) + + if (!dataStreamFinished) { + const dataStreamReader = dataStream.getReader() + + // NOTE: streaming flush + // We are buffering here for the inlined data stream because the + // "shell" stream might be chunkenized again by the underlying stream + // implementation, e.g. with a specific high-water mark. To ensure it's + // the safe timing to pipe the data stream, this extra tick is + // necessary. + dataStreamFinished = new Promise((res) => + setTimeout(async () => { + try { + while (true) { + const { done, value } = await dataStreamReader.read() + if (done) { + return res() + } + controller.enqueue(value) + } + } catch (err) { + controller.error(err) + } + res() + }, 0) + ) + } + }, + flush() { + if (dataStreamFinished) { + return dataStreamFinished + } + }, + }) +} diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index e80cf62991cc77d..c13d5ff65c1283e 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -64,7 +64,17 @@ import { } from '../lib/load-custom-routes' import RenderResult from './render-result' import isError from '../lib/is-error' -import { readableStreamTee } from './web/utils' +import { + readableStreamTee, + encodeText, + decodeText, + pipeThrough, + streamFromArray, + streamToString, + chainStreams, + createBufferedTransformStream, + renderToStream, +} from './node-web-streams-helper' import { ImageConfigContext } from '../shared/lib/image-config-context' import { FlushEffectsContext } from '../shared/lib/flush-effects' @@ -1637,335 +1647,3 @@ function serializeError( statusCode: 500, } } - -function createTransformStream({ - flush, - transform, -}: { - flush?: ( - controller: TransformStreamDefaultController - ) => Promise | void - transform?: ( - chunk: Input, - controller: TransformStreamDefaultController - ) => Promise | void -}): TransformStream { - const source = new TransformStream() - const sink = new TransformStream() - const reader = source.readable.getReader() - const writer = sink.writable.getWriter() - - const controller = { - enqueue(chunk: Output) { - writer.write(chunk) - }, - - error(reason: Error) { - writer.abort(reason) - reader.cancel() - }, - - terminate() { - writer.close() - reader.cancel() - }, - - get desiredSize() { - return writer.desiredSize - }, - } - - ;(async () => { - try { - while (true) { - const { done, value } = await reader.read() - - if (done) { - const maybePromise = flush?.(controller) - if (maybePromise) { - await maybePromise - } - writer.close() - return - } - - if (transform) { - const maybePromise = transform(value, controller) - if (maybePromise) { - await maybePromise - } - } else { - controller.enqueue(value) - } - } - } catch (err) { - writer.abort(err) - } - })() - - return { - readable: sink.readable, - writable: source.writable, - } -} - -function createBufferedTransformStream(): TransformStream< - Uint8Array, - Uint8Array -> { - let bufferedString = '' - let pendingFlush: Promise | null = null - - const flushBuffer = (controller: TransformStreamDefaultController) => { - if (!pendingFlush) { - pendingFlush = new Promise((resolve) => { - setTimeout(() => { - controller.enqueue(encodeText(bufferedString)) - bufferedString = '' - pendingFlush = null - resolve() - }, 0) - }) - } - return pendingFlush - } - - return createTransformStream({ - transform(chunk, controller) { - bufferedString += decodeText(chunk) - flushBuffer(controller) - }, - - flush() { - if (pendingFlush) { - return pendingFlush - } - }, - }) -} - -function createFlushEffectStream( - handleFlushEffect: () => Promise -): TransformStream { - return createTransformStream({ - async transform(chunk, controller) { - const extraChunk = await handleFlushEffect() - // those should flush together at once - controller.enqueue(encodeText(extraChunk + decodeText(chunk))) - }, - }) -} - -async function renderToStream({ - ReactDOMServer, - element, - suffix, - dataStream, - generateStaticHTML, - flushEffectHandler, -}: { - ReactDOMServer: typeof import('react-dom/server') - element: React.ReactElement - suffix?: string - dataStream?: ReadableStream - generateStaticHTML: boolean - flushEffectHandler?: () => Promise -}): Promise> { - const closeTag = '' - const suffixUnclosed = suffix ? suffix.split(closeTag)[0] : null - const renderStream: ReadableStream & { - allReady?: Promise - } = await (ReactDOMServer as any).renderToReadableStream(element) - - if (generateStaticHTML) { - await renderStream.allReady - } - - const transforms: Array> = [ - createBufferedTransformStream(), - flushEffectHandler ? createFlushEffectStream(flushEffectHandler) : null, - suffixUnclosed != null ? createPrefixStream(suffixUnclosed) : null, - dataStream ? createInlineDataStream(dataStream) : null, - suffixUnclosed != null ? createSuffixStream(closeTag) : null, - ].filter(Boolean) as any - - return transforms.reduce( - (readable, transform) => pipeThrough(readable, transform), - renderStream - ) -} - -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(encodeText(suffix)) - } - }, - }) -} - -function createPrefixStream( - prefix: string -): TransformStream { - let prefixFlushed = false - let prefixPrefixFlushFinished: Promise | null = null - return createTransformStream({ - transform(chunk, controller) { - controller.enqueue(chunk) - if (!prefixFlushed && prefix) { - prefixFlushed = true - prefixPrefixFlushFinished = new Promise((res) => { - // NOTE: streaming flush - // Enqueue prefix part before the major chunks are enqueued so that - // prefix won't be flushed too early to interrupt the data stream - setTimeout(() => { - controller.enqueue(encodeText(prefix)) - res() - }) - }) - } - }, - flush(controller) { - if (prefixPrefixFlushFinished) return prefixPrefixFlushFinished - if (!prefixFlushed && prefix) { - prefixFlushed = true - controller.enqueue(encodeText(prefix)) - } - }, - }) -} - -function createInlineDataStream( - dataStream: ReadableStream -): TransformStream { - let dataStreamFinished: Promise | null = null - return createTransformStream({ - transform(chunk, controller) { - controller.enqueue(chunk) - - if (!dataStreamFinished) { - const dataStreamReader = dataStream.getReader() - - // NOTE: streaming flush - // We are buffering here for the inlined data stream because the - // "shell" stream might be chunkenized again by the underlying stream - // implementation, e.g. with a specific high-water mark. To ensure it's - // the safe timing to pipe the data stream, this extra tick is - // necessary. - dataStreamFinished = new Promise((res) => - setTimeout(async () => { - try { - while (true) { - const { done, value } = await dataStreamReader.read() - if (done) { - return res() - } - controller.enqueue(value) - } - } catch (err) { - controller.error(err) - } - res() - }, 0) - ) - } - }, - flush() { - if (dataStreamFinished) { - return dataStreamFinished - } - }, - }) -} - -function pipeTo( - readable: ReadableStream, - writable: WritableStream, - options?: { preventClose: boolean } -) { - let resolver: () => void - const promise = new Promise((resolve) => (resolver = resolve)) - - const reader = readable.getReader() - const writer = writable.getWriter() - function process() { - reader.read().then(({ done, value }) => { - if (done) { - if (options?.preventClose) { - writer.releaseLock() - } else { - writer.close() - } - resolver() - } else { - writer.write(value) - process() - } - }) - } - process() - return promise -} - -function pipeThrough( - readable: ReadableStream, - transformStream: TransformStream -) { - pipeTo(readable, transformStream.writable) - return transformStream.readable -} - -function chainStreams(streams: ReadableStream[]): ReadableStream { - const { readable, writable } = new TransformStream() - - let promise = Promise.resolve() - for (let i = 0; i < streams.length; ++i) { - promise = promise.then(() => - pipeTo(streams[i], writable, { - preventClose: i + 1 < streams.length, - }) - ) - } - - return readable -} - -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(encodeText(str))) - writer.close() - - return readable -} - -async function streamToString( - stream: ReadableStream -): Promise { - const reader = stream.getReader() - let bufferedString = '' - - while (true) { - const { done, value } = await reader.read() - - if (done) { - return bufferedString - } - - bufferedString += decodeText(value) - } -} diff --git a/packages/next/server/web/utils.ts b/packages/next/server/web/utils.ts index a795ec35e144e42..864721bb3ab40c1 100644 --- a/packages/next/server/web/utils.ts +++ b/packages/next/server/web/utils.ts @@ -14,32 +14,6 @@ export async function* streamToIterator( reader.releaseLock() } -export function readableStreamTee( - readable: ReadableStream -): [ReadableStream, ReadableStream] { - const transformStream = new TransformStream() - const transformStream2 = new TransformStream() - const writer = transformStream.writable.getWriter() - const writer2 = transformStream2.writable.getWriter() - - const reader = readable.getReader() - function read() { - reader.read().then(({ done, value }) => { - if (done) { - writer.close() - writer2.close() - return - } - writer.write(value) - writer2.write(value) - read() - }) - } - read() - - return [transformStream.readable, transformStream2.readable] -} - export function notImplemented(name: string, method: string): any { throw new Error( `Failed to get the '${method}' property on '${name}': the property is not implemented` From 279f47b9caf32422f2a7239c905987774e30c11c Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 18 Mar 2022 01:19:03 +0100 Subject: [PATCH 8/9] Keep fouc tags for streaming (#35417) Basically the revert change of #31187 The fouc tag are rendered in first place, and the removing fouc tags script is executed before hydration which is early enough. This will unblock the dev mode of global CSS development ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [x] Errors have helpful link attached, see `contributing.md` --- packages/next/pages/_document.tsx | 5 +---- .../react-streaming-and-server-components/test/css.js | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index 8ac37914b0026e1..adc58bf357df7cb 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -585,11 +585,8 @@ export class Head extends Component< disableOptimizedLoading, optimizeCss, optimizeFonts, - runtime, } = this.context - const hasConcurrentFeatures = !!runtime - const disableRuntimeJS = unstable_runtimeJS === false const disableJsPreload = unstable_JsPreload === false || !disableOptimizedLoading @@ -702,7 +699,7 @@ export class Head extends Component< return ( - {!hasConcurrentFeatures && this.context.isDevelopment && ( + {this.context.isDevelopment && ( <>