Skip to content

Commit

Permalink
Refactor the middleware SSR loader (#31508)
Browse files Browse the repository at this point in the history
Initial step for #31506, to move the adapter logic out of the loader output string so it will be easier to reuse code in the future. Also, more options are passed to the loader to align with the serverless loader.

## 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`
  • Loading branch information
shuding committed Nov 16, 2021
1 parent 55ab4f0 commit 4ea7d7b
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 148 deletions.
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
186 changes: 44 additions & 142 deletions packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts
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: 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 (!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

0 comments on commit 4ea7d7b

Please sign in to comment.