rsnext/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts

497 lines
15 KiB
TypeScript
Raw Normal View History

import { IncomingMessage, ServerResponse } from 'http'
import { parse as parseUrl, format as formatUrl, UrlWithParsedQuery } from 'url'
2021-07-05 18:31:32 +02:00
import { DecodeError, isResSent } from '../../../../shared/lib/utils'
2021-09-04 16:41:06 +02:00
import { sendRenderResult } from '../../../../server/send-payload'
import { getUtils, vercelHeader, ServerlessHandlerCtx } from './utils'
import { renderToHTML } from '../../../../server/render'
import { tryGetPreviewData } from '../../../../server/api-utils/node'
Refactor Page Paths utils and Middleware Plugin (#36576) This PR brings some significant refactoring in preparation for upcoming middleware changes. Each commit can be reviewed independently, here is a summary of what each one does and the reasoning behind it: - [Move pagesDir to next-dev-server](https://github.com/javivelasco/next.js/pull/12/commits/f2fe154c007379f71c14960ddc553eaaaf786ffa) simply moves the `pagesDir` property to the dev server which is the only place where it is needed. Having it for every server is misleading. - [Move (de)normalize page path utils to a file page-path-utils.ts](https://github.com/javivelasco/next.js/pull/12/commits/27cedf087187b9632ef82a34b3af9cc4fe05d98b) Moves the functions to normalize and denormalize page paths to a single file that is intended to hold every utility function that transforms page paths. Since those are complementary it makes sense to have them together. I also added explanatory comments on why they are not idempotent and examples for input -> output that I find very useful. - [Extract removePagePathTail](https://github.com/javivelasco/next.js/pull/12/commits/6b121332aa9d3e50bd0f28b691fb7faea1b95f51) This extracts a function to remove the tail on a page path (absolute or relative). I'm sure there will be other contexts where we can use it. - [Extract getPagePaths and refactor findPageFile](https://github.com/javivelasco/next.js/pull/12/commits/cf2c7b842eebd8c02f23e79345681a794516b646) This extracts a function `getPagePaths` that is used to generate an array of paths to inspect when looking for a page file from `findPageFile`. Then it refactors such function to use it parallelizing lookups. This will allow us to print every path we look at when looking for a file which can be useful for debugging. It also adds a `flatten` helper. - [Refactor onDemandEntryHandler](https://github.com/javivelasco/next.js/pull/12/commits/4be685c37e3d1b797e929ea4f31495ed7b00e1cc) I've found this one quite difficult to understand so it is refactored to use some of the previously mentioned functions and make it easier to read. - [Extract absolutePagePath util](https://github.com/javivelasco/next.js/pull/12/commits/3bc078347426c73491a076d54ef4de977d9da073) Extracts yet another util from the `next-dev-server` that transforms an absolute path into a page name. Of course it adds comments, parameters and examples. - [Refactor MiddlewarePlugin](https://github.com/javivelasco/next.js/pull/12/commits/c595a2cc629b358cc61861a8a4848b7890d0a15b) This is the most significant change. The logic here was very hard to understand so it is totally redistributed with comments. This also removes a global variable `ssrEntries` that was deprecated in favour of module metadata added to Webpack from loaders keeping less dependencies. It also adds types and makes a clear distinction between phases where we statically analyze the code, find metadata and generate the manifest file cc @shuding @huozhi EDIT: - [Split page path utils](https://github.com/vercel/next.js/pull/36576/commits/158fb002d02887d7ce4be6747cf550a825a426eb) After seeing one of the utils was being used by the client while it was defined originally in the server, with this PR we are splitting the util into multiple files and moving it to `shared/lib` in order to make explicit that those can be also imported from client.
2022-04-30 13:19:27 +02:00
import { denormalizePagePath } from '../../../../shared/lib/page-path/denormalize-page-path'
import { setLazyProp, getCookieParser } from '../../../../server/api-utils'
import { getRedirectStatus } from '../../../../lib/load-custom-routes'
import getRouteNoAssetPath from '../../../../shared/lib/router/utils/get-route-from-asset-path'
import { PERMANENT_REDIRECT_STATUS } from '../../../../shared/lib/constants'
2021-09-04 16:41:06 +02:00
import RenderResult from '../../../../server/render-result'
import isError from '../../../../lib/is-error'
export function getPageHandler(ctx: ServerlessHandlerCtx) {
const {
page,
pageComponent,
pageConfig,
pageGetStaticProps,
pageGetStaticPaths,
pageGetServerSideProps,
appModule,
documentModule,
errorModule,
notFoundModule,
encodedPreviewProps,
pageIsDynamic,
generateEtags,
poweredByHeader,
runtimeConfig,
buildManifest,
reactLoadableManifest,
i18n,
buildId,
basePath,
assetPrefix,
canonicalBase,
escapedBuildId,
} = ctx
const {
handleLocale,
handleRewrites,
handleBasePath,
defaultRouteRegex,
dynamicRouteMatcher,
interpolateDynamicPath,
getParamsFromRouteMatches,
normalizeDynamicRouteParams,
normalizeVercelUrl,
} = getUtils(ctx)
async function renderReqToHTML(
req: IncomingMessage,
res: ServerResponse,
renderMode?: 'export' | 'passthrough' | true,
_renderOpts?: any,
_params?: any
) {
let Component
let App
let config
let Document
let Error
let notFoundMod
let getStaticProps
let getStaticPaths
let getServerSideProps
;[
getStaticProps,
getServerSideProps,
getStaticPaths,
Component,
App,
config,
{ default: Document },
{ default: Error },
notFoundMod,
] = await Promise.all([
pageGetStaticProps,
pageGetServerSideProps,
pageGetStaticPaths,
pageComponent,
appModule,
pageConfig,
documentModule,
errorModule,
notFoundModule,
])
const fromExport = renderMode === 'export' || renderMode === true
const nextStartMode = renderMode === 'passthrough'
let hasValidParams = true
setLazyProp({ req: req as any }, 'cookies', getCookieParser(req.headers))
const options = {
App,
Document,
ComponentMod: { default: Component },
buildManifest,
getStaticProps,
getServerSideProps,
getStaticPaths,
reactLoadableManifest,
canonicalBase,
buildId,
assetPrefix,
runtimeConfig: (runtimeConfig || {}).publicRuntimeConfig || {},
previewProps: encodedPreviewProps,
env: process.env,
basePath,
supportsDynamicHTML: false, // Serverless target doesn't support streaming
..._renderOpts,
}
let _nextData = false
let defaultLocale = i18n?.defaultLocale
let detectedLocale = i18n?.defaultLocale
let parsedUrl: UrlWithParsedQuery
try {
// We need to trust the dynamic route params from the proxy
// to ensure we are using the correct values
const trustQuery = !getStaticProps && req.headers[vercelHeader]
parsedUrl = parseUrl(req.url!, true)
let routeNoAssetPath = parsedUrl.pathname!
if (basePath) {
routeNoAssetPath =
routeNoAssetPath.replace(new RegExp(`^${basePath}`), '') || '/'
}
const origQuery = Object.assign({}, parsedUrl.query)
handleRewrites(req, parsedUrl)
handleBasePath(req, parsedUrl)
// remove ?amp=1 from request URL if rendering for export
if (fromExport && parsedUrl.query.amp) {
const queryNoAmp = Object.assign({}, origQuery)
delete queryNoAmp.amp
req.url = formatUrl({
...parsedUrl,
search: undefined,
query: queryNoAmp,
})
}
if (parsedUrl.pathname!.match(/_next\/data/)) {
_nextData = page !== '/_error'
parsedUrl.pathname = getRouteNoAssetPath(
parsedUrl.pathname!.replace(
new RegExp(`/_next/data/${escapedBuildId}/`),
'/'
),
'.json'
)
routeNoAssetPath = parsedUrl.pathname
}
const localeResult = handleLocale(
req,
res,
parsedUrl,
routeNoAssetPath,
fromExport || nextStartMode
)
defaultLocale = localeResult?.defaultLocale || defaultLocale
detectedLocale = localeResult?.detectedLocale || detectedLocale
routeNoAssetPath = localeResult?.routeNoAssetPath || routeNoAssetPath
if (parsedUrl.query.nextInternalLocale) {
detectedLocale = parsedUrl.query.nextInternalLocale as string
delete parsedUrl.query.nextInternalLocale
}
const renderOpts = Object.assign(
{
Component,
pageConfig: config,
nextExport: fromExport,
isDataReq: _nextData,
locales: i18n?.locales,
locale: detectedLocale,
defaultLocale,
domainLocales: i18n?.domains,
optimizeCss: process.env.__NEXT_OPTIMIZE_CSS,
Adds web worker support to `<Script />` using Partytown (#34244) ## Summary This PR adds a new `worker` strategy to the `<Script />` component that automatically relocates and executes the script in a web worker. ```jsx <Script strategy="worker" ... /> ``` [Partytown](https://partytown.builder.io/) is used under the hood to provide this functionality. ## Behavior - This will land as an experimental feature and will only work behind an opt-in flag in `next.config.js`: ```js experimental: { nextScriptWorkers: true } ``` - This setup use a similar approach to how ESLint and Typescript is used in Next.js by showing an error to the user to install the dependency locally themselves if they've enabled the experimental `nextScriptWorkers` flag. <img width="1068" alt="Screen Shot 2022-03-03 at 2 33 13 PM" src="https://user-images.githubusercontent.com/12476932/156639227-42af5353-a2a6-4126-936e-269112809651.png"> - For Partytown to work, a number of static files must be served directly from the site (see [docs](https://partytown.builder.io/copy-library-files)). In this PR, these files are automatically copied to a `~partytown` directory in `.next/static` during `next build` and `next dev` if the `nextScriptWorkers` flag is set to true. ## Checklist - [X] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [X] Related issues linked using `fixes #number` - [X] Integration tests added - [X] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. This PR fixes #31517.
2022-03-11 23:26:46 +01:00
nextScriptWorkers: process.env.__NEXT_SCRIPT_WORKERS,
crossOrigin: process.env.__NEXT_CROSS_ORIGIN,
},
options
)
if (page === '/_error' && !res.statusCode) {
res.statusCode = 404
}
let params = {}
if (!fromExport && pageIsDynamic) {
const result = normalizeDynamicRouteParams(
trustQuery
? parsedUrl.query
: (dynamicRouteMatcher!(parsedUrl.pathname) as Record<
string,
string | string[]
>)
)
hasValidParams = result.hasValidParams
params = result.params
}
let nowParams = null
if (
pageIsDynamic &&
!hasValidParams &&
req.headers?.['x-now-route-matches']
) {
nowParams = getParamsFromRouteMatches(req, renderOpts, detectedLocale)
}
// make sure to set renderOpts to the correct params e.g. _params
// if provided from worker or params if we're parsing them here
renderOpts.params = _params || params
normalizeVercelUrl(req, !!trustQuery)
// normalize request URL/asPath for fallback/revalidate pages since the
// proxy sets the request URL to the output's path for fallback pages
if (pageIsDynamic && nowParams && defaultRouteRegex) {
const _parsedUrl = parseUrl(req.url!)
_parsedUrl.pathname = interpolateDynamicPath(
_parsedUrl.pathname!,
nowParams
)
parsedUrl.pathname = _parsedUrl.pathname
req.url = formatUrl(_parsedUrl)
}
// make sure to normalize asPath for revalidate and _next/data requests
// since the asPath should match what is shown on the client
if (!fromExport && (getStaticProps || getServerSideProps)) {
// don't include dynamic route params in query while normalizing
// asPath
if (pageIsDynamic && defaultRouteRegex) {
delete (parsedUrl as any).search
for (const param of Object.keys(defaultRouteRegex.groups)) {
delete origQuery[param]
}
}
parsedUrl.pathname = denormalizePagePath(parsedUrl.pathname!)
renderOpts.resolvedUrl = formatUrl({
...parsedUrl,
query: origQuery,
})
// For getServerSideProps we need to ensure we use the original URL
// and not the resolved URL to prevent a hydration mismatch on asPath
renderOpts.resolvedAsPath = getServerSideProps
? formatUrl({
...parsedUrl,
pathname: routeNoAssetPath,
query: origQuery,
})
: renderOpts.resolvedUrl
}
const isFallback = parsedUrl.query.__nextFallback
const previewData = tryGetPreviewData(req, res, options.previewProps)
const isPreviewMode = previewData !== false
if (process.env.__NEXT_OPTIMIZE_FONTS) {
renderOpts.optimizeFonts = true
/**
* __webpack_require__.__NEXT_FONT_MANIFEST__ is added by
* font-stylesheet-gathering-plugin
*/
// @ts-ignore
renderOpts.fontManifest = __webpack_require__.__NEXT_FONT_MANIFEST__
}
let result = await renderToHTML(
req,
res,
page,
Object.assign(
{},
getStaticProps
? { ...(parsedUrl.query.amp ? { amp: '1' } : {}) }
: parsedUrl.query,
nowParams ? nowParams : params,
_params,
isFallback ? { __nextFallback: 'true' } : {}
),
renderOpts
)
if (!renderMode) {
if (_nextData || getStaticProps || getServerSideProps) {
if (renderOpts.isNotFound) {
res.statusCode = 404
if (_nextData) {
res.end('{"notFound":true}')
return null
}
const NotFoundComponent = notFoundMod ? notFoundMod.default : Error
const errPathname = notFoundMod ? '/404' : '/_error'
const result2 = await renderToHTML(
req,
res,
errPathname,
parsedUrl.query,
Object.assign({}, options, {
getStaticProps: notFoundMod
? notFoundMod.getStaticProps
: undefined,
getStaticPaths: undefined,
getServerSideProps: undefined,
Component: NotFoundComponent,
err: undefined,
locale: detectedLocale,
locales: i18n?.locales,
defaultLocale: i18n?.defaultLocale,
})
)
2021-09-04 16:41:06 +02:00
sendRenderResult({
req,
res,
2021-09-04 16:41:06 +02:00
result: result2 ?? RenderResult.empty,
type: 'html',
generateEtags,
poweredByHeader,
options: {
private: isPreviewMode || page === '/404',
stateful: !!getServerSideProps,
revalidate: renderOpts.revalidate,
2021-09-04 16:41:06 +02:00
},
})
return null
} else if (renderOpts.isRedirect && !_nextData) {
const redirect = {
destination: renderOpts.pageData.pageProps.__N_REDIRECT,
statusCode: renderOpts.pageData.pageProps.__N_REDIRECT_STATUS,
basePath: renderOpts.pageData.pageProps.__N_REDIRECT_BASE_PATH,
}
const statusCode = getRedirectStatus(redirect)
if (
basePath &&
redirect.basePath !== false &&
redirect.destination.startsWith('/')
) {
redirect.destination = `${basePath}${redirect.destination}`
}
if (statusCode === PERMANENT_REDIRECT_STATUS) {
res.setHeader('Refresh', `0;url=${redirect.destination}`)
}
res.statusCode = statusCode
res.setHeader('Location', redirect.destination)
res.end(redirect.destination)
return null
} else {
2021-09-04 16:41:06 +02:00
sendRenderResult({
req,
res,
2021-09-04 16:41:06 +02:00
result: _nextData
? RenderResult.fromStatic(JSON.stringify(renderOpts.pageData))
: result ?? RenderResult.empty,
type: _nextData ? 'json' : 'html',
generateEtags,
poweredByHeader,
options: {
private: isPreviewMode || renderOpts.is404Page,
stateful: !!getServerSideProps,
revalidate: renderOpts.revalidate,
2021-09-04 16:41:06 +02:00
},
})
return null
}
}
} else if (isPreviewMode) {
res.setHeader(
'Cache-Control',
'private, no-cache, no-store, max-age=0, must-revalidate'
)
}
if (renderMode) return { html: result, renderOpts }
return result ? result.toUnchunkedString() : null
} catch (err) {
if (!parsedUrl!) {
parsedUrl = parseUrl(req.url!, true)
}
if (isError(err) && err.code === 'ENOENT') {
res.statusCode = 404
2021-07-05 18:31:32 +02:00
} else if (err instanceof DecodeError) {
res.statusCode = 400
} else {
console.error('Unhandled error during request:', err)
// Backwards compat (call getInitialProps in custom error):
try {
await renderToHTML(
req,
res,
'/_error',
parsedUrl!.query,
Object.assign({}, options, {
getStaticProps: undefined,
getStaticPaths: undefined,
getServerSideProps: undefined,
Component: Error,
err: err,
// Short-circuit rendering:
isDataReq: true,
})
)
} catch (underErrorErr) {
console.error(
'Failed call /_error subroutine, continuing to crash function:',
underErrorErr
)
}
// Throw the error to crash the serverless function
if (isResSent(res)) {
console.error('!!! WARNING !!!')
console.error(
'Your function crashed, but closed the response before allowing the function to exit.\\n' +
'This may cause unexpected behavior for the next request.'
)
console.error('!!! WARNING !!!')
}
throw err
}
const result2 = await renderToHTML(
req,
res,
'/_error',
parsedUrl!.query,
Object.assign({}, options, {
getStaticProps: undefined,
getStaticPaths: undefined,
getServerSideProps: undefined,
Component: Error,
err: res.statusCode === 404 ? undefined : err,
})
)
2021-09-04 16:41:06 +02:00
return result2 ? result2.toUnchunkedString() : null
}
}
return {
renderReqToHTML,
render: async function render(req: IncomingMessage, res: ServerResponse) {
try {
const html = await renderReqToHTML(req, res)
if (html) {
2021-09-04 16:41:06 +02:00
sendRenderResult({
req,
res,
result: RenderResult.fromStatic(html as any),
type: 'html',
generateEtags,
poweredByHeader,
})
}
} catch (err) {
console.error(err)
// Throw the error to crash the serverless function
throw err
}
},
}
}