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
289 lines
7.9 KiB
TypeScript
289 lines
7.9 KiB
TypeScript
import chalk from 'chalk'
|
||
import fs from 'fs'
|
||
import textTable from 'next/dist/compiled/text-table'
|
||
import path from 'path'
|
||
import stripAnsi from 'strip-ansi'
|
||
import { promisify } from 'util'
|
||
|
||
import { isValidElementType } from 'react-is'
|
||
import prettyBytes from '../lib/pretty-bytes'
|
||
import { recursiveReadDir } from '../lib/recursive-readdir'
|
||
import { getPageChunks } from './webpack/plugins/chunk-graph-plugin'
|
||
import { isDynamicRoute } from '../next-server/lib/router/utils/is-dynamic'
|
||
import { getRouteMatcher, getRouteRegex } from '../next-server/lib/router/utils'
|
||
import { SPR_GET_INITIAL_PROPS_CONFLICT } from '../lib/constants'
|
||
|
||
const fsStatPromise = promisify(fs.stat)
|
||
const fileStats: { [k: string]: Promise<fs.Stats> } = {}
|
||
const fsStat = (file: string) => {
|
||
if (fileStats[file]) return fileStats[file]
|
||
|
||
fileStats[file] = fsStatPromise(file)
|
||
|
||
return fileStats[file]
|
||
}
|
||
|
||
export function collectPages(
|
||
directory: string,
|
||
pageExtensions: string[]
|
||
): Promise<string[]> {
|
||
return recursiveReadDir(
|
||
directory,
|
||
new RegExp(`\\.(?:${pageExtensions.join('|')})$`)
|
||
)
|
||
}
|
||
|
||
export interface PageInfo {
|
||
isAmp?: boolean
|
||
size: number
|
||
chunks?: ReturnType<typeof getPageChunks>
|
||
static?: boolean
|
||
serverBundle: string
|
||
}
|
||
|
||
export function printTreeView(
|
||
list: string[],
|
||
pageInfos: Map<string, PageInfo>,
|
||
serverless: boolean
|
||
) {
|
||
const getPrettySize = (_size: number): string => {
|
||
const size = prettyBytes(_size)
|
||
// green for 0-100kb
|
||
if (_size < 100 * 1000) return chalk.green(size)
|
||
// yellow for 100-250kb
|
||
if (_size < 250 * 1000) return chalk.yellow(size)
|
||
// red for >= 250kb
|
||
return chalk.red.bold(size)
|
||
}
|
||
|
||
const messages: string[][] = [
|
||
['Page', 'Size', 'Files', 'Packages'].map(entry => chalk.underline(entry)),
|
||
]
|
||
|
||
list
|
||
.sort((a, b) => a.localeCompare(b))
|
||
.forEach((item, i) => {
|
||
const symbol =
|
||
i === 0
|
||
? list.length === 1
|
||
? '─'
|
||
: '┌'
|
||
: i === list.length - 1
|
||
? '└'
|
||
: '├'
|
||
|
||
const pageInfo = pageInfos.get(item)
|
||
|
||
messages.push([
|
||
`${symbol} ${
|
||
item.startsWith('/_')
|
||
? ' '
|
||
: pageInfo && pageInfo.static
|
||
? chalk.bold('⚡')
|
||
: serverless
|
||
? 'λ'
|
||
: 'σ'
|
||
} ${item}`,
|
||
...(pageInfo
|
||
? [
|
||
pageInfo.isAmp
|
||
? chalk.cyan('AMP')
|
||
: pageInfo.size >= 0
|
||
? getPrettySize(pageInfo.size)
|
||
: '',
|
||
pageInfo.chunks ? pageInfo.chunks.internal.size.toString() : '',
|
||
pageInfo.chunks ? pageInfo.chunks.external.size.toString() : '',
|
||
]
|
||
: ['', '', '']),
|
||
])
|
||
})
|
||
|
||
console.log(
|
||
textTable(messages, {
|
||
align: ['l', 'l', 'r', 'r'],
|
||
stringLength: str => stripAnsi(str).length,
|
||
})
|
||
)
|
||
|
||
console.log()
|
||
console.log(
|
||
textTable(
|
||
[
|
||
serverless
|
||
? [
|
||
'λ',
|
||
'(Lambda)',
|
||
`page was emitted as a lambda (i.e. ${chalk.cyan(
|
||
'getInitialProps'
|
||
)})`,
|
||
]
|
||
: [
|
||
'σ',
|
||
'(Server)',
|
||
`page will be server rendered (i.e. ${chalk.cyan(
|
||
'getInitialProps'
|
||
)})`,
|
||
],
|
||
[
|
||
chalk.bold('⚡'),
|
||
'(Static File)',
|
||
'page was prerendered as static HTML',
|
||
],
|
||
],
|
||
{
|
||
align: ['l', 'l', 'l'],
|
||
stringLength: str => stripAnsi(str).length,
|
||
}
|
||
)
|
||
)
|
||
|
||
console.log()
|
||
}
|
||
|
||
export async function getPageSizeInKb(
|
||
page: string,
|
||
distPath: string,
|
||
buildId: string,
|
||
buildManifest: { pages: { [k: string]: string[] } },
|
||
isModern: boolean
|
||
): Promise<number> {
|
||
const clientBundle = path.join(
|
||
distPath,
|
||
`static/${buildId}/pages/`,
|
||
`${page}${isModern ? '.module' : ''}.js`
|
||
)
|
||
|
||
// With granularChunks flag enabled, each page may have additional chunks that it depends on
|
||
const baseDeps = page === '/_app' ? [] : buildManifest.pages['/_app']
|
||
|
||
// Get the list of chunks specific to this page
|
||
// With granularChunks: false, this will be []
|
||
const deps = (buildManifest.pages[page] || [])
|
||
.filter(
|
||
dep => !baseDeps.includes(dep) && /\.module\.js$/.test(dep) === isModern
|
||
)
|
||
.map(dep => `${distPath}/${dep}`)
|
||
|
||
// Add the main bundle for the page
|
||
deps.push(clientBundle)
|
||
|
||
try {
|
||
let depStats = await Promise.all(deps.map(fsStat))
|
||
|
||
return depStats.reduce((size, stat) => size + stat.size, 0)
|
||
} catch (_) {}
|
||
return -1
|
||
}
|
||
|
||
export async function isPageStatic(
|
||
page: string,
|
||
serverBundle: string,
|
||
runtimeEnvConfig: any
|
||
): Promise<{
|
||
static?: boolean
|
||
prerender?: boolean
|
||
isHybridAmp?: boolean
|
||
prerenderRoutes?: string[] | undefined
|
||
}> {
|
||
try {
|
||
require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig)
|
||
const mod = require(serverBundle)
|
||
const Comp = mod.default || mod
|
||
|
||
if (!Comp || !isValidElementType(Comp) || typeof Comp === 'string') {
|
||
throw new Error('INVALID_DEFAULT_EXPORT')
|
||
}
|
||
|
||
const hasGetInitialProps = !!(Comp as any).getInitialProps
|
||
const hasStaticProps = !!mod.unstable_getStaticProps
|
||
const hasStaticParams = !!mod.unstable_getStaticParams
|
||
|
||
// A page cannot be prerendered _and_ define a data requirement. That's
|
||
// contradictory!
|
||
if (hasGetInitialProps && hasStaticProps) {
|
||
throw new Error(SPR_GET_INITIAL_PROPS_CONFLICT)
|
||
}
|
||
|
||
// A page cannot have static parameters if it is not a dynamic page.
|
||
if (hasStaticProps && hasStaticParams && !isDynamicRoute(page)) {
|
||
throw new Error(
|
||
`unstable_getStaticParams can only be used with dynamic pages. https://nextjs.org/docs#dynamic-routing`
|
||
)
|
||
}
|
||
|
||
let prerenderPaths: string[] | undefined
|
||
if (hasStaticProps && hasStaticParams) {
|
||
prerenderPaths = [] as string[]
|
||
|
||
const _routeRegex = getRouteRegex(page)
|
||
const _routeMatcher = getRouteMatcher(_routeRegex)
|
||
|
||
// Get the default list of allowed params.
|
||
const _validParamKeys = Object.keys(_routeMatcher(page))
|
||
|
||
const toPrerender: Array<
|
||
{ [key: string]: string } | string
|
||
> = await mod.unstable_getStaticParams()
|
||
toPrerender.forEach(entry => {
|
||
// For a string-provided path, we must make sure it matches the dynamic
|
||
// route.
|
||
if (typeof entry === 'string') {
|
||
const result = _routeMatcher(entry)
|
||
if (!result) {
|
||
throw new Error(
|
||
`The provided path \`${entry}\` does not match the page: \`${page}\`.`
|
||
)
|
||
}
|
||
|
||
prerenderPaths!.push(entry)
|
||
}
|
||
// For the object-provided path, we must make sure it specifies all
|
||
// required keys.
|
||
else {
|
||
let builtPage = page
|
||
_validParamKeys.forEach(validParamKey => {
|
||
if (typeof entry[validParamKey] !== 'string') {
|
||
throw new Error(
|
||
`A required parameter (${validParamKey}) was not provided as a string.`
|
||
)
|
||
}
|
||
|
||
builtPage = builtPage.replace(
|
||
`[${validParamKey}]`,
|
||
encodeURIComponent(entry[validParamKey])
|
||
)
|
||
})
|
||
|
||
prerenderPaths!.push(builtPage)
|
||
}
|
||
})
|
||
}
|
||
|
||
const config = mod.config || {}
|
||
return {
|
||
static: !hasStaticProps && !hasGetInitialProps,
|
||
isHybridAmp: config.amp === 'hybrid',
|
||
prerenderRoutes: prerenderPaths,
|
||
prerender: hasStaticProps,
|
||
}
|
||
} catch (err) {
|
||
if (err.code === 'MODULE_NOT_FOUND') return {}
|
||
throw err
|
||
}
|
||
}
|
||
|
||
export function hasCustomAppGetInitialProps(
|
||
_appBundle: string,
|
||
runtimeEnvConfig: any
|
||
): boolean {
|
||
require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig)
|
||
let mod = require(_appBundle)
|
||
|
||
if (_appBundle.endsWith('_app.js')) {
|
||
mod = mod.default || mod
|
||
} else {
|
||
// since we don't output _app in serverless mode get it from a page
|
||
mod = mod._app
|
||
}
|
||
return mod.getInitialProps !== mod.origGetInitialProps
|
||
}
|