Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor the middleware SSR loader #31508

Merged
merged 3 commits into from Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 2 additions & 6 deletions packages/next/build/entries.ts
Expand Up @@ -160,14 +160,10 @@ export function createEntrypoints(
name: '[name].js',
value: `next-middleware-ssr-loader?${stringify({
page,
absoluteAppPath: pages['/_app'],
absoluteDocumentPath: pages['/_document'],
absoluteErrorPath: pages['/500'] || pages['/_error'],
absolute500Path: pages['/500'] || '',
absolutePagePath,
isServerComponent: isFlight,
buildId,
basePath: config.basePath,
assetPrefix: config.assetPrefix,
...defaultServerlessOptions,
} as any)}!`,
isServer: false,
isServerWeb: true,
Expand Down
Expand Up @@ -5,159 +5,61 @@ export default async function middlewareRSCLoader(this: any) {
absolutePagePath,
absoluteAppPath,
absoluteDocumentPath,
absolute500Path,
absoluteErrorPath,
basePath,
isServerComponent: isServerComponentQuery,
assetPrefix,
buildId,
isServerComponent,
...restRenderOpts
} = this.getOptions()

const isServerComponent = isServerComponentQuery === 'true'
const stringifiedAbsolutePagePath = stringifyRequest(this, absolutePagePath)
const stringifiedAbsoluteAppPath = stringifyRequest(this, absoluteAppPath)
const stringifiedAbsoluteErrorPath = stringifyRequest(this, absoluteErrorPath)
const stringifiedAbsolute500PagePath = stringifyRequest(
this,
absolute500Path || absoluteErrorPath
)
const stringifiedAbsoluteDocumentPath = stringifyRequest(
this,
absoluteDocumentPath
)

const transformed = `
import { adapter } from 'next/dist/server/web/adapter'
import { RouterContext } from 'next/dist/shared/lib/router-context'
import { renderToHTML } from 'next/dist/server/web/render'

import App from ${stringifiedAbsoluteAppPath}
import Document from ${stringifiedAbsoluteDocumentPath}
const errorMod = require(${stringifiedAbsoluteErrorPath})

const {
default: Page,
config,
getStaticProps,
getServerSideProps,
getStaticPaths
} = require(${stringifiedAbsolutePagePath})

const buildManifest = self.__BUILD_MANIFEST
const reactLoadableManifest = self.__REACT_LOADABLE_MANIFEST
const rscManifest = self.__RSC_MANIFEST

if (typeof Page !== 'function') {
throw new Error('Your page must export a \`default\` component')
}

const Component = Page

async function render(request) {
const url = request.nextUrl
const { pathname, searchParams } = url
const query = Object.fromEntries(searchParams)

// Preflight request
if (request.method === 'HEAD') {
return new Response('OK.', {
headers: { 'x-middleware-ssr': '1' }
})
}

const renderServerComponentData = ${
isServerComponent ? `query.__flight__ !== undefined` : 'false'
}
delete query.__flight__

const req = { url: pathname }
const renderOpts = {
Component,
pageConfig: config || {},
// Locales are not supported yet.
// locales: i18n?.locales,
// locale: detectedLocale,
// defaultLocale,
// domainLocales: i18n?.domains,
dev: process.env.NODE_ENV !== 'production',
App,
Document,
buildManifest,
getStaticProps,
getServerSideProps,
getStaticPaths,
reactLoadableManifest,
buildId: ${JSON.stringify(buildId)},
assetPrefix: ${JSON.stringify(assetPrefix || '')},
env: process.env,
basePath: ${JSON.stringify(basePath || '')},
supportsDynamicHTML: true,
concurrentFeatures: true,
renderServerComponentData,
serverComponentManifest: ${
isServerComponent ? 'rscManifest' : 'null'
},
}

const transformStream = new TransformStream()
const writer = transformStream.writable.getWriter()
const encoder = new TextEncoder()
let result
let renderError
let statusCode = 200
try {
result = await renderToHTML(
req,
{},
pathname,
query,
renderOpts
)
} catch (err) {
renderError = err
statusCode = 500
}
if (renderError) {
try {
const errorRes = { statusCode, err: renderError }
result = await renderToHTML(
req,
errorRes,
'/_error',
query,
{
...renderOpts,
Component: errorMod.default,
getStaticProps: errorMod.getStaticProps,
getServerSideProps: errorMod.getServerSideProps,
getStaticPaths: errorMod.getStaticPaths,
}
)
} catch (err) {
return new Response(
(err || 'An error occurred while rendering ' + pathname + '.').toString(),
{
status: 500,
headers: { 'x-middleware-ssr': '1' }
}
)
}
}

result.pipe({
write: str => writer.write(encoder.encode(str)),
end: () => writer.close(),
// Not implemented: cork/uncork/on/removeListener
})

return new Response(transformStream.readable, {
headers: { 'x-middleware-ssr': '1' },
status: statusCode
})
}

export default function rscMiddleware(opts) {
return adapter({
...opts,
handler: render
})
}
`
import { adapter } from 'next/dist/server/web/adapter'
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 pageMod = require(${stringifiedAbsolutePagePath})
const errorMod = require(${stringifiedAbsolute500PagePath})

const buildManifest = self.__BUILD_MANIFEST
const reactLoadableManifest = self.__REACT_LOADABLE_MANIFEST
const rscManifest = self.__RSC_MANIFEST

if (typeof pageMod.default !== 'function') {
throw new Error('Your page must export a \`default\` component')
}

const render = getRender({
App,
Document,
pageMod,
errorMod,
buildManifest,
reactLoadableManifest,
rscManifest,
isServerComponent: ${JSON.stringify(isServerComponent)},
restRenderOpts: ${JSON.stringify(restRenderOpts)}
})

export default function rscMiddleware(opts) {
return adapter({
...opts,
handler: render
})
}`

return transformed
}
@@ -0,0 +1,133 @@
import { NextRequest } from '../../../../server/web/spec-extension/request'
import { renderToHTML } from '../../../../server/web/render'
import RenderResult from '../../../../server/render-result'

export function getRender({
App,
Document,
pageMod,
errorMod,
rscManifest,
buildManifest,
reactLoadableManifest,
isServerComponent,
restRenderOpts,
}: {
App: any
Document: any
pageMod: any
errorMod: any
rscManifest: object
buildManifest: any
reactLoadableManifest: any
isServerComponent: boolean
restRenderOpts: any
}) {
return async function render(request: NextRequest) {
const url = request.nextUrl
const { pathname, searchParams } = url

const query = Object.fromEntries(searchParams)

// Preflight request
if (request.method === 'HEAD') {
return new Response('OK.', {
headers: { 'x-middleware-ssr': '1' },
})
}

const renderServerComponentData = isServerComponent
? query.__flight__ !== undefined
: false
delete query.__flight__

const req = { url: pathname }
const renderOpts = {
...restRenderOpts,
// Locales are not supported yet.
// locales: i18n?.locales,
// locale: detectedLocale,
// defaultLocale,
// domainLocales: i18n?.domains,
dev: process.env.NODE_ENV !== 'production',
App,
Document,
buildManifest,
Component: pageMod.default,
pageConfig: pageMod.config || {},
getStaticProps: pageMod.getStaticProps,
getServerSideProps: pageMod.getServerSideProps,
getStaticPaths: pageMod.getStaticPaths,
reactLoadableManifest,
env: process.env,
supportsDynamicHTML: true,
concurrentFeatures: true,
renderServerComponentData,
serverComponentManifest: isServerComponent ? rscManifest : null,
ComponentMod: null,
}

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) {
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 (err: any) {
return new Response(
(
err || 'An error occurred while rendering ' + pathname + '.'
).toString(),
{
status: 500,
headers: { 'x-middleware-ssr': '1' },
}
)
}
}

if (!result) {
return new Response(
'An error occurred while rendering ' + pathname + '.',
{
status: 500,
headers: { 'x-middleware-ssr': '1' },
}
)
}

result.pipe({
write: (str: string) => writer.write(encoder.encode(str)),
end: () => writer.close(),
// Not implemented: cork/uncork/on/removeListener
} as any)

return new Response(transformStream.readable, {
headers: { 'x-middleware-ssr': '1' },
status: 200,
})
}
}
6 changes: 6 additions & 0 deletions packages/next/server/dev/hot-reloader.ts
Expand Up @@ -529,11 +529,17 @@ export default class HotReloader {
absoluteAppPath: this.pagesMapping['/_app'],
absoluteDocumentPath: this.pagesMapping['/_document'],
absoluteErrorPath: this.pagesMapping['/_error'],
absolute404Path: this.pagesMapping['/404'] || '',
absolutePagePath,
isServerComponent,
buildId: this.buildId,
basePath: this.config.basePath,
assetPrefix: this.config.assetPrefix,
generateEtags: this.config.generateEtags,
poweredByHeader: this.config.poweredByHeader,
canonicalBase: this.config.amp.canonicalBase,
i18n: this.config.i18n,
previewProps: this.previewProps,
} as any)}!`,
isServer: false,
isServerWeb: true,
Expand Down