85e720a092
* 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
219 lines
6.1 KiB
JavaScript
219 lines
6.1 KiB
JavaScript
import mkdirpModule from 'mkdirp'
|
|
import { promisify } from 'util'
|
|
import { extname, join, dirname, sep } from 'path'
|
|
import { renderToHTML } from '../next-server/server/render'
|
|
import { writeFile, access } from 'fs'
|
|
import AmpHtmlValidator from 'amphtml-validator'
|
|
import { loadComponents } from '../next-server/server/load-components'
|
|
import { isDynamicRoute } from '../next-server/lib/router/utils/is-dynamic'
|
|
import { getRouteMatcher } from '../next-server/lib/router/utils/route-matcher'
|
|
import { getRouteRegex } from '../next-server/lib/router/utils/route-regex'
|
|
|
|
const envConfig = require('../next-server/lib/runtime-config')
|
|
const writeFileP = promisify(writeFile)
|
|
const mkdirp = promisify(mkdirpModule)
|
|
const accessP = promisify(access)
|
|
|
|
global.__NEXT_DATA__ = {
|
|
nextExport: true
|
|
}
|
|
|
|
export default async function ({
|
|
path,
|
|
pathMap,
|
|
distDir,
|
|
buildId,
|
|
outDir,
|
|
sprDataDir,
|
|
renderOpts,
|
|
buildExport,
|
|
serverRuntimeConfig,
|
|
subFolders,
|
|
serverless
|
|
}) {
|
|
let results = {
|
|
ampValidations: []
|
|
}
|
|
|
|
try {
|
|
let { query = {} } = pathMap
|
|
const { page } = pathMap
|
|
const filePath = path === '/' ? '/index' : path
|
|
const ampPath = `${filePath}.amp`
|
|
|
|
// Check if the page is a specified dynamic route
|
|
if (isDynamicRoute(page) && page !== path) {
|
|
const params = getRouteMatcher(getRouteRegex(page))(path)
|
|
if (params) {
|
|
query = {
|
|
...query,
|
|
...params
|
|
}
|
|
} else {
|
|
throw new Error(
|
|
`The provided export path '${path}' doesn't match the '${page}' page.\nRead more: https://err.sh/zeit/next.js/export-path-mismatch`
|
|
)
|
|
}
|
|
}
|
|
|
|
const headerMocks = {
|
|
headers: {},
|
|
getHeader: () => ({}),
|
|
setHeader: () => {},
|
|
hasHeader: () => false,
|
|
removeHeader: () => {},
|
|
getHeaderNames: () => []
|
|
}
|
|
|
|
const req = {
|
|
url: path,
|
|
...headerMocks
|
|
}
|
|
const res = {
|
|
...headerMocks
|
|
}
|
|
|
|
envConfig.setConfig({
|
|
serverRuntimeConfig,
|
|
publicRuntimeConfig: renderOpts.runtimeConfig
|
|
})
|
|
|
|
let htmlFilename = `${filePath}${sep}index.html`
|
|
if (!subFolders) htmlFilename = `${filePath}.html`
|
|
|
|
const pageExt = extname(page)
|
|
const pathExt = extname(path)
|
|
// Make sure page isn't a folder with a dot in the name e.g. `v1.2`
|
|
if (pageExt !== pathExt && pathExt !== '') {
|
|
// If the path has an extension, use that as the filename instead
|
|
htmlFilename = path
|
|
} else if (path === '/') {
|
|
// If the path is the root, just use index.html
|
|
htmlFilename = 'index.html'
|
|
}
|
|
|
|
const baseDir = join(outDir, dirname(htmlFilename))
|
|
const htmlFilepath = join(outDir, htmlFilename)
|
|
|
|
await mkdirp(baseDir)
|
|
let html
|
|
let curRenderOpts = {}
|
|
let renderMethod = renderToHTML
|
|
|
|
// eslint-disable-next-line camelcase
|
|
const renderedDuringBuild = unstable_getStaticProps => {
|
|
// eslint-disable-next-line camelcase
|
|
return !buildExport && unstable_getStaticProps && !isDynamicRoute(path)
|
|
}
|
|
|
|
if (serverless) {
|
|
const mod = require(join(
|
|
distDir,
|
|
'serverless/pages',
|
|
(page === '/' ? 'index' : page) + '.js'
|
|
))
|
|
|
|
// for non-dynamic SPR pages we should have already
|
|
// prerendered the file
|
|
if (renderedDuringBuild(mod.unstable_getStaticProps)) return results
|
|
|
|
renderMethod = mod.renderReqToHTML
|
|
const result = await renderMethod(req, res, true)
|
|
curRenderOpts = result.renderOpts || {}
|
|
html = result.html
|
|
|
|
if (!html) {
|
|
throw new Error(`Failed to render serverless page`)
|
|
}
|
|
} else {
|
|
const components = await loadComponents(
|
|
distDir,
|
|
buildId,
|
|
page,
|
|
serverless
|
|
)
|
|
|
|
// for non-dynamic SPR pages we should have already
|
|
// prerendered the file
|
|
if (renderedDuringBuild(components.unstable_getStaticProps)) {
|
|
return results
|
|
}
|
|
|
|
if (typeof components.Component === 'string') {
|
|
html = components.Component
|
|
} else {
|
|
curRenderOpts = { ...components, ...renderOpts, ampPath }
|
|
html = await renderMethod(req, res, page, query, curRenderOpts)
|
|
}
|
|
}
|
|
|
|
const validateAmp = async (html, page) => {
|
|
const validator = await AmpHtmlValidator.getInstance()
|
|
const result = validator.validateString(html)
|
|
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,
|
|
result: {
|
|
errors,
|
|
warnings
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
if (curRenderOpts.inAmpMode) {
|
|
await validateAmp(html, path)
|
|
} else if (curRenderOpts.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 accessP(ampHtmlFilepath)
|
|
} catch (_) {
|
|
// make sure it doesn't exist from manual mapping
|
|
let ampHtml
|
|
if (serverless) {
|
|
req.url += (req.url.includes('?') ? '&' : '?') + 'amp=1'
|
|
ampHtml = (await renderMethod(req, res, true)).html
|
|
} else {
|
|
ampHtml = await renderMethod(
|
|
req,
|
|
res,
|
|
page,
|
|
{ ...query, amp: 1 },
|
|
curRenderOpts
|
|
)
|
|
}
|
|
|
|
await validateAmp(ampHtml, page + '?amp=1')
|
|
await mkdirp(ampBaseDir)
|
|
await writeFileP(ampHtmlFilepath, ampHtml, 'utf8')
|
|
}
|
|
}
|
|
|
|
if (curRenderOpts.sprData) {
|
|
const dataFile = join(
|
|
sprDataDir,
|
|
htmlFilename.replace(/\.html$/, '.json')
|
|
)
|
|
|
|
await mkdirp(dirname(dataFile))
|
|
await writeFileP(dataFile, JSON.stringify(curRenderOpts.sprData), 'utf8')
|
|
}
|
|
results.fromBuildExportRevalidate = curRenderOpts.revalidate
|
|
|
|
await writeFileP(htmlFilepath, html, 'utf8')
|
|
return results
|
|
} catch (error) {
|
|
console.error(`\nError occurred prerendering ${path}:`, error)
|
|
return { ...results, error: true }
|
|
}
|
|
}
|