From f43dff987e85ea99bb36f33c896dffcb3be464dd Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 25 Nov 2021 12:51:43 +0100 Subject: [PATCH 01/10] WebRequestBasedIncomingMessage --- .../next-middleware-ssr-loader/utils.ts | 22 ++++++++++++++----- .../next-serverless-loader/page-handler.ts | 3 ++- .../loaders/next-serverless-loader/utils.ts | 20 ++++++++++++----- packages/next/server/api-utils.ts | 3 ++- packages/next/server/render.tsx | 5 +++-- packages/next/server/request-meta.ts | 18 ++++++++++----- packages/next/server/send-payload.ts | 5 +++-- .../shared/lib/i18n/detect-locale-cookie.ts | 6 ++++- .../lib/router/utils/prepare-destination.ts | 3 ++- packages/next/shared/lib/utils.ts | 3 ++- 10 files changed, 63 insertions(+), 25 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts index 76d2f6fe0f34f..8111d1a3fc7e3 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts @@ -1,8 +1,18 @@ -import { stringifyRequest } from '../../stringify-request' +import type { IncomingHttpHeaders } from 'http' -export function getStringifiedAbsolutePath(target: any, path: string) { - return stringifyRequest( - target, - target.utils.absolutify(target.rootContext, path) - ) +import { NextRequest } from '../../../../server/web/spec-extension/request' +import { toNodeHeaders } from '../../../../server/web/utils' + +export class WebRequestBasedIncomingMessage { + url: string + headers: IncomingHttpHeaders + cookies: { [key: string]: string } + method?: string + + constructor(req: NextRequest) { + this.url = req.url + this.headers = toNodeHeaders(req.headers) + this.cookies = req.cookies + this.method = req.method + } } diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts index ec162b4e5a9a1..543cb139cbb80 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts @@ -13,6 +13,7 @@ import getRouteNoAssetPath from '../../../../shared/lib/router/utils/get-route-f import { PERMANENT_REDIRECT_STATUS } from '../../../../shared/lib/constants' import RenderResult from '../../../../server/render-result' import isError from '../../../../lib/is-error' +import { WebRequestBasedIncomingMessage } from '../next-middleware-ssr-loader/utils' export function getPageHandler(ctx: ServerlessHandlerCtx) { const { @@ -58,7 +59,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { } = getUtils(ctx) async function renderReqToHTML( - req: IncomingMessage, + req: IncomingMessage | WebRequestBasedIncomingMessage, res: ServerResponse, renderMode?: 'export' | 'passthrough' | true, _renderOpts?: any, diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts index 92adf265a2788..fec20b1e6334a 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts @@ -25,6 +25,7 @@ import cookie from 'next/dist/compiled/cookie' import { TEMPORARY_REDIRECT_STATUS } from '../../../../shared/lib/constants' import { NextConfig } from '../../../../server/config' import { addRequestMeta } from '../../../../server/request-meta' +import { WebRequestBasedIncomingMessage } from '../next-middleware-ssr-loader/utils' const getCustomRouteMatcher = pathMatch(true) @@ -85,7 +86,10 @@ export function getUtils({ defaultRouteMatches = dynamicRouteMatcher(page) as ParsedUrlQuery } - function handleRewrites(req: IncomingMessage, parsedUrl: UrlWithParsedQuery) { + function handleRewrites( + req: IncomingMessage | WebRequestBasedIncomingMessage, + parsedUrl: UrlWithParsedQuery + ) { for (const rewrite of rewrites) { const matcher = getCustomRouteMatcher(rewrite.source) let params = matcher(parsedUrl.pathname) @@ -150,7 +154,10 @@ export function getUtils({ return parsedUrl } - function handleBasePath(req: IncomingMessage, parsedUrl: UrlWithParsedQuery) { + function handleBasePath( + req: IncomingMessage | WebRequestBasedIncomingMessage, + parsedUrl: UrlWithParsedQuery + ) { // always strip the basePath if configured since it is required req.url = req.url!.replace(new RegExp(`^${basePath}`), '') || '/' parsedUrl.pathname = @@ -158,7 +165,7 @@ export function getUtils({ } function getParamsFromRouteMatches( - req: IncomingMessage, + req: IncomingMessage | WebRequestBasedIncomingMessage, renderOpts?: any, detectedLocale?: string ) { @@ -269,7 +276,10 @@ export function getUtils({ return pathname } - function normalizeVercelUrl(req: IncomingMessage, trustQuery: boolean) { + function normalizeVercelUrl( + req: IncomingMessage | WebRequestBasedIncomingMessage, + trustQuery: boolean + ) { // make sure to normalize req.url on Vercel to strip dynamic params // from the query which are added during routing if (pageIsDynamic && trustQuery && defaultRouteRegex) { @@ -345,7 +355,7 @@ export function getUtils({ } function handleLocale( - req: IncomingMessage, + req: IncomingMessage | WebRequestBasedIncomingMessage, res: ServerResponse, parsedUrl: UrlWithParsedQuery, routeNoAssetPath: string, diff --git a/packages/next/server/api-utils.ts b/packages/next/server/api-utils.ts index d44c6fc8db9ed..3d91cbf287467 100644 --- a/packages/next/server/api-utils.ts +++ b/packages/next/server/api-utils.ts @@ -10,6 +10,7 @@ import { sendEtagResponse } from './send-payload' import generateETag from 'etag' import isError from '../lib/is-error' import { interopDefault } from '../lib/interop-default' +import { WebRequestBasedIncomingMessage } from '../build/webpack/loaders/next-middleware-ssr-loader/utils' export type NextApiRequestCookies = { [key: string]: string } export type NextApiRequestQuery = { [key: string]: string | string[] } @@ -338,7 +339,7 @@ export const SYMBOL_PREVIEW_DATA = Symbol(COOKIE_NAME_PRERENDER_DATA) const SYMBOL_CLEARED_COOKIES = Symbol(COOKIE_NAME_PRERENDER_BYPASS) export function tryGetPreviewData( - req: IncomingMessage, + req: IncomingMessage | WebRequestBasedIncomingMessage, res: ServerResponse, options: __ApiPreviewProps ): PreviewData { diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index e575c00527daf..f41774c5fbb07 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -62,6 +62,7 @@ import { import { DomainLocale } from './config' import RenderResult, { NodeWritablePiper } from './render-result' import isError from '../lib/is-error' +import { WebRequestBasedIncomingMessage } from '../build/webpack/loaders/next-middleware-ssr-loader/utils' let Writable: typeof import('stream').Writable let Buffer: typeof import('buffer').Buffer @@ -233,7 +234,7 @@ const invalidKeysMsg = (methodName: string, invalidKeys: string[]) => { function checkRedirectValues( redirect: Redirect, - req: IncomingMessage, + req: IncomingMessage | WebRequestBasedIncomingMessage, method: 'getStaticProps' | 'getServerSideProps' ) { const { destination, permanent, statusCode, basePath } = redirect @@ -323,7 +324,7 @@ function createServerComponentRenderer( } export async function renderToHTML( - req: IncomingMessage, + req: IncomingMessage | WebRequestBasedIncomingMessage, res: ServerResponse, pathname: string, query: NextParsedUrlQuery, diff --git a/packages/next/server/request-meta.ts b/packages/next/server/request-meta.ts index 79866ad8913b7..a9268476af2b9 100644 --- a/packages/next/server/request-meta.ts +++ b/packages/next/server/request-meta.ts @@ -2,12 +2,17 @@ import type { ParsedUrlQuery } from 'querystring' import type { IncomingMessage } from 'http' import type { UrlWithParsedQuery } from 'url' +import { WebRequestBasedIncomingMessage } from '../build/webpack/loaders/next-middleware-ssr-loader/utils' const NEXT_REQUEST_META = Symbol('NextRequestMeta') interface NextIncomingMessage extends IncomingMessage { [NEXT_REQUEST_META]?: RequestMeta } +interface WebRequestBasedNextIncomingMessage + extends WebRequestBasedIncomingMessage { + [NEXT_REQUEST_META]?: RequestMeta +} interface RequestMeta { __NEXT_INIT_QUERY?: ParsedUrlQuery @@ -21,28 +26,31 @@ interface RequestMeta { } export function getRequestMeta( - req: NextIncomingMessage, + req: NextIncomingMessage | WebRequestBasedNextIncomingMessage, key?: undefined ): RequestMeta export function getRequestMeta( - req: NextIncomingMessage, + req: NextIncomingMessage | WebRequestBasedNextIncomingMessage, key: K ): RequestMeta[K] export function getRequestMeta( - req: NextIncomingMessage, + req: NextIncomingMessage | WebRequestBasedNextIncomingMessage, key?: K ): RequestMeta | RequestMeta[K] { const meta = req[NEXT_REQUEST_META] || {} return typeof key === 'string' ? meta[key] : meta } -export function setRequestMeta(req: NextIncomingMessage, meta: RequestMeta) { +export function setRequestMeta( + req: NextIncomingMessage | WebRequestBasedNextIncomingMessage, + meta: RequestMeta +) { req[NEXT_REQUEST_META] = meta return getRequestMeta(req) } export function addRequestMeta( - request: NextIncomingMessage, + request: NextIncomingMessage | WebRequestBasedNextIncomingMessage, key: K, value: RequestMeta[K] ) { diff --git a/packages/next/server/send-payload.ts b/packages/next/server/send-payload.ts index 9d9b9dc7a4c92..0b481707dabab 100644 --- a/packages/next/server/send-payload.ts +++ b/packages/next/server/send-payload.ts @@ -3,6 +3,7 @@ import { isResSent } from '../shared/lib/utils' import generateETag from 'etag' import fresh from 'next/dist/compiled/fresh' import RenderResult from './render-result' +import { WebRequestBasedIncomingMessage } from '../build/webpack/loaders/next-middleware-ssr-loader/utils' export type PayloadOptions = | { private: true } @@ -45,7 +46,7 @@ export async function sendRenderResult({ poweredByHeader, options, }: { - req: IncomingMessage + req: IncomingMessage | WebRequestBasedIncomingMessage res: ServerResponse result: RenderResult type: 'html' | 'json' @@ -95,7 +96,7 @@ export async function sendRenderResult({ } export function sendEtagResponse( - req: IncomingMessage, + req: IncomingMessage | WebRequestBasedIncomingMessage, res: ServerResponse, etag: string | undefined ): boolean { diff --git a/packages/next/shared/lib/i18n/detect-locale-cookie.ts b/packages/next/shared/lib/i18n/detect-locale-cookie.ts index 9089f438eb0d0..542e75ff6a2e4 100644 --- a/packages/next/shared/lib/i18n/detect-locale-cookie.ts +++ b/packages/next/shared/lib/i18n/detect-locale-cookie.ts @@ -1,6 +1,10 @@ import { IncomingMessage } from 'http' +import { WebRequestBasedIncomingMessage } from '../../../build/webpack/loaders/next-middleware-ssr-loader/utils' -export function detectLocaleCookie(req: IncomingMessage, locales: string[]) { +export function detectLocaleCookie( + req: IncomingMessage | WebRequestBasedIncomingMessage, + locales: string[] +) { const { NEXT_LOCALE } = (req as any).cookies || {} return NEXT_LOCALE ? locales.find( diff --git a/packages/next/shared/lib/router/utils/prepare-destination.ts b/packages/next/shared/lib/router/utils/prepare-destination.ts index c09fb384dfe4d..23eb0b250f3f7 100644 --- a/packages/next/shared/lib/router/utils/prepare-destination.ts +++ b/packages/next/shared/lib/router/utils/prepare-destination.ts @@ -5,9 +5,10 @@ import type { Params } from '../../../../server/router' import type { RouteHas } from '../../../../lib/load-custom-routes' import { compile, pathToRegexp } from 'next/dist/compiled/path-to-regexp' import { parseUrl } from './parse-url' +import { WebRequestBasedIncomingMessage } from '../../../../build/webpack/loaders/next-middleware-ssr-loader/utils' export function matchHas( - req: IncomingMessage, + req: IncomingMessage | WebRequestBasedIncomingMessage, has: RouteHas[], query: Params ): false | Params { diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts index a74499fdba9b8..27f99b86a8889 100644 --- a/packages/next/shared/lib/utils.ts +++ b/packages/next/shared/lib/utils.ts @@ -9,6 +9,7 @@ import type { ParsedUrlQuery } from 'querystring' import type { PreviewData } from 'next/types' import type { UrlObject } from 'url' import { createContext } from 'react' +import { WebRequestBasedIncomingMessage } from '../../build/webpack/loaders/next-middleware-ssr-loader/utils' export type NextComponentType< C extends BaseContext = NextPageContext, @@ -121,7 +122,7 @@ export interface NextPageContext { /** * `HTTP` request object. */ - req?: IncomingMessage + req?: IncomingMessage | WebRequestBasedIncomingMessage /** * `HTTP` response object. */ From 05f9786d42256f4461fd74a787223c4534780f85 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 25 Nov 2021 17:17:59 +0100 Subject: [PATCH 02/10] add adapter --- .../next-middleware-ssr-loader/render.ts | 62 +++++++++++++++++++ .../next-middleware-ssr-loader/utils.ts | 46 ++++++++++++++ .../next-serverless-loader/page-handler.ts | 7 ++- .../loaders/next-serverless-loader/utils.ts | 7 ++- packages/next/server/api-utils.ts | 7 ++- packages/next/server/dev/hot-reloader.ts | 13 +++- packages/next/server/render-result.ts | 5 +- packages/next/server/render.tsx | 42 +++++++------ packages/next/server/send-payload.ts | 11 ++-- packages/next/shared/lib/utils.ts | 13 ++-- 10 files changed, 178 insertions(+), 35 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts index 122bd32179441..b3ae2ba2ff7c0 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts @@ -1,7 +1,19 @@ +// import devalue from 'next/dist/compiled/devalue' +import escapeRegexp from 'next/dist/compiled/escape-string-regexp' + import { NextRequest } from '../../../../server/web/spec-extension/request' import { renderToHTML } from '../../../../server/web/render' import RenderResult from '../../../../server/render-result' +import { getPageHandler } from '../next-serverless-loader/page-handler' +import { isDynamicRoute } from '../../../../shared/lib/router/utils' +import { __ApiPreviewProps } from '../../../../server/api-utils' + +import { + WebRequestBasedIncomingMessage, + WebResponseBasedServerResponse, +} from './utils' + export function getRender({ App, Document, @@ -23,6 +35,12 @@ export function getRender({ isServerComponent: boolean restRenderOpts: any }) { + const { page, buildId, previewProps, runtimeConfig } = restRenderOpts + + const pageIsDynamicRoute = isDynamicRoute(page) + const escapedBuildId = escapeRegexp(buildId) + const encodedPreviewProps = JSON.parse(previewProps) as __ApiPreviewProps + return async function render(request: NextRequest) { const url = request.nextUrl const { pathname, searchParams } = url @@ -67,6 +85,50 @@ export function getRender({ ComponentMod: null, } + const pageHandler = getPageHandler({ + pageModule: pageMod, + pageComponent: pageMod.default, + pageConfig: pageMod.config || {}, + appModule: App, + documentModule: { default: Document }, + errorModule: errorMod, + // notFoundModule: ${ + // absolute404Path + // ? `require(${stringifyRequest(this, absolute404Path)})` + // : undefined + // }, + pageGetStaticProps: pageMod.getStaticProps, + pageGetStaticPaths: pageMod.getStaticPaths, + pageGetServerSideProps: pageMod.getServerSideProps, + + assetPrefix: restRenderOpts.assetPrefix, + canonicalBase: restRenderOpts.canonicalBase, + generateEtags: restRenderOpts.generateEtags || false, + poweredByHeader: restRenderOpts.poweredByHeader || false, + + runtimeConfig, + buildManifest, + reactLoadableManifest, + + // rewrites: combinedRewrites, + rewrites: [], + i18n: restRenderOpts.i18n, + page, + buildId, + escapedBuildId: escapedBuildId, + basePath: restRenderOpts.basePath, + pageIsDynamic: pageIsDynamicRoute, + encodedPreviewProps, + distDir: restRenderOpts.distDir, + }) + + console.log( + await pageHandler.renderReqToHTML( + new WebRequestBasedIncomingMessage(request), + new WebResponseBasedServerResponse() + ) + ) + const transformStream = new TransformStream() const writer = transformStream.writable.getWriter() const encoder = new TextEncoder() diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts index 8111d1a3fc7e3..9a6a1e9ec3cab 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts @@ -16,3 +16,49 @@ export class WebRequestBasedIncomingMessage { this.method = req.method } } + +export class WebResponseBasedServerResponse { + private headers: { [name: string]: number | string | string[] } = {} + + statusCode: number = 200 + finished: boolean = false // TODO + headersSent: boolean = false // TODO + body: [] = [] // TODO + + constructor() {} + + setHeader(name: string, value: number | string | string[]) { + this.headers[name] = value + } + getHeader(name: string): number | string | string[] | undefined { + return this.headers[name] + } + getHeaders() { + return this.headers + } + getHeaderNames() { + return Object.keys(this.headers) + } + hasHeader(name: string) { + return this.headers.hasOwnProperty(name) + } + removeHeader(name: string) { + delete this.headers[name] + } + + // Not supported in Web Streams, left to empty here. + cork() {} + uncork() {} + + on(_name: string, _callback: () => void) {} + removeListener(_name: string, _callback: () => void) {} + + write(chunk?: string) { + // TODO + console.log('write ->', chunk) + } + end(chunk?: string) { + // TODO + console.log('end ->', chunk) + } +} diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts index 543cb139cbb80..88de7bf2d9849 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts @@ -13,7 +13,10 @@ import getRouteNoAssetPath from '../../../../shared/lib/router/utils/get-route-f import { PERMANENT_REDIRECT_STATUS } from '../../../../shared/lib/constants' import RenderResult from '../../../../server/render-result' import isError from '../../../../lib/is-error' -import { WebRequestBasedIncomingMessage } from '../next-middleware-ssr-loader/utils' +import { + WebRequestBasedIncomingMessage, + WebResponseBasedServerResponse, +} from '../next-middleware-ssr-loader/utils' export function getPageHandler(ctx: ServerlessHandlerCtx) { const { @@ -60,7 +63,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { async function renderReqToHTML( req: IncomingMessage | WebRequestBasedIncomingMessage, - res: ServerResponse, + res: ServerResponse | WebResponseBasedServerResponse, renderMode?: 'export' | 'passthrough' | true, _renderOpts?: any, _params?: any diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts index fec20b1e6334a..b5ad0cabdeff5 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts @@ -25,7 +25,10 @@ import cookie from 'next/dist/compiled/cookie' import { TEMPORARY_REDIRECT_STATUS } from '../../../../shared/lib/constants' import { NextConfig } from '../../../../server/config' import { addRequestMeta } from '../../../../server/request-meta' -import { WebRequestBasedIncomingMessage } from '../next-middleware-ssr-loader/utils' +import { + WebRequestBasedIncomingMessage, + WebResponseBasedServerResponse, +} from '../next-middleware-ssr-loader/utils' const getCustomRouteMatcher = pathMatch(true) @@ -356,7 +359,7 @@ export function getUtils({ function handleLocale( req: IncomingMessage | WebRequestBasedIncomingMessage, - res: ServerResponse, + res: ServerResponse | WebResponseBasedServerResponse, parsedUrl: UrlWithParsedQuery, routeNoAssetPath: string, shouldNotRedirect: boolean diff --git a/packages/next/server/api-utils.ts b/packages/next/server/api-utils.ts index 3d91cbf287467..20116e7019e14 100644 --- a/packages/next/server/api-utils.ts +++ b/packages/next/server/api-utils.ts @@ -10,7 +10,10 @@ import { sendEtagResponse } from './send-payload' import generateETag from 'etag' import isError from '../lib/is-error' import { interopDefault } from '../lib/interop-default' -import { WebRequestBasedIncomingMessage } from '../build/webpack/loaders/next-middleware-ssr-loader/utils' +import { + WebRequestBasedIncomingMessage, + WebResponseBasedServerResponse, +} from '../build/webpack/loaders/next-middleware-ssr-loader/utils' export type NextApiRequestCookies = { [key: string]: string } export type NextApiRequestQuery = { [key: string]: string | string[] } @@ -340,7 +343,7 @@ const SYMBOL_CLEARED_COOKIES = Symbol(COOKIE_NAME_PRERENDER_BYPASS) export function tryGetPreviewData( req: IncomingMessage | WebRequestBasedIncomingMessage, - res: ServerResponse, + res: ServerResponse | WebResponseBasedServerResponse, options: __ApiPreviewProps ): PreviewData { // Read cached preview data if present diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index f4150fcba925d..f6fc3ddfa3b8e 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -522,6 +522,10 @@ export default class HotReloader { } } else if (isServerWebCompilation) { if (!isReserved) { + const hasRuntimeConfig = + Object.keys(this.config.publicRuntimeConfig).length > 0 || + Object.keys(this.config.serverRuntimeConfig).length > 0 + entrypoints[bundlePath] = finalizeEntrypoint({ name: '[name].js', value: `next-middleware-ssr-loader?${stringify({ @@ -539,7 +543,14 @@ export default class HotReloader { poweredByHeader: this.config.poweredByHeader, canonicalBase: this.config.amp.canonicalBase, i18n: this.config.i18n, - previewProps: this.previewProps, + previewProps: JSON.stringify(this.previewProps), + runtimeConfig: hasRuntimeConfig + ? JSON.stringify({ + publicRuntimeConfig: this.config.publicRuntimeConfig, + serverRuntimeConfig: this.config.serverRuntimeConfig, + }) + : '', + distDir: this.config.distDir, } as any)}!`, isServer: false, isServerWeb: true, diff --git a/packages/next/server/render-result.ts b/packages/next/server/render-result.ts index 925f6c1680d5b..428f5c364d17b 100644 --- a/packages/next/server/render-result.ts +++ b/packages/next/server/render-result.ts @@ -1,8 +1,9 @@ import type { ServerResponse } from 'http' import type { Writable } from 'stream' +import { WebResponseBasedServerResponse } from '../build/webpack/loaders/next-middleware-ssr-loader/utils' export type NodeWritablePiper = ( - res: Writable, + res: Writable | WebResponseBasedServerResponse, next: (err?: Error) => void ) => void @@ -22,7 +23,7 @@ export default class RenderResult { return this._result } - pipe(res: ServerResponse): Promise { + pipe(res: ServerResponse | WebResponseBasedServerResponse): Promise { if (typeof this._result === 'string') { throw new Error( 'invariant: static responses cannot be piped. This is a bug in Next.js' diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index f41774c5fbb07..f0f8db9c41446 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -62,7 +62,10 @@ import { import { DomainLocale } from './config' import RenderResult, { NodeWritablePiper } from './render-result' import isError from '../lib/is-error' -import { WebRequestBasedIncomingMessage } from '../build/webpack/loaders/next-middleware-ssr-loader/utils' +import { + WebRequestBasedIncomingMessage, + WebResponseBasedServerResponse, +} from '../build/webpack/loaders/next-middleware-ssr-loader/utils' let Writable: typeof import('stream').Writable let Buffer: typeof import('buffer').Buffer @@ -325,7 +328,7 @@ function createServerComponentRenderer( export async function renderToHTML( req: IncomingMessage | WebRequestBasedIncomingMessage, - res: ServerResponse, + res: ServerResponse | WebResponseBasedServerResponse, pathname: string, query: NextParsedUrlQuery, renderOpts: RenderOpts @@ -854,22 +857,25 @@ export async function renderToHTML( let resOrProxy = res let deferredContent = false if (process.env.NODE_ENV !== 'production') { - resOrProxy = new Proxy(res, { - get: function (obj, prop, receiver) { - if (!canAccessRes) { - const message = - `You should not access 'res' after getServerSideProps resolves.` + - `\nRead more: https://nextjs.org/docs/messages/gssp-no-mutating-res` - - if (deferredContent) { - throw new Error(message) - } else { - warn(message) + resOrProxy = new Proxy( + res, + { + get: function (obj, prop, receiver) { + if (!canAccessRes) { + const message = + `You should not access 'res' after getServerSideProps resolves.` + + `\nRead more: https://nextjs.org/docs/messages/gssp-no-mutating-res` + + if (deferredContent) { + throw new Error(message) + } else { + warn(message) + } } - } - return Reflect.get(obj, prop, receiver) - }, - }) + return Reflect.get(obj, prop, receiver) + }, + } + ) } try { @@ -1433,7 +1439,7 @@ function renderToNodeStream( return new Promise((resolve, reject) => { let underlyingStream: { resolve: (error?: Error) => void - writable: WritableType + writable: WritableType | WebResponseBasedServerResponse queuedCallbacks: Array<() => void> } | null = null diff --git a/packages/next/server/send-payload.ts b/packages/next/server/send-payload.ts index 0b481707dabab..65bfea91e6e17 100644 --- a/packages/next/server/send-payload.ts +++ b/packages/next/server/send-payload.ts @@ -3,7 +3,10 @@ import { isResSent } from '../shared/lib/utils' import generateETag from 'etag' import fresh from 'next/dist/compiled/fresh' import RenderResult from './render-result' -import { WebRequestBasedIncomingMessage } from '../build/webpack/loaders/next-middleware-ssr-loader/utils' +import { + WebRequestBasedIncomingMessage, + WebResponseBasedServerResponse, +} from '../build/webpack/loaders/next-middleware-ssr-loader/utils' export type PayloadOptions = | { private: true } @@ -11,7 +14,7 @@ export type PayloadOptions = | { private: boolean; stateful: false; revalidate: number | false } export function setRevalidateHeaders( - res: ServerResponse, + res: ServerResponse | WebResponseBasedServerResponse, options: PayloadOptions ) { if (options.private || options.stateful) { @@ -47,7 +50,7 @@ export async function sendRenderResult({ options, }: { req: IncomingMessage | WebRequestBasedIncomingMessage - res: ServerResponse + res: ServerResponse | WebResponseBasedServerResponse result: RenderResult type: 'html' | 'json' generateEtags: boolean @@ -97,7 +100,7 @@ export async function sendRenderResult({ export function sendEtagResponse( req: IncomingMessage | WebRequestBasedIncomingMessage, - res: ServerResponse, + res: ServerResponse | WebResponseBasedServerResponse, etag: string | undefined ): boolean { if (etag) { diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts index 27f99b86a8889..37449682433f2 100644 --- a/packages/next/shared/lib/utils.ts +++ b/packages/next/shared/lib/utils.ts @@ -9,7 +9,10 @@ import type { ParsedUrlQuery } from 'querystring' import type { PreviewData } from 'next/types' import type { UrlObject } from 'url' import { createContext } from 'react' -import { WebRequestBasedIncomingMessage } from '../../build/webpack/loaders/next-middleware-ssr-loader/utils' +import { + WebRequestBasedIncomingMessage, + WebResponseBasedServerResponse, +} from '../../build/webpack/loaders/next-middleware-ssr-loader/utils' export type NextComponentType< C extends BaseContext = NextPageContext, @@ -81,7 +84,7 @@ export type RenderPage = ( ) => DocumentInitialProps | Promise export type BaseContext = { - res?: ServerResponse + res?: ServerResponse | WebResponseBasedServerResponse [k: string]: any } @@ -126,7 +129,7 @@ export interface NextPageContext { /** * `HTTP` response object. */ - res?: ServerResponse + res?: ServerResponse | WebResponseBasedServerResponse /** * Path section of `URL`. */ @@ -335,7 +338,9 @@ export function getDisplayName

(Component: ComponentType

) { : Component.displayName || Component.name || 'Unknown' } -export function isResSent(res: ServerResponse) { +export function isResSent( + res: ServerResponse | WebResponseBasedServerResponse +) { return res.finished || res.headersSent } From 288bb8137e96607c705879d4ac5e28ee41e9ae1e Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 25 Nov 2021 19:47:13 +0100 Subject: [PATCH 03/10] clean up loader logic --- .../next-middleware-ssr-loader/index.ts | 2 +- .../next-middleware-ssr-loader/render.ts | 63 +++++-------------- .../next-middleware-ssr-loader/utils.ts | 23 +++---- packages/next/server/render.tsx | 5 +- 4 files changed, 27 insertions(+), 66 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts index 8d7d94a02f32e..e4826c61b0f88 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts @@ -1,6 +1,6 @@ import { stringifyRequest } from '../../stringify-request' -export default async function middlewareRSCLoader(this: any) { +export default async function middlewareSSRLoader(this: any) { const { absolutePagePath, absoluteAppPath, diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts index b3ae2ba2ff7c0..124b197c47a7a 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts @@ -1,10 +1,6 @@ -// import devalue from 'next/dist/compiled/devalue' import escapeRegexp from 'next/dist/compiled/escape-string-regexp' import { NextRequest } from '../../../../server/web/spec-extension/request' -import { renderToHTML } from '../../../../server/web/render' -import RenderResult from '../../../../server/render-result' - import { getPageHandler } from '../next-serverless-loader/page-handler' import { isDynamicRoute } from '../../../../shared/lib/router/utils' import { __ApiPreviewProps } from '../../../../server/api-utils' @@ -59,7 +55,6 @@ export function getRender({ : false delete query.__flight__ - const req = { url: pathname } const renderOpts = { ...restRenderOpts, // Locales are not supported yet. @@ -122,56 +117,28 @@ export function getRender({ distDir: restRenderOpts.distDir, }) - console.log( - await pageHandler.renderReqToHTML( - new WebRequestBasedIncomingMessage(request), - new WebResponseBasedServerResponse() - ) + const result = await pageHandler.renderReqToHTML( + new WebRequestBasedIncomingMessage(request), + new WebResponseBasedServerResponse(), + 'passthrough', + { + supportsDynamicHTML: true, + ...renderOpts, + } ) const transformStream = new TransformStream() const writer = transformStream.writable.getWriter() const encoder = new TextEncoder() - let result: RenderResult | null - try { - result = await renderToHTML( - req as any, - {} as any, - pathname, - query, - renderOpts - ) - } catch (err: any) { - const errorRes = { statusCode: 500, err } - try { - result = await renderToHTML( - req as any, - errorRes as any, - '/_error', - query, - { - ...renderOpts, - Component: errorMod.default, - getStaticProps: errorMod.getStaticProps, - getServerSideProps: errorMod.getServerSideProps, - getStaticPaths: errorMod.getStaticPaths, - } - ) - } catch (err2: any) { - return new Response( - ( - err2 || 'An error occurred while rendering ' + pathname + '.' - ).toString(), - { - status: 500, - headers: { 'x-middleware-ssr': '1' }, - } - ) - } + if (typeof result === 'string') { + return new Response(result, { + headers: { 'x-middleware-ssr': '1' }, + status: 200, + }) } - if (!result) { + if (!result || !result.html) { return new Response( 'An error occurred while rendering ' + pathname + '.', { @@ -181,7 +148,7 @@ export function getRender({ ) } - result.pipe({ + result.html.pipe({ write: (str: string) => writer.write(encoder.encode(str)), end: () => writer.close(), // Not implemented: cork/uncork/on/removeListener diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts index 9a6a1e9ec3cab..c0d5289f851da 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts @@ -21,11 +21,11 @@ export class WebResponseBasedServerResponse { private headers: { [name: string]: number | string | string[] } = {} statusCode: number = 200 - finished: boolean = false // TODO - headersSent: boolean = false // TODO - body: [] = [] // TODO - constructor() {} + // FIXME + finished: boolean = false + headersSent: boolean = false + body: [] = [] setHeader(name: string, value: number | string | string[]) { this.headers[name] = value @@ -46,19 +46,12 @@ export class WebResponseBasedServerResponse { delete this.headers[name] } - // Not supported in Web Streams, left to empty here. + // Leave the implementation empty here because they're not currently used. + // The underlying stream is abstracted out with `RenderResult`. cork() {} uncork() {} - on(_name: string, _callback: () => void) {} removeListener(_name: string, _callback: () => void) {} - - write(chunk?: string) { - // TODO - console.log('write ->', chunk) - } - end(chunk?: string) { - // TODO - console.log('end ->', chunk) - } + write(_chunk?: string) {} + end(_chunk?: string) {} } diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index f0f8db9c41446..1732a94700594 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -423,7 +423,8 @@ export async function renderToHTML( !hasPageGetInitialProps && defaultAppGetInitialProps && !isSSG && - !getServerSideProps + !getServerSideProps && + !concurrentFeatures for (const methodName of [ 'getStaticProps', @@ -1015,7 +1016,7 @@ export async function renderToHTML( ) startWriting( renderToReadableStream( - , + , serverComponentManifest ).getReader() ) From c0166323c6d0ea154c9a54073943ada3eb87b230 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 25 Nov 2021 19:48:17 +0100 Subject: [PATCH 04/10] fix test for dynamic routes --- .../app/pages/routes/[dynamic].server.js | 4 ++-- .../react-streaming-and-server-components/test/index.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 eee586678967b..eb78a09f969d6 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,3 +1,3 @@ -export default function Pid() { - return '[pid]' // TODO: display based on query +export default function Pid({ router }) { + return router.query.dynamic } 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 d801c484e5ff3..2f582352d4a2c 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 @@ -242,8 +242,8 @@ async function runBasicTests(context, env) { expect(homeHTML).toContain('env_var_test') expect(homeHTML).toContain('foo.client') - expect(dynamicRouteHTML1).toContain('[pid]') - expect(dynamicRouteHTML2).toContain('[pid]') + expect(dynamicRouteHTML1).toContain('dynamic1') + expect(dynamicRouteHTML2).toContain('dynamic2') expect(path404HTML).toContain('custom-404-page') // in dev mode: custom error page is still using default _error From 8e36a88ed10a2ab8ec0e9666911a6ad3bbdb84cf Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 25 Nov 2021 19:59:37 +0100 Subject: [PATCH 05/10] move the util --- .../next-middleware-ssr-loader/render.ts | 11 ++-- .../next-serverless-loader/page-handler.ts | 10 ++-- .../loaders/next-serverless-loader/utils.ts | 18 +++---- packages/next/server/api-utils.ts | 10 ++-- packages/next/server/render-result.ts | 6 +-- packages/next/server/render.tsx | 50 +++++++++---------- packages/next/server/request-meta.ts | 6 +-- packages/next/server/send-payload.ts | 15 +++--- .../utils.ts => server/web/http-adapter.ts} | 8 +-- .../shared/lib/i18n/detect-locale-cookie.ts | 4 +- .../lib/router/utils/prepare-destination.ts | 4 +- packages/next/shared/lib/utils.ts | 16 +++--- 12 files changed, 74 insertions(+), 84 deletions(-) rename packages/next/{build/webpack/loaders/next-middleware-ssr-loader/utils.ts => server/web/http-adapter.ts} (84%) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts index 124b197c47a7a..52d908193e55f 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts @@ -4,11 +4,10 @@ import { NextRequest } from '../../../../server/web/spec-extension/request' import { getPageHandler } from '../next-serverless-loader/page-handler' import { isDynamicRoute } from '../../../../shared/lib/router/utils' import { __ApiPreviewProps } from '../../../../server/api-utils' - import { - WebRequestBasedIncomingMessage, - WebResponseBasedServerResponse, -} from './utils' + WebIncomingMessage, + WebServerResponse, +} from '../../../../server/web/http-adapter' export function getRender({ App, @@ -118,8 +117,8 @@ export function getRender({ }) const result = await pageHandler.renderReqToHTML( - new WebRequestBasedIncomingMessage(request), - new WebResponseBasedServerResponse(), + new WebIncomingMessage(request), + new WebServerResponse(), 'passthrough', { supportsDynamicHTML: true, diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts index 88de7bf2d9849..b2f4a7398c3a8 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts @@ -14,9 +14,9 @@ import { PERMANENT_REDIRECT_STATUS } from '../../../../shared/lib/constants' import RenderResult from '../../../../server/render-result' import isError from '../../../../lib/is-error' import { - WebRequestBasedIncomingMessage, - WebResponseBasedServerResponse, -} from '../next-middleware-ssr-loader/utils' + WebIncomingMessage, + WebServerResponse, +} from '../../../../server/web/http-adapter' export function getPageHandler(ctx: ServerlessHandlerCtx) { const { @@ -62,8 +62,8 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { } = getUtils(ctx) async function renderReqToHTML( - req: IncomingMessage | WebRequestBasedIncomingMessage, - res: ServerResponse | WebResponseBasedServerResponse, + req: IncomingMessage | WebIncomingMessage, + res: ServerResponse | WebServerResponse, renderMode?: 'export' | 'passthrough' | true, _renderOpts?: any, _params?: any diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts index b5ad0cabdeff5..4f221bcdbc9d9 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts @@ -26,9 +26,9 @@ import { TEMPORARY_REDIRECT_STATUS } from '../../../../shared/lib/constants' import { NextConfig } from '../../../../server/config' import { addRequestMeta } from '../../../../server/request-meta' import { - WebRequestBasedIncomingMessage, - WebResponseBasedServerResponse, -} from '../next-middleware-ssr-loader/utils' + WebIncomingMessage, + WebServerResponse, +} from '../../../../server/web/http-adapter' const getCustomRouteMatcher = pathMatch(true) @@ -90,7 +90,7 @@ export function getUtils({ } function handleRewrites( - req: IncomingMessage | WebRequestBasedIncomingMessage, + req: IncomingMessage | WebIncomingMessage, parsedUrl: UrlWithParsedQuery ) { for (const rewrite of rewrites) { @@ -158,7 +158,7 @@ export function getUtils({ } function handleBasePath( - req: IncomingMessage | WebRequestBasedIncomingMessage, + req: IncomingMessage | WebIncomingMessage, parsedUrl: UrlWithParsedQuery ) { // always strip the basePath if configured since it is required @@ -168,7 +168,7 @@ export function getUtils({ } function getParamsFromRouteMatches( - req: IncomingMessage | WebRequestBasedIncomingMessage, + req: IncomingMessage | WebIncomingMessage, renderOpts?: any, detectedLocale?: string ) { @@ -280,7 +280,7 @@ export function getUtils({ } function normalizeVercelUrl( - req: IncomingMessage | WebRequestBasedIncomingMessage, + req: IncomingMessage | WebIncomingMessage, trustQuery: boolean ) { // make sure to normalize req.url on Vercel to strip dynamic params @@ -358,8 +358,8 @@ export function getUtils({ } function handleLocale( - req: IncomingMessage | WebRequestBasedIncomingMessage, - res: ServerResponse | WebResponseBasedServerResponse, + req: IncomingMessage | WebIncomingMessage, + res: ServerResponse | WebServerResponse, parsedUrl: UrlWithParsedQuery, routeNoAssetPath: string, shouldNotRedirect: boolean diff --git a/packages/next/server/api-utils.ts b/packages/next/server/api-utils.ts index 20116e7019e14..241fe71211f2c 100644 --- a/packages/next/server/api-utils.ts +++ b/packages/next/server/api-utils.ts @@ -11,9 +11,9 @@ import generateETag from 'etag' import isError from '../lib/is-error' import { interopDefault } from '../lib/interop-default' import { - WebRequestBasedIncomingMessage, - WebResponseBasedServerResponse, -} from '../build/webpack/loaders/next-middleware-ssr-loader/utils' + WebIncomingMessage, + WebServerResponse, +} from '../server/web/http-adapter' export type NextApiRequestCookies = { [key: string]: string } export type NextApiRequestQuery = { [key: string]: string | string[] } @@ -342,8 +342,8 @@ export const SYMBOL_PREVIEW_DATA = Symbol(COOKIE_NAME_PRERENDER_DATA) const SYMBOL_CLEARED_COOKIES = Symbol(COOKIE_NAME_PRERENDER_BYPASS) export function tryGetPreviewData( - req: IncomingMessage | WebRequestBasedIncomingMessage, - res: ServerResponse | WebResponseBasedServerResponse, + req: IncomingMessage | WebIncomingMessage, + res: ServerResponse | WebServerResponse, options: __ApiPreviewProps ): PreviewData { // Read cached preview data if present diff --git a/packages/next/server/render-result.ts b/packages/next/server/render-result.ts index 428f5c364d17b..c80bab1fd8a91 100644 --- a/packages/next/server/render-result.ts +++ b/packages/next/server/render-result.ts @@ -1,9 +1,9 @@ import type { ServerResponse } from 'http' import type { Writable } from 'stream' -import { WebResponseBasedServerResponse } from '../build/webpack/loaders/next-middleware-ssr-loader/utils' +import { WebServerResponse } from '../build/webpack/loaders/next-middleware-ssr-loader/utils' export type NodeWritablePiper = ( - res: Writable | WebResponseBasedServerResponse, + res: Writable | WebServerResponse, next: (err?: Error) => void ) => void @@ -23,7 +23,7 @@ export default class RenderResult { return this._result } - pipe(res: ServerResponse | WebResponseBasedServerResponse): Promise { + pipe(res: ServerResponse | WebServerResponse): Promise { if (typeof this._result === 'string') { throw new Error( 'invariant: static responses cannot be piped. This is a bug in Next.js' diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 1732a94700594..b8e64fd3750aa 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -62,10 +62,7 @@ import { import { DomainLocale } from './config' import RenderResult, { NodeWritablePiper } from './render-result' import isError from '../lib/is-error' -import { - WebRequestBasedIncomingMessage, - WebResponseBasedServerResponse, -} from '../build/webpack/loaders/next-middleware-ssr-loader/utils' +import { WebIncomingMessage, WebServerResponse } from './web/http-adapter' let Writable: typeof import('stream').Writable let Buffer: typeof import('buffer').Buffer @@ -237,7 +234,7 @@ const invalidKeysMsg = (methodName: string, invalidKeys: string[]) => { function checkRedirectValues( redirect: Redirect, - req: IncomingMessage | WebRequestBasedIncomingMessage, + req: IncomingMessage | WebIncomingMessage, method: 'getStaticProps' | 'getServerSideProps' ) { const { destination, permanent, statusCode, basePath } = redirect @@ -327,8 +324,8 @@ function createServerComponentRenderer( } export async function renderToHTML( - req: IncomingMessage | WebRequestBasedIncomingMessage, - res: ServerResponse | WebResponseBasedServerResponse, + req: IncomingMessage | WebIncomingMessage, + res: ServerResponse | WebServerResponse, pathname: string, query: NextParsedUrlQuery, renderOpts: RenderOpts @@ -858,25 +855,22 @@ export async function renderToHTML( let resOrProxy = res let deferredContent = false if (process.env.NODE_ENV !== 'production') { - resOrProxy = new Proxy( - res, - { - get: function (obj, prop, receiver) { - if (!canAccessRes) { - const message = - `You should not access 'res' after getServerSideProps resolves.` + - `\nRead more: https://nextjs.org/docs/messages/gssp-no-mutating-res` - - if (deferredContent) { - throw new Error(message) - } else { - warn(message) - } + resOrProxy = new Proxy(res, { + get: function (obj, prop, receiver) { + if (!canAccessRes) { + const message = + `You should not access 'res' after getServerSideProps resolves.` + + `\nRead more: https://nextjs.org/docs/messages/gssp-no-mutating-res` + + if (deferredContent) { + throw new Error(message) + } else { + warn(message) } - return Reflect.get(obj, prop, receiver) - }, - } - ) + } + return Reflect.get(obj, prop, receiver) + }, + }) } try { @@ -884,7 +878,9 @@ export async function renderToHTML( req: req as IncomingMessage & { cookies: NextApiRequestCookies }, - res: resOrProxy, + // When using the web runtime, `res` will not be the expected HTTP + // server response. + res: resOrProxy as ServerResponse, query, resolvedUrl: renderOpts.resolvedUrl as string, ...(pageIsDynamic ? { params: params as ParsedUrlQuery } : undefined), @@ -1440,7 +1436,7 @@ function renderToNodeStream( return new Promise((resolve, reject) => { let underlyingStream: { resolve: (error?: Error) => void - writable: WritableType | WebResponseBasedServerResponse + writable: WritableType | WebServerResponse queuedCallbacks: Array<() => void> } | null = null diff --git a/packages/next/server/request-meta.ts b/packages/next/server/request-meta.ts index a9268476af2b9..c3c68e0c9e47a 100644 --- a/packages/next/server/request-meta.ts +++ b/packages/next/server/request-meta.ts @@ -2,15 +2,15 @@ import type { ParsedUrlQuery } from 'querystring' import type { IncomingMessage } from 'http' import type { UrlWithParsedQuery } from 'url' -import { WebRequestBasedIncomingMessage } from '../build/webpack/loaders/next-middleware-ssr-loader/utils' + +import { WebIncomingMessage } from './web/http-adapter' const NEXT_REQUEST_META = Symbol('NextRequestMeta') interface NextIncomingMessage extends IncomingMessage { [NEXT_REQUEST_META]?: RequestMeta } -interface WebRequestBasedNextIncomingMessage - extends WebRequestBasedIncomingMessage { +interface WebRequestBasedNextIncomingMessage extends WebIncomingMessage { [NEXT_REQUEST_META]?: RequestMeta } diff --git a/packages/next/server/send-payload.ts b/packages/next/server/send-payload.ts index 65bfea91e6e17..8913619fedd79 100644 --- a/packages/next/server/send-payload.ts +++ b/packages/next/server/send-payload.ts @@ -3,10 +3,7 @@ import { isResSent } from '../shared/lib/utils' import generateETag from 'etag' import fresh from 'next/dist/compiled/fresh' import RenderResult from './render-result' -import { - WebRequestBasedIncomingMessage, - WebResponseBasedServerResponse, -} from '../build/webpack/loaders/next-middleware-ssr-loader/utils' +import { WebIncomingMessage, WebServerResponse } from './web/http-adapter' export type PayloadOptions = | { private: true } @@ -14,7 +11,7 @@ export type PayloadOptions = | { private: boolean; stateful: false; revalidate: number | false } export function setRevalidateHeaders( - res: ServerResponse | WebResponseBasedServerResponse, + res: ServerResponse | WebServerResponse, options: PayloadOptions ) { if (options.private || options.stateful) { @@ -49,8 +46,8 @@ export async function sendRenderResult({ poweredByHeader, options, }: { - req: IncomingMessage | WebRequestBasedIncomingMessage - res: ServerResponse | WebResponseBasedServerResponse + req: IncomingMessage | WebIncomingMessage + res: ServerResponse | WebServerResponse result: RenderResult type: 'html' | 'json' generateEtags: boolean @@ -99,8 +96,8 @@ export async function sendRenderResult({ } export function sendEtagResponse( - req: IncomingMessage | WebRequestBasedIncomingMessage, - res: ServerResponse | WebResponseBasedServerResponse, + req: IncomingMessage | WebIncomingMessage, + res: ServerResponse | WebServerResponse, etag: string | undefined ): boolean { if (etag) { diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts b/packages/next/server/web/http-adapter.ts similarity index 84% rename from packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts rename to packages/next/server/web/http-adapter.ts index c0d5289f851da..a0f3b9116b68b 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/utils.ts +++ b/packages/next/server/web/http-adapter.ts @@ -1,9 +1,9 @@ import type { IncomingHttpHeaders } from 'http' -import { NextRequest } from '../../../../server/web/spec-extension/request' -import { toNodeHeaders } from '../../../../server/web/utils' +import { NextRequest } from './spec-extension/request' +import { toNodeHeaders } from './utils' -export class WebRequestBasedIncomingMessage { +export class WebIncomingMessage { url: string headers: IncomingHttpHeaders cookies: { [key: string]: string } @@ -17,7 +17,7 @@ export class WebRequestBasedIncomingMessage { } } -export class WebResponseBasedServerResponse { +export class WebServerResponse { private headers: { [name: string]: number | string | string[] } = {} statusCode: number = 200 diff --git a/packages/next/shared/lib/i18n/detect-locale-cookie.ts b/packages/next/shared/lib/i18n/detect-locale-cookie.ts index 542e75ff6a2e4..214e9c06e11f2 100644 --- a/packages/next/shared/lib/i18n/detect-locale-cookie.ts +++ b/packages/next/shared/lib/i18n/detect-locale-cookie.ts @@ -1,8 +1,8 @@ import { IncomingMessage } from 'http' -import { WebRequestBasedIncomingMessage } from '../../../build/webpack/loaders/next-middleware-ssr-loader/utils' +import { WebIncomingMessage } from '../../../server/web/http-adapter' export function detectLocaleCookie( - req: IncomingMessage | WebRequestBasedIncomingMessage, + req: IncomingMessage | WebIncomingMessage, locales: string[] ) { const { NEXT_LOCALE } = (req as any).cookies || {} diff --git a/packages/next/shared/lib/router/utils/prepare-destination.ts b/packages/next/shared/lib/router/utils/prepare-destination.ts index 23eb0b250f3f7..8c72b63924c37 100644 --- a/packages/next/shared/lib/router/utils/prepare-destination.ts +++ b/packages/next/shared/lib/router/utils/prepare-destination.ts @@ -5,10 +5,10 @@ import type { Params } from '../../../../server/router' import type { RouteHas } from '../../../../lib/load-custom-routes' import { compile, pathToRegexp } from 'next/dist/compiled/path-to-regexp' import { parseUrl } from './parse-url' -import { WebRequestBasedIncomingMessage } from '../../../../build/webpack/loaders/next-middleware-ssr-loader/utils' +import { WebIncomingMessage } from '../../../../server/web/http-adapter' export function matchHas( - req: IncomingMessage | WebRequestBasedIncomingMessage, + req: IncomingMessage | WebIncomingMessage, has: RouteHas[], query: Params ): false | Params { diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts index 37449682433f2..18a436547cb97 100644 --- a/packages/next/shared/lib/utils.ts +++ b/packages/next/shared/lib/utils.ts @@ -10,9 +10,9 @@ import type { PreviewData } from 'next/types' import type { UrlObject } from 'url' import { createContext } from 'react' import { - WebRequestBasedIncomingMessage, - WebResponseBasedServerResponse, -} from '../../build/webpack/loaders/next-middleware-ssr-loader/utils' + WebIncomingMessage, + WebServerResponse, +} from '../../server/web/http-adapter' export type NextComponentType< C extends BaseContext = NextPageContext, @@ -84,7 +84,7 @@ export type RenderPage = ( ) => DocumentInitialProps | Promise export type BaseContext = { - res?: ServerResponse | WebResponseBasedServerResponse + res?: ServerResponse | WebServerResponse [k: string]: any } @@ -125,11 +125,11 @@ export interface NextPageContext { /** * `HTTP` request object. */ - req?: IncomingMessage | WebRequestBasedIncomingMessage + req?: IncomingMessage | WebIncomingMessage /** * `HTTP` response object. */ - res?: ServerResponse | WebResponseBasedServerResponse + res?: ServerResponse | WebServerResponse /** * Path section of `URL`. */ @@ -338,9 +338,7 @@ export function getDisplayName

(Component: ComponentType

) { : Component.displayName || Component.name || 'Unknown' } -export function isResSent( - res: ServerResponse | WebResponseBasedServerResponse -) { +export function isResSent(res: ServerResponse | WebServerResponse) { return res.finished || res.headersSent } From a53a6168333df1c4316fa4b14b044d115a12e19b Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 25 Nov 2021 20:29:58 +0100 Subject: [PATCH 06/10] fix import path --- packages/next/server/render-result.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/server/render-result.ts b/packages/next/server/render-result.ts index c80bab1fd8a91..1085c8621ad77 100644 --- a/packages/next/server/render-result.ts +++ b/packages/next/server/render-result.ts @@ -1,6 +1,6 @@ import type { ServerResponse } from 'http' import type { Writable } from 'stream' -import { WebServerResponse } from '../build/webpack/loaders/next-middleware-ssr-loader/utils' +import { WebServerResponse } from './web/http-adapter' export type NodeWritablePiper = ( res: Writable | WebServerResponse, From 188aa8024fdbfd4e8af89bddf5b3698b43aa70e1 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 25 Nov 2021 21:02:38 +0100 Subject: [PATCH 07/10] fix lint error --- .../build/webpack/loaders/next-middleware-ssr-loader/render.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts index 9289e3c5a8de9..b121ee1ee3757 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts @@ -42,7 +42,7 @@ export function getRender({ const encodedPreviewProps = JSON.parse(previewProps) as __ApiPreviewProps return async function render(request: NextRequest) { - const { nextUrl: url, cookies, headers } = request + const { nextUrl: url } = request const { pathname, searchParams } = url const query = Object.fromEntries(searchParams) From a86a55f0595855089ebdf7d9dcc97095f05a18a8 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 25 Nov 2021 21:03:20 +0100 Subject: [PATCH 08/10] fix test --- .../app/pages/index.server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/react-streaming-and-server-components/app/pages/index.server.js b/test/integration/react-streaming-and-server-components/app/pages/index.server.js index 1ec9eda6e3b8c..d790171027046 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/index.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/index.server.js @@ -19,7 +19,7 @@ export default function Index({ header, router }) { export function getServerSideProps({ req }) { const { headers } = req - const header = headers.get(headerKey) + const header = headers[headerKey] || null return { props: { From 13b46228aebe9347ab6e1eb2d08dcb1879bed200 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 25 Nov 2021 23:03:18 +0100 Subject: [PATCH 09/10] fix Document gIP case --- .../next-middleware-ssr-loader/index.ts | 4 +- .../next-middleware-ssr-loader/render.ts | 73 +++++++++++++++---- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts index e4826c61b0f88..3e6b57285e042 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts @@ -27,10 +27,10 @@ export default async function middlewareSSRLoader(this: any) { import { RouterContext } from 'next/dist/shared/lib/router-context' import App from ${stringifiedAbsoluteAppPath} - import Document from ${stringifiedAbsoluteDocumentPath} import { getRender } from 'next/dist/build/webpack/loaders/next-middleware-ssr-loader/render' + const documentMod = require(${stringifiedAbsoluteDocumentPath}) const pageMod = require(${stringifiedAbsolutePagePath}) const errorMod = require(${stringifiedAbsolute500PagePath}) @@ -44,7 +44,7 @@ export default async function middlewareSSRLoader(this: any) { const render = getRender({ App, - Document, + documentMod, pageMod, errorMod, buildManifest, diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts index b121ee1ee3757..c3c31c954d000 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts @@ -8,6 +8,8 @@ import { WebIncomingMessage, WebServerResponse, } from '../../../../server/web/http-adapter' +import { renderToHTML } from '../../../../server/web/render' +import RenderResult from '../../../../server/render-result' const createHeaders = (args?: any) => ({ ...args, @@ -16,7 +18,7 @@ const createHeaders = (args?: any) => ({ export function getRender({ App, - Document, + documentMod, pageMod, errorMod, rscManifest, @@ -26,7 +28,7 @@ export function getRender({ restRenderOpts, }: { App: any - Document: any + documentMod: any pageMod: any errorMod: any rscManifest: object @@ -68,7 +70,7 @@ export function getRender({ // domainLocales: i18n?.domains, dev: process.env.NODE_ENV !== 'production', App, - Document, + Document: documentMod.default, buildManifest, Component: pageMod.default, pageConfig: pageMod.config || {}, @@ -89,7 +91,7 @@ export function getRender({ pageComponent: pageMod.default, pageConfig: pageMod.config || {}, appModule: App, - documentModule: { default: Document }, + documentModule: documentMod, errorModule: errorMod, // notFoundModule: ${ // absolute404Path @@ -121,15 +123,54 @@ export function getRender({ distDir: restRenderOpts.distDir, }) - const result = await pageHandler.renderReqToHTML( - new WebIncomingMessage(request), - new WebServerResponse(), - 'passthrough', - { - supportsDynamicHTML: true, - ...renderOpts, + const req = new WebIncomingMessage(request) + const res = new WebServerResponse() + let result: null | string | RenderResult = null + let statusCode = 200 + + try { + const rendered = await pageHandler.renderReqToHTML( + req, + res, + 'passthrough', + { + supportsDynamicHTML: true, + ...renderOpts, + } + ) + if (typeof rendered === 'string') { + result = rendered + } else if (rendered) { + result = rendered.html + } + } catch (err) { + statusCode = 500 + try { + result = await renderToHTML( + req as any, + { statusCode: 500, err } as any, + '/_error', + query, + { + ...renderOpts, + Component: errorMod.default, + getStaticProps: errorMod.getStaticProps, + getServerSideProps: errorMod.getServerSideProps, + getStaticPaths: errorMod.getStaticPaths, + } + ) + } catch (err2: any) { + return new Response( + ( + err2 || 'An error occurred while rendering ' + pathname + '.' + ).toString(), + { + status: 500, + headers: createHeaders(), + } + ) } - ) + } const transformStream = new TransformStream() const writer = transformStream.writable.getWriter() @@ -138,11 +179,11 @@ export function getRender({ if (typeof result === 'string') { return new Response(result, { headers: { 'x-middleware-ssr': '1' }, - status: 200, + status: statusCode, }) } - if (!result || !result.html) { + if (!result) { return new Response( 'An error occurred while rendering ' + pathname + '.', { @@ -152,7 +193,7 @@ export function getRender({ ) } - result.html.pipe({ + result.pipe({ write: (str: string) => writer.write(encoder.encode(str)), end: () => writer.close(), // Not implemented: cork/uncork/on/removeListener @@ -160,7 +201,7 @@ export function getRender({ return new Response(transformStream.readable, { headers: createHeaders(), - status: 200, + status: statusCode, }) } } From 9957e94660fa352373de1e8ab6b9ef88340ac866 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Fri, 26 Nov 2021 15:11:27 +0100 Subject: [PATCH 10/10] clean up --- .../loaders/next-middleware-ssr-loader/render.ts | 9 ++------- packages/next/server/request-meta.ts | 12 ++++++------ packages/next/server/web/http-adapter.ts | 4 +++- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts index c3c31c954d000..52a350a044437 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts @@ -93,11 +93,6 @@ export function getRender({ appModule: App, documentModule: documentMod, errorModule: errorMod, - // notFoundModule: ${ - // absolute404Path - // ? `require(${stringifyRequest(this, absolute404Path)})` - // : undefined - // }, pageGetStaticProps: pageMod.getStaticProps, pageGetStaticPaths: pageMod.getStaticPaths, pageGetServerSideProps: pageMod.getServerSideProps, @@ -111,7 +106,7 @@ export function getRender({ buildManifest, reactLoadableManifest, - // rewrites: combinedRewrites, + // FIXME: implement rewrites rewrites: [], i18n: restRenderOpts.i18n, page, @@ -178,7 +173,7 @@ export function getRender({ if (typeof result === 'string') { return new Response(result, { - headers: { 'x-middleware-ssr': '1' }, + headers: createHeaders(), status: statusCode, }) } diff --git a/packages/next/server/request-meta.ts b/packages/next/server/request-meta.ts index c3c68e0c9e47a..2d9a0065ff26c 100644 --- a/packages/next/server/request-meta.ts +++ b/packages/next/server/request-meta.ts @@ -10,7 +10,7 @@ const NEXT_REQUEST_META = Symbol('NextRequestMeta') interface NextIncomingMessage extends IncomingMessage { [NEXT_REQUEST_META]?: RequestMeta } -interface WebRequestBasedNextIncomingMessage extends WebIncomingMessage { +interface NextWebIncomingMessage extends WebIncomingMessage { [NEXT_REQUEST_META]?: RequestMeta } @@ -26,15 +26,15 @@ interface RequestMeta { } export function getRequestMeta( - req: NextIncomingMessage | WebRequestBasedNextIncomingMessage, + req: NextIncomingMessage | NextWebIncomingMessage, key?: undefined ): RequestMeta export function getRequestMeta( - req: NextIncomingMessage | WebRequestBasedNextIncomingMessage, + req: NextIncomingMessage | NextWebIncomingMessage, key: K ): RequestMeta[K] export function getRequestMeta( - req: NextIncomingMessage | WebRequestBasedNextIncomingMessage, + req: NextIncomingMessage | NextWebIncomingMessage, key?: K ): RequestMeta | RequestMeta[K] { const meta = req[NEXT_REQUEST_META] || {} @@ -42,7 +42,7 @@ export function getRequestMeta( } export function setRequestMeta( - req: NextIncomingMessage | WebRequestBasedNextIncomingMessage, + req: NextIncomingMessage | NextWebIncomingMessage, meta: RequestMeta ) { req[NEXT_REQUEST_META] = meta @@ -50,7 +50,7 @@ export function setRequestMeta( } export function addRequestMeta( - request: NextIncomingMessage | WebRequestBasedNextIncomingMessage, + request: NextIncomingMessage | NextWebIncomingMessage, key: K, value: RequestMeta[K] ) { diff --git a/packages/next/server/web/http-adapter.ts b/packages/next/server/web/http-adapter.ts index a0f3b9116b68b..f77596fc2a70c 100644 --- a/packages/next/server/web/http-adapter.ts +++ b/packages/next/server/web/http-adapter.ts @@ -22,7 +22,9 @@ export class WebServerResponse { statusCode: number = 200 - // FIXME + // FIXME: The internal states should support correctly throwing errors when + // using improperly. And in the future it should be able to be translated to + // the Node HTTP OutgoingMessage. finished: boolean = false headersSent: boolean = false body: [] = []