2019-05-14 17:11:22 +02:00
|
|
|
import chalk from 'chalk'
|
2019-12-14 07:39:59 +01:00
|
|
|
import gzipSize from 'gzip-size'
|
2019-05-14 17:11:22 +02:00
|
|
|
import textTable from 'next/dist/compiled/text-table'
|
2019-04-10 18:22:10 +02:00
|
|
|
import path from 'path'
|
2019-11-04 17:14:04 +01:00
|
|
|
import { isValidElementType } from 'react-is'
|
2019-05-14 17:11:22 +02:00
|
|
|
import stripAnsi from 'strip-ansi'
|
2019-12-12 17:20:24 +01:00
|
|
|
import { Redirect, Rewrite } from '../lib/check-custom-routes'
|
2019-11-04 17:14:04 +01:00
|
|
|
import { SPR_GET_INITIAL_PROPS_CONFLICT } from '../lib/constants'
|
2019-05-01 22:31:08 +02:00
|
|
|
import prettyBytes from '../lib/pretty-bytes'
|
2019-04-10 05:15:35 +02:00
|
|
|
import { recursiveReadDir } from '../lib/recursive-readdir'
|
2019-12-12 17:20:24 +01:00
|
|
|
import { DEFAULT_REDIRECT_STATUS } from '../next-server/lib/constants'
|
2019-09-24 10:50:04 +02:00
|
|
|
import { getRouteMatcher, getRouteRegex } from '../next-server/lib/router/utils'
|
2019-11-04 17:14:04 +01:00
|
|
|
import { isDynamicRoute } from '../next-server/lib/router/utils/is-dynamic'
|
2019-12-12 17:20:24 +01:00
|
|
|
import { findPageFile } from '../server/lib/find-page-file'
|
2019-04-10 05:15:35 +02:00
|
|
|
|
2019-12-14 07:39:59 +01:00
|
|
|
const fileGzipStats: { [k: string]: Promise<number> } = {}
|
|
|
|
const fsStatGzip = (file: string) => {
|
|
|
|
if (fileGzipStats[file]) return fileGzipStats[file]
|
|
|
|
fileGzipStats[file] = gzipSize.file(file)
|
|
|
|
return fileGzipStats[file]
|
2019-09-19 18:16:51 +02:00
|
|
|
}
|
2019-04-10 21:19:50 +02:00
|
|
|
|
2019-04-10 05:15:35 +02:00
|
|
|
export function collectPages(
|
|
|
|
directory: string,
|
|
|
|
pageExtensions: string[]
|
|
|
|
): Promise<string[]> {
|
|
|
|
return recursiveReadDir(
|
|
|
|
directory,
|
|
|
|
new RegExp(`\\.(?:${pageExtensions.join('|')})$`)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-05-14 17:11:22 +02:00
|
|
|
export interface PageInfo {
|
|
|
|
isAmp?: boolean
|
2019-12-14 20:23:04 +01:00
|
|
|
isHybridAmp?: boolean
|
2019-05-14 17:11:22 +02:00
|
|
|
size: number
|
2019-12-12 10:45:45 +01:00
|
|
|
static: boolean
|
|
|
|
isSsg: boolean
|
|
|
|
ssgPageRoutes: string[] | null
|
2019-05-22 18:36:53 +02:00
|
|
|
serverBundle: string
|
2019-05-01 22:31:08 +02:00
|
|
|
}
|
|
|
|
|
2019-12-12 17:20:24 +01:00
|
|
|
export async function printTreeView(
|
|
|
|
list: readonly string[],
|
2019-06-24 20:59:51 +02:00
|
|
|
pageInfos: Map<string, PageInfo>,
|
2019-12-12 17:20:24 +01:00
|
|
|
serverless: boolean,
|
2019-12-12 20:44:34 +01:00
|
|
|
{
|
|
|
|
distPath,
|
|
|
|
pagesDir,
|
|
|
|
pageExtensions,
|
|
|
|
buildManifest,
|
|
|
|
isModern,
|
|
|
|
}: {
|
|
|
|
distPath: string
|
|
|
|
pagesDir: string
|
|
|
|
pageExtensions: string[]
|
|
|
|
buildManifest: BuildManifestShape
|
|
|
|
isModern: boolean
|
|
|
|
}
|
2019-05-14 17:11:22 +02:00
|
|
|
) {
|
|
|
|
const getPrettySize = (_size: number): string => {
|
|
|
|
const size = prettyBytes(_size)
|
2019-12-14 07:39:59 +01:00
|
|
|
// green for 0-130kb
|
|
|
|
if (_size < 130 * 1000) return chalk.green(size)
|
|
|
|
// yellow for 130-170kb
|
|
|
|
if (_size < 170 * 1000) return chalk.yellow(size)
|
|
|
|
// red for >= 170kb
|
2019-05-14 17:11:22 +02:00
|
|
|
return chalk.red.bold(size)
|
2019-05-01 22:31:08 +02:00
|
|
|
}
|
|
|
|
|
2019-12-12 10:45:45 +01:00
|
|
|
const messages: [string, string][] = [
|
|
|
|
['Page', 'Size'].map(entry => chalk.underline(entry)) as [string, string],
|
2019-05-14 17:11:22 +02:00
|
|
|
]
|
|
|
|
|
2019-12-12 17:20:24 +01:00
|
|
|
const hasCustomApp = await findPageFile(pagesDir, '/_app', pageExtensions)
|
|
|
|
const hasCustomError = await findPageFile(pagesDir, '/_error', pageExtensions)
|
|
|
|
|
|
|
|
const pageList = list
|
|
|
|
.slice()
|
|
|
|
.filter(
|
|
|
|
e =>
|
|
|
|
!(
|
|
|
|
e === '/_document' ||
|
|
|
|
(!hasCustomApp && e === '/_app') ||
|
|
|
|
(!hasCustomError && e === '/_error')
|
|
|
|
)
|
|
|
|
)
|
2019-05-14 17:11:22 +02:00
|
|
|
.sort((a, b) => a.localeCompare(b))
|
2019-12-12 17:20:24 +01:00
|
|
|
|
|
|
|
pageList.forEach((item, i, arr) => {
|
|
|
|
const symbol =
|
|
|
|
i === 0
|
|
|
|
? arr.length === 1
|
|
|
|
? '─'
|
|
|
|
: '┌'
|
|
|
|
: i === arr.length - 1
|
|
|
|
? '└'
|
|
|
|
: '├'
|
|
|
|
|
|
|
|
const pageInfo = pageInfos.get(item)
|
|
|
|
|
|
|
|
messages.push([
|
|
|
|
`${symbol} ${
|
2019-12-13 14:45:15 +01:00
|
|
|
item === '/_app'
|
2019-12-12 17:20:24 +01:00
|
|
|
? ' '
|
|
|
|
: pageInfo && pageInfo.static
|
|
|
|
? '○'
|
|
|
|
: pageInfo && pageInfo.isSsg
|
|
|
|
? '●'
|
|
|
|
: 'λ'
|
|
|
|
} ${item}`,
|
|
|
|
pageInfo
|
|
|
|
? pageInfo.isAmp
|
|
|
|
? chalk.cyan('AMP')
|
|
|
|
: pageInfo.size >= 0
|
|
|
|
? getPrettySize(pageInfo.size)
|
|
|
|
: ''
|
|
|
|
: '',
|
|
|
|
])
|
|
|
|
|
|
|
|
if (pageInfo && pageInfo.ssgPageRoutes && pageInfo.ssgPageRoutes.length) {
|
|
|
|
const totalRoutes = pageInfo.ssgPageRoutes.length
|
|
|
|
const previewPages = totalRoutes === 4 ? 4 : 3
|
|
|
|
const contSymbol = i === arr.length - 1 ? ' ' : '├'
|
|
|
|
|
|
|
|
const routes = pageInfo.ssgPageRoutes.slice(0, previewPages)
|
|
|
|
if (totalRoutes > previewPages) {
|
|
|
|
const remaining = totalRoutes - previewPages
|
|
|
|
routes.push(`[+${remaining} more paths]`)
|
2019-12-12 10:45:45 +01:00
|
|
|
}
|
2019-12-12 17:20:24 +01:00
|
|
|
|
|
|
|
routes.forEach((slug, index, { length }) => {
|
|
|
|
const innerSymbol = index === length - 1 ? '└' : '├'
|
|
|
|
messages.push([`${contSymbol} ${innerSymbol} ${slug}`, ''])
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
2019-05-01 22:31:08 +02:00
|
|
|
|
2019-12-14 20:23:04 +01:00
|
|
|
const sharedData = await getSharedSizes(
|
|
|
|
distPath,
|
|
|
|
buildManifest,
|
|
|
|
isModern,
|
|
|
|
pageInfos
|
|
|
|
)
|
2019-12-12 20:44:34 +01:00
|
|
|
|
|
|
|
messages.push(['+ shared by all', getPrettySize(sharedData.total)])
|
|
|
|
Object.keys(sharedData.files)
|
|
|
|
.sort()
|
|
|
|
.forEach((fileName, index, { length }) => {
|
|
|
|
const innerSymbol = index === length - 1 ? '└' : '├'
|
|
|
|
messages.push([
|
|
|
|
` ${innerSymbol} ${fileName
|
|
|
|
.replace(/^static\//, '')
|
|
|
|
.replace(/[.-][0-9a-z]{20}(?=\.)/, '')}`,
|
|
|
|
getPrettySize(sharedData.files[fileName]),
|
|
|
|
])
|
|
|
|
})
|
|
|
|
|
2019-05-14 17:11:22 +02:00
|
|
|
console.log(
|
|
|
|
textTable(messages, {
|
2019-12-12 10:45:45 +01:00
|
|
|
align: ['l', 'l'],
|
2019-05-14 17:11:22 +02:00
|
|
|
stringLength: str => stripAnsi(str).length,
|
2019-04-10 05:15:35 +02:00
|
|
|
})
|
2019-05-14 17:11:22 +02:00
|
|
|
)
|
2019-04-10 05:15:35 +02:00
|
|
|
|
2019-06-10 20:59:36 +02:00
|
|
|
console.log()
|
|
|
|
console.log(
|
|
|
|
textTable(
|
|
|
|
[
|
2019-12-12 10:45:45 +01:00
|
|
|
[
|
|
|
|
'λ',
|
|
|
|
serverless ? '(Lambda)' : '(Server)',
|
|
|
|
`server-side renders at runtime (uses ${chalk.cyan(
|
|
|
|
'getInitialProps'
|
|
|
|
)} or ${chalk.cyan('getServerProps')})`,
|
|
|
|
],
|
|
|
|
[
|
|
|
|
'○',
|
|
|
|
'(Static)',
|
|
|
|
'automatically rendered as static HTML (uses no initial props)',
|
|
|
|
],
|
|
|
|
[
|
|
|
|
'●',
|
|
|
|
'(SSG)',
|
|
|
|
`automatically generated as static HTML + JSON (uses ${chalk.cyan(
|
|
|
|
'getStaticProps'
|
|
|
|
)})`,
|
|
|
|
],
|
|
|
|
] as [string, string, string][],
|
2019-06-10 20:59:36 +02:00
|
|
|
{
|
|
|
|
align: ['l', 'l', 'l'],
|
|
|
|
stringLength: str => stripAnsi(str).length,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2019-04-10 05:15:35 +02:00
|
|
|
console.log()
|
|
|
|
}
|
|
|
|
|
2019-11-26 10:33:47 +01:00
|
|
|
export function printCustomRoutes({
|
|
|
|
redirects,
|
|
|
|
rewrites,
|
|
|
|
}: {
|
|
|
|
redirects: Redirect[]
|
|
|
|
rewrites: Rewrite[]
|
|
|
|
}) {
|
|
|
|
const printRoutes = (
|
|
|
|
routes: Redirect[] | Rewrite[],
|
|
|
|
type: 'Redirects' | 'Rewrites'
|
|
|
|
) => {
|
|
|
|
const isRedirects = type === 'Redirects'
|
|
|
|
console.log(chalk.underline(type))
|
|
|
|
console.log()
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
textTable(
|
|
|
|
[
|
|
|
|
[
|
|
|
|
'Source',
|
|
|
|
'Destination',
|
|
|
|
...(isRedirects ? ['statusCode'] : []),
|
|
|
|
].map(str => chalk.bold(str)),
|
|
|
|
...Object.entries(routes).map(([key, route]) => {
|
|
|
|
return [
|
|
|
|
route.source,
|
|
|
|
route.destination,
|
|
|
|
...(isRedirects
|
|
|
|
? [
|
|
|
|
((route as Redirect).statusCode ||
|
|
|
|
DEFAULT_REDIRECT_STATUS) + '',
|
|
|
|
]
|
|
|
|
: []),
|
|
|
|
]
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
{
|
|
|
|
align: ['l', 'l', 'l'],
|
|
|
|
stringLength: str => stripAnsi(str).length,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
console.log()
|
|
|
|
}
|
|
|
|
|
|
|
|
if (redirects.length) {
|
|
|
|
printRoutes(redirects, 'Redirects')
|
|
|
|
}
|
|
|
|
if (rewrites.length) {
|
|
|
|
printRoutes(rewrites, 'Rewrites')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-12 20:44:34 +01:00
|
|
|
type BuildManifestShape = { pages: { [k: string]: string[] } }
|
|
|
|
type ComputeManifestShape = {
|
|
|
|
commonFiles: string[]
|
|
|
|
sizeCommonFile: { [file: string]: number }
|
|
|
|
sizeCommonFiles: number
|
|
|
|
}
|
|
|
|
|
|
|
|
let cachedBuildManifest: BuildManifestShape | undefined
|
|
|
|
|
|
|
|
let lastCompute: ComputeManifestShape | undefined
|
|
|
|
let lastComputeModern: boolean | undefined
|
2019-12-14 20:23:04 +01:00
|
|
|
let lastComputePageInfo: boolean | undefined
|
2019-12-12 20:44:34 +01:00
|
|
|
|
|
|
|
async function computeFromManifest(
|
|
|
|
manifest: BuildManifestShape,
|
|
|
|
distPath: string,
|
2019-12-14 20:23:04 +01:00
|
|
|
isModern: boolean,
|
|
|
|
pageInfos?: Map<string, PageInfo>
|
2019-12-12 20:44:34 +01:00
|
|
|
): Promise<ComputeManifestShape> {
|
|
|
|
if (
|
|
|
|
Object.is(cachedBuildManifest, manifest) &&
|
2019-12-14 20:23:04 +01:00
|
|
|
lastComputeModern === isModern &&
|
|
|
|
lastComputePageInfo === !!pageInfos
|
2019-12-12 20:44:34 +01:00
|
|
|
) {
|
|
|
|
return lastCompute!
|
|
|
|
}
|
|
|
|
|
|
|
|
let expected = 0
|
|
|
|
const files = new Map<string, number>()
|
|
|
|
Object.keys(manifest.pages).forEach(key => {
|
|
|
|
if (key === '/_polyfills') {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-12-14 20:23:04 +01:00
|
|
|
if (pageInfos) {
|
|
|
|
const cleanKey = key.replace(/\/index$/, '') || '/'
|
|
|
|
const pageInfo = pageInfos.get(cleanKey)
|
|
|
|
// don't include AMP pages since they don't rely on shared bundles
|
|
|
|
if (pageInfo?.isHybridAmp || pageInfo?.isAmp) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-12 20:44:34 +01:00
|
|
|
++expected
|
|
|
|
manifest.pages[key].forEach(file => {
|
|
|
|
if (
|
|
|
|
// Filter out CSS
|
|
|
|
!file.endsWith('.js') ||
|
|
|
|
// Select Modern or Legacy scripts
|
|
|
|
file.endsWith('.module.js') !== isModern
|
|
|
|
) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (files.has(file)) {
|
|
|
|
files.set(file, files.get(file)! + 1)
|
|
|
|
} else {
|
|
|
|
files.set(file, 1)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
const commonFiles = [...files.entries()]
|
|
|
|
.filter(([, len]) => len === expected)
|
|
|
|
.map(([f]) => f)
|
|
|
|
|
2019-12-14 07:39:59 +01:00
|
|
|
let stats: [string, number][]
|
2019-12-12 20:44:34 +01:00
|
|
|
try {
|
|
|
|
stats = await Promise.all(
|
|
|
|
commonFiles.map(
|
|
|
|
async f =>
|
2019-12-14 07:39:59 +01:00
|
|
|
[f, await fsStatGzip(path.join(distPath, f))] as [string, number]
|
2019-12-12 20:44:34 +01:00
|
|
|
)
|
|
|
|
)
|
|
|
|
} catch (_) {
|
|
|
|
stats = []
|
|
|
|
}
|
|
|
|
|
|
|
|
lastCompute = {
|
|
|
|
commonFiles,
|
|
|
|
sizeCommonFile: stats.reduce(
|
2019-12-14 07:39:59 +01:00
|
|
|
(obj, n) => Object.assign(obj, { [n[0]]: n[1] }),
|
2019-12-12 20:44:34 +01:00
|
|
|
{}
|
|
|
|
),
|
2019-12-14 07:39:59 +01:00
|
|
|
sizeCommonFiles: stats.reduce((size, [, stat]) => size + stat, 0),
|
2019-12-12 20:44:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
cachedBuildManifest = manifest
|
|
|
|
lastComputeModern = isModern
|
2019-12-14 20:23:04 +01:00
|
|
|
lastComputePageInfo = !!pageInfos
|
2019-12-12 20:44:34 +01:00
|
|
|
return lastCompute!
|
|
|
|
}
|
|
|
|
|
|
|
|
function difference<T>(main: T[], sub: T[]): T[] {
|
|
|
|
const a = new Set(main)
|
|
|
|
const b = new Set(sub)
|
|
|
|
return [...a].filter(x => !b.has(x))
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getSharedSizes(
|
|
|
|
distPath: string,
|
|
|
|
buildManifest: BuildManifestShape,
|
2019-12-14 20:23:04 +01:00
|
|
|
isModern: boolean,
|
|
|
|
pageInfos: Map<string, PageInfo>
|
2019-12-12 20:44:34 +01:00
|
|
|
): Promise<{ total: number; files: { [page: string]: number } }> {
|
2019-12-14 20:23:04 +01:00
|
|
|
const data = await computeFromManifest(
|
|
|
|
buildManifest,
|
|
|
|
distPath,
|
|
|
|
isModern,
|
|
|
|
pageInfos
|
|
|
|
)
|
2019-12-12 20:44:34 +01:00
|
|
|
return { total: data.sizeCommonFiles, files: data.sizeCommonFile }
|
|
|
|
}
|
|
|
|
|
2019-05-14 17:11:22 +02:00
|
|
|
export async function getPageSizeInKb(
|
2019-05-01 22:31:08 +02:00
|
|
|
page: string,
|
|
|
|
distPath: string,
|
2019-09-19 18:16:51 +02:00
|
|
|
buildId: string,
|
2019-12-12 20:44:34 +01:00
|
|
|
buildManifest: BuildManifestShape,
|
2019-09-19 18:16:51 +02:00
|
|
|
isModern: boolean
|
2019-05-14 17:11:22 +02:00
|
|
|
): Promise<number> {
|
2019-12-12 20:44:34 +01:00
|
|
|
const data = await computeFromManifest(buildManifest, distPath, isModern)
|
|
|
|
const deps = difference(buildManifest.pages[page] || [], data.commonFiles)
|
|
|
|
.filter(
|
|
|
|
entry =>
|
|
|
|
entry.endsWith('.js') && entry.endsWith('.module.js') === isModern
|
|
|
|
)
|
|
|
|
.map(dep => `${distPath}/${dep}`)
|
|
|
|
|
2019-05-01 22:31:08 +02:00
|
|
|
const clientBundle = path.join(
|
2019-05-14 17:11:22 +02:00
|
|
|
distPath,
|
|
|
|
`static/${buildId}/pages/`,
|
2019-09-19 18:16:51 +02:00
|
|
|
`${page}${isModern ? '.module' : ''}.js`
|
2019-05-01 22:31:08 +02:00
|
|
|
)
|
2019-09-19 18:16:51 +02:00
|
|
|
deps.push(clientBundle)
|
|
|
|
|
2019-05-14 17:11:22 +02:00
|
|
|
try {
|
2019-12-14 07:39:59 +01:00
|
|
|
let depStats = await Promise.all(deps.map(fsStatGzip))
|
|
|
|
return depStats.reduce((size, stat) => size + stat, 0)
|
2019-05-14 17:11:22 +02:00
|
|
|
} catch (_) {}
|
|
|
|
return -1
|
2019-05-01 22:31:08 +02:00
|
|
|
}
|
2019-05-22 18:36:53 +02:00
|
|
|
|
2019-09-24 10:50:04 +02:00
|
|
|
export async function isPageStatic(
|
|
|
|
page: string,
|
2019-05-22 18:36:53 +02:00
|
|
|
serverBundle: string,
|
2019-05-29 13:57:26 +02:00
|
|
|
runtimeEnvConfig: any
|
2019-09-24 10:50:04 +02:00
|
|
|
): Promise<{
|
|
|
|
static?: boolean
|
|
|
|
prerender?: boolean
|
|
|
|
isHybridAmp?: boolean
|
|
|
|
prerenderRoutes?: string[] | undefined
|
|
|
|
}> {
|
2019-05-22 18:36:53 +02:00
|
|
|
try {
|
2019-09-04 16:00:54 +02:00
|
|
|
require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig)
|
2019-07-01 23:13:52 +02:00
|
|
|
const mod = require(serverBundle)
|
2019-07-09 19:23:38 +02:00
|
|
|
const Comp = mod.default || mod
|
2019-06-25 22:36:21 +02:00
|
|
|
|
2019-06-14 02:08:19 +02:00
|
|
|
if (!Comp || !isValidElementType(Comp) || typeof Comp === 'string') {
|
2019-06-26 04:54:28 +02:00
|
|
|
throw new Error('INVALID_DEFAULT_EXPORT')
|
2019-05-22 18:36:53 +02:00
|
|
|
}
|
2019-07-01 23:13:52 +02:00
|
|
|
|
2019-09-24 10:50:04 +02:00
|
|
|
const hasGetInitialProps = !!(Comp as any).getInitialProps
|
|
|
|
const hasStaticProps = !!mod.unstable_getStaticProps
|
2019-11-28 04:46:16 +01:00
|
|
|
const hasStaticPaths = !!mod.unstable_getStaticPaths
|
|
|
|
const hasLegacyStaticParams = !!mod.unstable_getStaticParams
|
|
|
|
|
|
|
|
if (hasLegacyStaticParams) {
|
|
|
|
throw new Error(
|
|
|
|
`unstable_getStaticParams was replaced with unstable_getStaticPaths. Please update your code.`
|
|
|
|
)
|
|
|
|
}
|
2019-09-24 10:50:04 +02:00
|
|
|
|
|
|
|
// 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.
|
2019-11-28 04:46:16 +01:00
|
|
|
if (hasStaticProps && hasStaticPaths && !isDynamicRoute(page)) {
|
2019-09-24 10:50:04 +02:00
|
|
|
throw new Error(
|
2019-11-28 04:46:16 +01:00
|
|
|
`unstable_getStaticPaths can only be used with dynamic pages. https://nextjs.org/docs#dynamic-routing`
|
2019-09-24 10:50:04 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
let prerenderPaths: string[] | undefined
|
2019-11-28 04:46:16 +01:00
|
|
|
if (hasStaticProps && hasStaticPaths) {
|
2019-09-24 10:50:04 +02:00
|
|
|
prerenderPaths = [] as string[]
|
|
|
|
|
2019-12-02 22:43:30 +01:00
|
|
|
const _routeMatcher = getRouteMatcher(getRouteRegex(page))
|
2019-09-24 10:50:04 +02:00
|
|
|
|
|
|
|
// Get the default list of allowed params.
|
|
|
|
const _validParamKeys = Object.keys(_routeMatcher(page))
|
|
|
|
|
|
|
|
const toPrerender: Array<
|
2019-11-28 04:46:16 +01:00
|
|
|
{ params?: { [key: string]: string } } | string
|
|
|
|
> = await mod.unstable_getStaticPaths()
|
2019-09-24 10:50:04 +02:00
|
|
|
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 {
|
2019-11-28 04:46:16 +01:00
|
|
|
const invalidKeys = Object.keys(entry).filter(key => key !== 'params')
|
|
|
|
if (invalidKeys.length) {
|
|
|
|
throw new Error(
|
|
|
|
`Additional keys were returned from \`unstable_getStaticPaths\` in page "${page}". ` +
|
|
|
|
`URL Parameters intended for this dynamic route must be nested under the \`params\` key, i.e.:` +
|
|
|
|
`\n\n\treturn { params: { ${_validParamKeys
|
|
|
|
.map(k => `${k}: ...`)
|
|
|
|
.join(', ')} } }` +
|
2019-12-18 01:25:02 +01:00
|
|
|
`\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.\n`
|
2019-11-28 04:46:16 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const { params = {} } = entry
|
2019-09-24 10:50:04 +02:00
|
|
|
let builtPage = page
|
|
|
|
_validParamKeys.forEach(validParamKey => {
|
2019-11-28 04:46:16 +01:00
|
|
|
if (typeof params[validParamKey] !== 'string') {
|
2019-09-24 10:50:04 +02:00
|
|
|
throw new Error(
|
|
|
|
`A required parameter (${validParamKey}) was not provided as a string.`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
builtPage = builtPage.replace(
|
|
|
|
`[${validParamKey}]`,
|
2019-11-28 04:46:16 +01:00
|
|
|
encodeURIComponent(params[validParamKey])
|
2019-09-24 10:50:04 +02:00
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
prerenderPaths!.push(builtPage)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const config = mod.config || {}
|
2019-07-01 23:13:52 +02:00
|
|
|
return {
|
2019-09-24 10:50:04 +02:00
|
|
|
static: !hasStaticProps && !hasGetInitialProps,
|
2019-08-12 03:56:57 +02:00
|
|
|
isHybridAmp: config.amp === 'hybrid',
|
2019-09-24 10:50:04 +02:00
|
|
|
prerenderRoutes: prerenderPaths,
|
|
|
|
prerender: hasStaticProps,
|
2019-07-01 23:13:52 +02:00
|
|
|
}
|
2019-05-22 18:36:53 +02:00
|
|
|
} catch (err) {
|
2019-07-01 23:13:52 +02:00
|
|
|
if (err.code === 'MODULE_NOT_FOUND') return {}
|
2019-05-22 18:36:53 +02:00
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function hasCustomAppGetInitialProps(
|
|
|
|
_appBundle: string,
|
2019-05-29 13:57:26 +02:00
|
|
|
runtimeEnvConfig: any
|
2019-05-22 18:36:53 +02:00
|
|
|
): boolean {
|
2019-09-04 16:00:54 +02:00
|
|
|
require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig)
|
2019-05-22 18:36:53 +02:00
|
|
|
let mod = require(_appBundle)
|
|
|
|
|
|
|
|
if (_appBundle.endsWith('_app.js')) {
|
2019-07-09 19:23:38 +02:00
|
|
|
mod = mod.default || mod
|
2019-05-22 18:36:53 +02:00
|
|
|
} else {
|
|
|
|
// since we don't output _app in serverless mode get it from a page
|
|
|
|
mod = mod._app
|
|
|
|
}
|
|
|
|
return mod.getInitialProps !== mod.origGetInitialProps
|
|
|
|
}
|