rsnext/packages/next/build/utils.ts
JJ Kasper 85e720a092 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

289 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}