rsnext/packages/next/build/utils.ts
JJ Kasper ca13752e24
Implement experimentalPrerender option (#7983)
* Revert "Remove Old Prerender Implementation (#8218)"

This reverts commit 2ab300dd81.

* Add contentHandler for page config

* Rename config from contentHandler to re-use
experimentalPrerender

* Remove un-needed changes

* Replace backslashes for manifest

* Update manifest output format

* Make prerender: true enable SPR behavior and update
to merge prerender-manifest for flying-shuttle

* Fix output path for / prerender file

* Add dynamic routes to test suite

* Add generating and previewing of skeletons
for prerendered dynamic routes

* remove inline prerender option

* update to not replace getInitialProps which allows
nested getInitialProps and add query when fetching prerender

* Apply suggestions from code review

Co-Authored-By: Joe Haddad <timer150@gmail.com>

* Remove legacy prerender option

* Apply suggestions from review

* Apply more suggestions from review

* Apply suggestions from code review

Co-Authored-By: Joe Haddad <timer150@gmail.com>

* Add handling of error when parsing json

* Update handling of moving exported pages

* Rename nextPreviewSkeleton to _nextPreviewSkeleton

* bump
2019-08-06 15:26:01 -05:00

302 lines
7.7 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 crypto from 'crypto'
import findUp from 'find-up'
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 { fileExists } from '../lib/file-exists'
import { getPageChunks } from './webpack/plugins/chunk-graph-plugin'
const fsStat = promisify(fs.stat)
const fsReadFile = promisify(fs.readFile)
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()
}
function flatten<T>(arr: T[][]): T[] {
return arr.reduce((acc, val) => acc.concat(val), [] as T[])
}
function getPossibleFiles(pageExtensions: string[], pages: string[]) {
const res = pages.map(page =>
pageExtensions
.map(e => `${page}.${e}`)
.concat(pageExtensions.map(e => `${path.join(page, 'index')}.${e}`))
.concat(page)
)
return flatten<string>(res)
}
export async function getFileForPage({
page,
pagesDirectory,
pageExtensions,
}: {
page: string
pagesDirectory: string
pageExtensions: string[]
}) {
const theFile = getPossibleFiles(pageExtensions, [
path.join(pagesDirectory, page),
]).find(f => fs.existsSync(f) && fs.lstatSync(f).isFile())
if (theFile) {
return path.sep + path.relative(pagesDirectory, theFile)
}
return theFile
}
export async function getSpecifiedPages(
dir: string,
pagesString: string,
pageExtensions: string[]
) {
const pagesDir = path.join(dir, 'pages')
const reservedPages = ['/_app', '/_document', '/_error']
const explodedPages = [
...new Set([...pagesString.split(','), ...reservedPages]),
].map(p => {
let resolvedPage: string | undefined
if (path.isAbsolute(p)) {
resolvedPage = getPossibleFiles(pageExtensions, [
path.join(pagesDir, p),
p,
]).find(f => fs.existsSync(f) && fs.lstatSync(f).isFile())
} else {
resolvedPage = getPossibleFiles(pageExtensions, [
path.join(pagesDir, p),
path.join(dir, p),
]).find(f => fs.existsSync(f) && fs.lstatSync(f).isFile())
}
return { original: p, resolved: resolvedPage || null }
})
const missingPage = explodedPages.find(
({ original, resolved }) => !resolved && !reservedPages.includes(original)
)
if (missingPage) {
throw new Error(`Unable to identify page: ${missingPage.original}`)
}
const resolvedPagePaths = explodedPages
.filter(page => page.resolved)
.map(page => '/' + path.relative(pagesDir, page.resolved!))
return resolvedPagePaths.sort()
}
export async function getCacheIdentifier({
pagesDirectory,
env = {},
}: {
pagesDirectory: string
env?: any
}) {
let selectivePageBuildingCacheIdentifier = ''
const envObject = env
? Object.keys(env)
.sort()
// eslint-disable-next-line
.reduce((a, c) => ((a[c] = env[c]), a), {} as any)
: {}
selectivePageBuildingCacheIdentifier += JSON.stringify(envObject)
const pkgPath = await findUp('package.json', { cwd: pagesDirectory })
if (pkgPath) {
const yarnLock = path.join(path.dirname(pkgPath), 'yarn.lock')
const packageLock = path.join(path.dirname(pkgPath), 'package-lock.json')
if (await fileExists(yarnLock)) {
selectivePageBuildingCacheIdentifier += await fsReadFile(yarnLock, 'utf8')
} else if (await fileExists(packageLock)) {
selectivePageBuildingCacheIdentifier += await fsReadFile(
packageLock,
'utf8'
)
} else {
selectivePageBuildingCacheIdentifier += JSON.stringify(require(pkgPath))
}
}
return crypto
.createHash('sha1')
.update(selectivePageBuildingCacheIdentifier)
.digest('hex')
}
export async function getPageSizeInKb(
page: string,
distPath: string,
buildId: string
): Promise<number> {
const clientBundle = path.join(
distPath,
`static/${buildId}/pages/`,
`${page}.js`
)
try {
return (await fsStat(clientBundle)).size
} catch (_) {}
return -1
}
export function isPageStatic(
serverBundle: string,
runtimeEnvConfig: any
): { static?: boolean; prerender?: boolean } {
try {
require('next-server/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')
}
return {
static: typeof (Comp as any).getInitialProps !== 'function',
prerender: mod.config && mod.config.experimentalPrerender === true,
}
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') return {}
throw err
}
}
export function hasCustomAppGetInitialProps(
_appBundle: string,
runtimeEnvConfig: any
): boolean {
require('next-server/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
}