rsnext/packages/next/export/worker.ts

525 lines
16 KiB
TypeScript
Raw Normal View History

import type { ComponentType } from 'react'
import type { FontManifest } from '../server/font-utils'
import type { GetStaticProps } from '../types'
import type { IncomingMessage, ServerResponse } from 'http'
import type { NextConfigComplete } from '../server/config-shared'
import type { NextParsedUrlQuery } from '../server/request-meta'
import url from 'url'
import { extname, join, dirname, sep } from 'path'
import { renderToHTML } from '../server/render'
import { promises } from 'fs'
2020-03-29 00:31:06 +01:00
import AmpHtmlValidator from 'next/dist/compiled/amphtml-validator'
import { loadComponents } from '../server/load-components'
import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher'
import { getRouteRegex } from '../shared/lib/router/utils/route-regex'
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 { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { SERVER_PROPS_EXPORT_ERROR } from '../lib/constants'
import '../server/node-polyfill-fetch'
import { requireFontManifest } from '../server/require'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import { trace } from '../trace'
import { isInAmpMode } from '../shared/lib/amp-mode'
import { setHttpAgentOptions } from '../server/config'
2021-09-04 16:41:06 +02:00
import RenderResult from '../server/render-result'
import isError from '../lib/is-error'
const envConfig = require('../shared/lib/runtime-config')
;(global as any).__NEXT_DATA__ = {
nextExport: true,
}
interface AmpValidation {
page: string
result: {
errors: AmpHtmlValidator.ValidationError[]
warnings: AmpHtmlValidator.ValidationError[]
}
}
interface PathMap {
page: string
query?: NextParsedUrlQuery
}
interface ExportPageInput {
path: string
pathMap: PathMap
distDir: string
outDir: string
pagesDataDir: string
renderOpts: RenderOpts
buildExport?: boolean
serverRuntimeConfig: { [key: string]: any }
subFolders?: boolean
serverless: boolean
optimizeFonts: boolean
optimizeCss: any
disableOptimizedLoading: any
parentSpanId: any
httpAgentOptions: NextConfigComplete['httpAgentOptions']
serverComponents?: boolean
2022-05-25 11:46:26 +02:00
appDir?: boolean
}
interface ExportPageResults {
ampValidations: AmpValidation[]
fromBuildExportRevalidate?: number
error?: boolean
ssgNotFound?: boolean
duration: number
}
interface RenderOpts {
runtimeConfig?: { [key: string]: any }
params?: { [key: string]: string | string[] }
ampPath?: string
ampValidatorPath?: string
ampSkipValidation?: boolean
optimizeFonts?: boolean
disableOptimizedLoading?: boolean
optimizeCss?: any
fontManifest?: FontManifest
locales?: string[]
locale?: string
defaultLocale?: string
trailingSlash?: boolean
2022-05-25 11:46:26 +02:00
appDir?: boolean
}
type ComponentModule = ComponentType<{}> & {
renderReqToHTML: typeof renderToHTML
getStaticProps?: GetStaticProps
}
export default async function exportPage({
parentSpanId,
path,
pathMap,
distDir,
outDir,
2022-05-25 11:46:26 +02:00
appDir,
pagesDataDir,
renderOpts,
Add experimental SPR support (#8832) * initial commit for SPRv2 * Add initial SPR cache handling * update SPR handling * Implement SPR handling in render * Update tests, handle caching with serverless next start, add TODOs, and update manifest generating * Handle no prerender-manifest from not being used * Fix url.parse error * Apply suggestions from code review Co-Authored-By: Joe Haddad <joe.haddad@zeit.co> * Replace set with constants in next-page-config * simplify sprStatus.used * Add error if getStaticProps is used with getInitialProps * Remove stale TODO * Update revalidate values in SPR cache for non-seeded routes * Apply suggestions from code review * Remove concurrency type * Rename variable for clarity * Add copying prerender files during export * Add comment for clarity * Fix exporting * Update comment * Add additional note * Rename variable * Update to not re-export SPR pages from build * Hard navigate when fetching data fails * Remove default extension * Add brackets * Add checking output files to prerender tests * Adjust export move logic * Clarify behavior of export aggregation * Update variable names for clarity * Update tests * Add comment * s/an oxymoron/contradictory/ * rename * Extract error case * Add tests for exporting SPR pages and update /_next/data endpoint to end with .json * Relocate variable * Adjust route building * Rename to unstable * Rename unstable_getStaticParams * Fix linting * Only add this when a data request * Update prerender data tests * s/isServerless/isLikeServerless/ * Don't rely on query for `next start` in serverless mode * Rename var * Update renderedDuringBuild check * Add test for dynamic param with bracket * Fix serverless next start handling * remove todo * Adjust comment * Update calculateRevalidate * Remove cache logic from render.tsx * Remove extra imports * Move SPR cache logic to next-server * Remove old isDynamic prop * Add calling App getInitialProps for SPR pages * Update revalidate logic * Add isStale to SprCacheValue * Update headers for SPR * add awaiting pendingRevalidation * Dont return null for revalidation render * Adjust logic * Be sure to remove coalesced render * Fix data for serverless * Create a method coalescing utility * Remove TODO * Extract send payload helper * Wrap in-line * Move around some code * Add tests for de-duping and revalidating * Update prerender manifest test
2019-09-24 10:50:04 +02:00
buildExport,
serverRuntimeConfig,
subFolders,
serverless,
optimizeFonts,
optimizeCss,
disableOptimizedLoading,
httpAgentOptions,
serverComponents,
}: ExportPageInput): Promise<ExportPageResults> {
setHttpAgentOptions(httpAgentOptions)
const exportPageSpan = trace('export-page-worker', parentSpanId)
return exportPageSpan.traceAsyncFn(async () => {
const start = Date.now()
let results: Omit<ExportPageResults, 'duration'> = {
ampValidations: [],
}
try {
const { query: originalQuery = {} } = pathMap
const { page } = pathMap
const filePath = normalizePagePath(path)
const isDynamic = isDynamicRoute(page)
const ampPath = `${filePath}.amp`
let renderAmpPath = ampPath
let query = { ...originalQuery }
let params: { [key: string]: string | string[] } | undefined
let updatedPath = query.__nextSsgPath || path
let locale = query.__nextLocale || renderOpts.locale
delete query.__nextLocale
delete query.__nextSsgPath
if (renderOpts.locale) {
const localePathResult = normalizeLocalePath(path, renderOpts.locales)
if (localePathResult.detectedLocale) {
updatedPath = localePathResult.pathname
locale = localePathResult.detectedLocale
if (locale === renderOpts.defaultLocale) {
renderAmpPath = `${normalizePagePath(updatedPath)}.amp`
}
}
}
// We need to show a warning if they try to provide query values
// for an auto-exported page since they won't be available
const hasOrigQueryValues = Object.keys(originalQuery).length > 0
const queryWithAutoExportWarn = () => {
if (hasOrigQueryValues) {
throw new Error(
`\nError: you provided query values for ${path} which is an auto-exported page. These can not be applied since the page can no longer be re-rendered on the server. To disable auto-export for this page add \`getInitialProps\`\n`
)
}
}
// Check if the page is a specified dynamic route
const nonLocalizedPath = normalizeLocalePath(
path,
renderOpts.locales
).pathname
if (isDynamic && page !== nonLocalizedPath) {
params = getRouteMatcher(getRouteRegex(page))(updatedPath) || undefined
if (params) {
// we have to pass these separately for serverless
if (!serverless) {
query = {
...query,
...params,
}
}
} else {
throw new Error(
`The provided export path '${updatedPath}' doesn't match the '${page}' page.\nRead more: https://nextjs.org/docs/messages/export-path-mismatch`
)
}
}
const headerMocks = {
headers: {},
getHeader: () => ({}),
setHeader: () => {},
hasHeader: () => false,
removeHeader: () => {},
getHeaderNames: () => [],
}
const req = {
url: updatedPath,
...headerMocks,
} as unknown as IncomingMessage
const res = {
...headerMocks,
} as unknown as ServerResponse
if (updatedPath === '/500' && page === '/_error') {
res.statusCode = 500
}
if (renderOpts.trailingSlash && !req.url?.endsWith('/')) {
req.url += '/'
}
envConfig.setConfig({
serverRuntimeConfig,
publicRuntimeConfig: renderOpts.runtimeConfig,
})
const getHtmlFilename = (_path: string) =>
subFolders ? `${_path}${sep}index.html` : `${_path}.html`
let htmlFilename = getHtmlFilename(filePath)
// dynamic routes can provide invalid extensions e.g. /blog/[...slug] returns an
// extension of `.slug]`
const pageExt = isDynamic ? '' : extname(page)
const pathExt = isDynamic ? '' : extname(path)
// force output 404.html for backwards compat
if (path === '/404.html') {
htmlFilename = path
}
// Make sure page isn't a folder with a dot in the name e.g. `v1.2`
else if (pageExt !== pathExt && pathExt !== '') {
const isBuiltinPaths = ['/500', '/404'].some(
(p) => p === path || p === path + '.html'
)
// If the ssg path has .html extension, and it's not builtin paths, use it directly
// Otherwise, use that as the filename instead
const isHtmlExtPath =
!serverless && !isBuiltinPaths && path.endsWith('.html')
htmlFilename = isHtmlExtPath ? getHtmlFilename(path) : path
} else if (path === '/') {
// If the path is the root, just use index.html
htmlFilename = 'index.html'
}
const baseDir = join(outDir, dirname(htmlFilename))
let htmlFilepath = join(outDir, htmlFilename)
await promises.mkdir(baseDir, { recursive: true })
let renderResult
let curRenderOpts: RenderOpts = {}
let renderMethod = renderToHTML
let inAmpMode = false,
hybridAmp = false
const renderedDuringBuild = (getStaticProps: any) => {
return !buildExport && getStaticProps && !isDynamicRoute(path)
}
if (serverless) {
const curUrl = url.parse(req.url!, true)
req.url = url.format({
...curUrl,
query: {
...curUrl.query,
...query,
},
})
const {
Component,
ComponentMod,
getServerSideProps,
getStaticProps,
pageConfig,
} = await loadComponents(
distDir,
page,
serverless,
serverComponents,
2022-05-25 11:46:26 +02:00
appDir
)
const ampState = {
ampFirst: pageConfig?.amp === true,
hasQuery: Boolean(query.amp),
hybrid: pageConfig?.amp === 'hybrid',
}
inAmpMode = isInAmpMode(ampState)
hybridAmp = ampState.hybrid
if (getServerSideProps) {
throw new Error(
`Error for page ${page}: ${SERVER_PROPS_EXPORT_ERROR}`
)
}
// if it was auto-exported the HTML is loaded here
if (typeof Component === 'string') {
renderResult = RenderResult.fromStatic(Component)
queryWithAutoExportWarn()
} else {
// for non-dynamic SSG pages we should have already
// prerendered the file
if (renderedDuringBuild(getStaticProps))
return { ...results, duration: Date.now() - start }
if (getStaticProps && !htmlFilepath.endsWith('.html')) {
// make sure it ends with .html if the name contains a dot
htmlFilename += '.html'
htmlFilepath += '.html'
}
renderMethod = (ComponentMod as ComponentModule).renderReqToHTML
const result = await renderMethod(
req,
res,
'export',
{
ampPath: renderAmpPath,
/// @ts-ignore
optimizeFonts,
/// @ts-ignore
optimizeCss,
disableOptimizedLoading,
distDir,
fontManifest: optimizeFonts
? requireFontManifest(distDir, serverless)
: null,
locale: locale!,
locales: renderOpts.locales!,
},
// @ts-ignore
params
)
curRenderOpts = (result as any).renderOpts || {}
renderResult = (result as any).html
}
if (!renderResult && !(curRenderOpts as any).isNotFound) {
throw new Error(`Failed to render serverless page`)
}
} else {
const components = await loadComponents(
distDir,
page,
serverless,
serverComponents
)
const ampState = {
ampFirst: components.pageConfig?.amp === true,
hasQuery: Boolean(query.amp),
hybrid: components.pageConfig?.amp === 'hybrid',
}
inAmpMode = isInAmpMode(ampState)
hybridAmp = ampState.hybrid
if (components.getServerSideProps) {
throw new Error(
`Error for page ${page}: ${SERVER_PROPS_EXPORT_ERROR}`
)
}
// for non-dynamic SSG pages we should have already
// prerendered the file
if (renderedDuringBuild(components.getStaticProps)) {
return { ...results, duration: Date.now() - start }
}
Add experimental SPR support (#8832) * initial commit for SPRv2 * Add initial SPR cache handling * update SPR handling * Implement SPR handling in render * Update tests, handle caching with serverless next start, add TODOs, and update manifest generating * Handle no prerender-manifest from not being used * Fix url.parse error * Apply suggestions from code review Co-Authored-By: Joe Haddad <joe.haddad@zeit.co> * Replace set with constants in next-page-config * simplify sprStatus.used * Add error if getStaticProps is used with getInitialProps * Remove stale TODO * Update revalidate values in SPR cache for non-seeded routes * Apply suggestions from code review * Remove concurrency type * Rename variable for clarity * Add copying prerender files during export * Add comment for clarity * Fix exporting * Update comment * Add additional note * Rename variable * Update to not re-export SPR pages from build * Hard navigate when fetching data fails * Remove default extension * Add brackets * Add checking output files to prerender tests * Adjust export move logic * Clarify behavior of export aggregation * Update variable names for clarity * Update tests * Add comment * s/an oxymoron/contradictory/ * rename * Extract error case * Add tests for exporting SPR pages and update /_next/data endpoint to end with .json * Relocate variable * Adjust route building * Rename to unstable * Rename unstable_getStaticParams * Fix linting * Only add this when a data request * Update prerender data tests * s/isServerless/isLikeServerless/ * Don't rely on query for `next start` in serverless mode * Rename var * Update renderedDuringBuild check * Add test for dynamic param with bracket * Fix serverless next start handling * remove todo * Adjust comment * Update calculateRevalidate * Remove cache logic from render.tsx * Remove extra imports * Move SPR cache logic to next-server * Remove old isDynamic prop * Add calling App getInitialProps for SPR pages * Update revalidate logic * Add isStale to SprCacheValue * Update headers for SPR * add awaiting pendingRevalidation * Dont return null for revalidation render * Adjust logic * Be sure to remove coalesced render * Fix data for serverless * Create a method coalescing utility * Remove TODO * Extract send payload helper * Wrap in-line * Move around some code * Add tests for de-duping and revalidating * Update prerender manifest test
2019-09-24 10:50:04 +02:00
// TODO: de-dupe the logic here between serverless and server mode
if (components.getStaticProps && !htmlFilepath.endsWith('.html')) {
// make sure it ends with .html if the name contains a dot
htmlFilepath += '.html'
htmlFilename += '.html'
}
if (typeof components.Component === 'string') {
2021-09-04 16:41:06 +02:00
renderResult = RenderResult.fromStatic(components.Component)
queryWithAutoExportWarn()
} else {
/**
* This sets environment variable to be used at the time of static export by head.tsx.
* Using this from process.env allows targeting both serverless and SSR by calling
* `process.env.__NEXT_OPTIMIZE_FONTS`.
* TODO(prateekbh@): Remove this when experimental.optimizeFonts are being cleaned up.
*/
if (optimizeFonts) {
process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true)
}
if (optimizeCss) {
process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true)
}
curRenderOpts = {
...components,
...renderOpts,
ampPath: renderAmpPath,
params,
optimizeFonts,
optimizeCss,
disableOptimizedLoading,
fontManifest: optimizeFonts
? requireFontManifest(distDir, serverless)
: null,
locale: locale as string,
}
renderResult = await renderMethod(
req,
res,
page,
query,
// @ts-ignore
curRenderOpts
)
}
}
results.ssgNotFound = (curRenderOpts as any).isNotFound
const validateAmp = async (
rawAmpHtml: string,
ampPageName: string,
validatorPath?: string
) => {
const validator = await AmpHtmlValidator.getInstance(validatorPath)
const result = validator.validateString(rawAmpHtml)
const errors = result.errors.filter((e) => e.severity === 'ERROR')
const warnings = result.errors.filter((e) => e.severity !== 'ERROR')
if (warnings.length || errors.length) {
results.ampValidations.push({
page: ampPageName,
result: {
errors,
warnings,
},
})
}
}
const html = renderResult ? renderResult.toUnchunkedString() : ''
if (inAmpMode && !curRenderOpts.ampSkipValidation) {
if (!results.ssgNotFound) {
await validateAmp(html, path, curRenderOpts.ampValidatorPath)
}
} else if (hybridAmp) {
// we need to render the AMP version
let ampHtmlFilename = `${ampPath}${sep}index.html`
if (!subFolders) {
ampHtmlFilename = `${ampPath}.html`
}
const ampBaseDir = join(outDir, dirname(ampHtmlFilename))
const ampHtmlFilepath = join(outDir, ampHtmlFilename)
try {
await promises.access(ampHtmlFilepath)
} catch (_) {
// make sure it doesn't exist from manual mapping
let ampRenderResult
if (serverless) {
req.url += (req.url!.includes('?') ? '&' : '?') + 'amp=1'
// @ts-ignore
ampRenderResult = (
await (renderMethod as any)(
req,
res,
'export',
curRenderOpts,
params
)
).html
} else {
ampRenderResult = await renderMethod(
req,
res,
page,
// @ts-ignore
{ ...query, amp: '1' },
curRenderOpts as any
)
}
Add experimental SPR support (#8832) * initial commit for SPRv2 * Add initial SPR cache handling * update SPR handling * Implement SPR handling in render * Update tests, handle caching with serverless next start, add TODOs, and update manifest generating * Handle no prerender-manifest from not being used * Fix url.parse error * Apply suggestions from code review Co-Authored-By: Joe Haddad <joe.haddad@zeit.co> * Replace set with constants in next-page-config * simplify sprStatus.used * Add error if getStaticProps is used with getInitialProps * Remove stale TODO * Update revalidate values in SPR cache for non-seeded routes * Apply suggestions from code review * Remove concurrency type * Rename variable for clarity * Add copying prerender files during export * Add comment for clarity * Fix exporting * Update comment * Add additional note * Rename variable * Update to not re-export SPR pages from build * Hard navigate when fetching data fails * Remove default extension * Add brackets * Add checking output files to prerender tests * Adjust export move logic * Clarify behavior of export aggregation * Update variable names for clarity * Update tests * Add comment * s/an oxymoron/contradictory/ * rename * Extract error case * Add tests for exporting SPR pages and update /_next/data endpoint to end with .json * Relocate variable * Adjust route building * Rename to unstable * Rename unstable_getStaticParams * Fix linting * Only add this when a data request * Update prerender data tests * s/isServerless/isLikeServerless/ * Don't rely on query for `next start` in serverless mode * Rename var * Update renderedDuringBuild check * Add test for dynamic param with bracket * Fix serverless next start handling * remove todo * Adjust comment * Update calculateRevalidate * Remove cache logic from render.tsx * Remove extra imports * Move SPR cache logic to next-server * Remove old isDynamic prop * Add calling App getInitialProps for SPR pages * Update revalidate logic * Add isStale to SprCacheValue * Update headers for SPR * add awaiting pendingRevalidation * Dont return null for revalidation render * Adjust logic * Be sure to remove coalesced render * Fix data for serverless * Create a method coalescing utility * Remove TODO * Extract send payload helper * Wrap in-line * Move around some code * Add tests for de-duping and revalidating * Update prerender manifest test
2019-09-24 10:50:04 +02:00
const ampHtml = ampRenderResult
? ampRenderResult.toUnchunkedString()
: ''
if (!curRenderOpts.ampSkipValidation) {
await validateAmp(ampHtml, page + '?amp=1')
}
await promises.mkdir(ampBaseDir, { recursive: true })
await promises.writeFile(ampHtmlFilepath, ampHtml, 'utf8')
}
}
if ((curRenderOpts as any).pageData) {
const dataFile = join(
pagesDataDir,
htmlFilename.replace(/\.html$/, '.json')
)
await promises.mkdir(dirname(dataFile), { recursive: true })
await promises.writeFile(
dataFile,
JSON.stringify((curRenderOpts as any).pageData),
'utf8'
)
if (hybridAmp) {
await promises.writeFile(
dataFile.replace(/\.json$/, '.amp.json'),
JSON.stringify((curRenderOpts as any).pageData),
'utf8'
)
}
}
results.fromBuildExportRevalidate = (curRenderOpts as any).revalidate
if (!results.ssgNotFound) {
// don't attempt writing to disk if getStaticProps returned not found
await promises.writeFile(htmlFilepath, html, 'utf8')
}
} catch (error) {
console.error(
`\nError occurred prerendering page "${path}". Read more: https://nextjs.org/docs/messages/prerender-error\n` +
(isError(error) && error.stack ? error.stack : error)
)
results.error = true
}
return { ...results, duration: Date.now() - start }
})
}