2019-05-14 17:11:22 +02:00
|
|
|
|
import chalk from 'chalk'
|
2019-04-10 05:15:35 +02:00
|
|
|
|
import fs from 'fs'
|
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-04-10 21:19:50 +02:00
|
|
|
|
import { promisify } from 'util'
|
2019-05-14 17:11:22 +02:00
|
|
|
|
|
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-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-10 15:54:56 +01:00
|
|
|
|
import { Redirect, Rewrite } from '../lib/check-custom-routes'
|
2019-11-26 10:33:47 +01:00
|
|
|
|
import { DEFAULT_REDIRECT_STATUS } from '../next-server/lib/constants'
|
2019-04-10 05:15:35 +02:00
|
|
|
|
|
2019-09-19 18:16:51 +02:00
|
|
|
|
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]
|
|
|
|
|
}
|
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
|
|
|
|
|
size: number
|
2019-05-22 18:36:53 +02:00
|
|
|
|
static?: boolean
|
|
|
|
|
serverBundle: string
|
2019-05-01 22:31:08 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-14 17:11:22 +02:00
|
|
|
|
export function printTreeView(
|
|
|
|
|
list: string[],
|
2019-06-24 20:59:51 +02:00
|
|
|
|
pageInfos: Map<string, PageInfo>,
|
|
|
|
|
serverless: boolean
|
2019-05-14 17:11:22 +02:00
|
|
|
|
) {
|
|
|
|
|
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)
|
2019-05-01 22:31:08 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-14 17:11:22 +02:00
|
|
|
|
const messages: string[][] = [
|
2019-11-04 17:14:04 +01:00
|
|
|
|
['Page', 'Size'].map(entry => chalk.underline(entry)),
|
2019-05-14 17:11:22 +02:00
|
|
|
|
]
|
|
|
|
|
|
2019-04-10 05:15:35 +02:00
|
|
|
|
list
|
2019-05-14 17:11:22 +02:00
|
|
|
|
.sort((a, b) => a.localeCompare(b))
|
2019-04-10 05:15:35 +02:00
|
|
|
|
.forEach((item, i) => {
|
2019-05-14 17:11:22 +02:00
|
|
|
|
const symbol =
|
2019-04-10 05:15:35 +02:00
|
|
|
|
i === 0
|
|
|
|
|
? list.length === 1
|
|
|
|
|
? '─'
|
|
|
|
|
: '┌'
|
2019-05-14 17:11:22 +02:00
|
|
|
|
: i === list.length - 1
|
|
|
|
|
? '└'
|
2019-04-10 05:15:35 +02:00
|
|
|
|
: '├'
|
2019-05-01 22:31:08 +02:00
|
|
|
|
|
2019-05-14 17:11:22 +02:00
|
|
|
|
const pageInfo = pageInfos.get(item)
|
|
|
|
|
|
|
|
|
|
messages.push([
|
2019-06-10 20:59:36 +02:00
|
|
|
|
`${symbol} ${
|
|
|
|
|
item.startsWith('/_')
|
|
|
|
|
? ' '
|
|
|
|
|
: pageInfo && pageInfo.static
|
2019-11-04 17:14:04 +01:00
|
|
|
|
? '*'
|
2019-06-24 20:59:51 +02:00
|
|
|
|
: serverless
|
|
|
|
|
? 'λ'
|
|
|
|
|
: 'σ'
|
2019-06-10 20:59:36 +02:00
|
|
|
|
} ${item}`,
|
2019-11-04 17:14:04 +01:00
|
|
|
|
pageInfo
|
|
|
|
|
? pageInfo.isAmp
|
|
|
|
|
? chalk.cyan('AMP')
|
|
|
|
|
: pageInfo.size >= 0
|
|
|
|
|
? getPrettySize(pageInfo.size)
|
|
|
|
|
: ''
|
|
|
|
|
: '',
|
2019-05-14 17:11:22 +02:00
|
|
|
|
])
|
|
|
|
|
})
|
2019-05-01 22:31:08 +02:00
|
|
|
|
|
2019-05-14 17:11:22 +02:00
|
|
|
|
console.log(
|
|
|
|
|
textTable(messages, {
|
|
|
|
|
align: ['l', 'l', 'r', 'r'],
|
|
|
|
|
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-06-24 20:59:51 +02:00
|
|
|
|
serverless
|
|
|
|
|
? [
|
|
|
|
|
'λ',
|
|
|
|
|
'(Lambda)',
|
|
|
|
|
`page was emitted as a lambda (i.e. ${chalk.cyan(
|
|
|
|
|
'getInitialProps'
|
|
|
|
|
)})`,
|
|
|
|
|
]
|
|
|
|
|
: [
|
|
|
|
|
'σ',
|
|
|
|
|
'(Server)',
|
|
|
|
|
`page will be server rendered (i.e. ${chalk.cyan(
|
|
|
|
|
'getInitialProps'
|
|
|
|
|
)})`,
|
|
|
|
|
],
|
2019-11-04 17:14:04 +01:00
|
|
|
|
['*', '(Static File)', 'page was prerendered as static HTML'],
|
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-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,
|
|
|
|
|
buildManifest: { pages: { [k: string]: string[] } },
|
|
|
|
|
isModern: boolean
|
2019-05-14 17:11:22 +02:00
|
|
|
|
): Promise<number> {
|
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
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
2019-05-14 17:11:22 +02:00
|
|
|
|
try {
|
2019-09-19 18:16:51 +02:00
|
|
|
|
let depStats = await Promise.all(deps.map(fsStat))
|
|
|
|
|
|
|
|
|
|
return depStats.reduce((size, stat) => size + stat.size, 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(', ')} } }` +
|
|
|
|
|
`\n\nKeys that need moved: ${invalidKeys.join(', ')}.\n`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|