[Feature] Progress bar for static build (#15297)

Co-authored-by: Tim Neutkens <timneutkens@me.com>
Co-authored-by: JJ Kasper <jj@jjsweb.site>
This commit is contained in:
Jonathan G 2020-08-04 00:58:23 -07:00 committed by GitHub
parent 1ea8bdcdc7
commit 6c59cbb46a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 129 additions and 122 deletions

View file

@ -75,6 +75,8 @@ import {
import getBaseWebpackConfig from './webpack-config'
import { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
import { writeBuildId } from './write-build-id'
import * as Log from './output/log'
const staticCheckWorker = require.resolve('./utils')
export type SsgRoute = {
@ -126,17 +128,13 @@ export default async function build(
// Intentionally not piping to stderr in case people fail in CI when
// stderr is detected.
console.log(
chalk.bold.yellow(`Warning: `) +
chalk.bold(
`No build cache found. Please configure build caching for faster rebuilds. Read more: https://err.sh/next.js/no-cache`
)
`${Log.prefixes.warn} No build cache found. Please configure build caching for faster rebuilds. Read more: https://err.sh/next.js/no-cache`
)
console.log('')
}
}
const buildSpinner = createSpinner({
prefixText: 'Creating an optimized production build',
prefixText: `${Log.prefixes.info} Creating an optimized production build`,
})
const telemetry = new Telemetry({ distDir })
@ -243,16 +241,10 @@ export default async function build(
})
if (nestedReservedPages.length) {
console.warn(
'\n' +
chalk.bold.yellow(`Warning: `) +
chalk.bold(
`The following reserved Next.js pages were detected not directly under the pages directory:\n`
) +
Log.warn(
`The following reserved Next.js pages were detected not directly under the pages directory:\n` +
nestedReservedPages.join('\n') +
chalk.bold(
`\nSee more info here: https://err.sh/next.js/nested-reserved-page\n`
)
`\nSee more info here: https://err.sh/next.js/nested-reserved-page\n`
)
}
@ -354,11 +346,8 @@ export default async function build(
(clientConfig.optimization.minimizer &&
clientConfig.optimization.minimizer.length === 0))
) {
console.warn(
chalk.bold.yellow(`Warning: `) +
chalk.bold(
`Production code optimization has been disabled in your project. Read more: https://err.sh/vercel/next.js/minification-disabled`
)
Log.warn(
`Production code optimization has been disabled in your project. Read more: https://err.sh/vercel/next.js/minification-disabled`
)
}
@ -389,7 +378,6 @@ export default async function build(
if (buildSpinner) {
buildSpinner.stopAndPersist()
}
console.log()
result = formatWebpackMessages(result)
@ -435,15 +423,16 @@ export default async function build(
)
if (result.warnings.length > 0) {
console.warn(chalk.yellow('Compiled with warnings.\n'))
Log.warn('Compiled with warnings\n')
console.warn(result.warnings.join('\n\n'))
console.warn()
} else {
console.log(chalk.green('Compiled successfully.\n'))
Log.info('Compiled successfully')
}
}
const postBuildSpinner = createSpinner({
prefixText: 'Automatically optimizing pages',
const postCompileSpinner = createSpinner({
prefixText: `${Log.prefixes.info} Collecting page data`,
})
const manifestPath = path.join(
@ -685,15 +674,18 @@ export default async function build(
const finalPrerenderRoutes: { [route: string]: SsgRoute } = {}
const tbdPrerenderRoutes: string[] = []
if (postCompileSpinner) postCompileSpinner.stopAndPersist()
if (staticPages.size > 0 || ssgPages.size > 0 || useStatic404) {
const combinedPages = [...staticPages, ...ssgPages]
const exportApp = require('../export').default
const exportOptions = {
silent: true,
silent: false,
buildExport: true,
threads: config.experimental.cpus,
pages: combinedPages,
outdir: path.join(distDir, 'export'),
statusMessage: 'Generating static pages',
}
const exportConfig: any = {
...config,
@ -745,6 +737,10 @@ export default async function build(
await exportApp(dir, exportOptions, exportConfig)
const postBuildSpinner = createSpinner({
prefixText: `${Log.prefixes.info} Finalizing page optimization`,
})
// remove server bundles that were exported
for (const page of staticPages) {
const serverBundle = getPagePath(page, distDir, isLikeServerless)
@ -876,10 +872,10 @@ export default async function build(
JSON.stringify(pagesManifest, null, 2),
'utf8'
)
}
if (postBuildSpinner) postBuildSpinner.stopAndPersist()
console.log()
if (postBuildSpinner) postBuildSpinner.stopAndPersist()
console.log()
}
const analysisEnd = process.hrtime(analysisBegin)
telemetry.record(

View file

@ -1,6 +1,6 @@
import chalk from 'next/dist/compiled/chalk'
const prefixes = {
export const prefixes = {
wait: chalk.cyan('wait') + ' -',
error: chalk.red('error') + ' -',
warn: chalk.yellow('warn') + ' -',
@ -14,11 +14,11 @@ export function wait(...message: string[]) {
}
export function error(...message: string[]) {
console.log(prefixes.error, ...message)
console.error(prefixes.error, ...message)
}
export function warn(...message: string[]) {
console.log(prefixes.warn, ...message)
console.warn(prefixes.warn, ...message)
}
export function ready(...message: string[]) {

View file

@ -7,7 +7,8 @@ const dotsSpinner = {
export default function createSpinner(
text: string | { prefixText: string },
options: ora.Options = {}
options: ora.Options = {},
logFn: (...data: any[]) => void = console.log
) {
let spinner: undefined | ora.Ora
let prefixText = text && typeof text === 'object' && text.prefixText
@ -55,7 +56,7 @@ export default function createSpinner(
return spinner!
}
} else if (prefixText || text) {
console.log(prefixText ? prefixText + '...' : text)
logFn(prefixText ? prefixText + '...' : text)
}
return spinner

View file

@ -12,6 +12,7 @@ import { cpus } from 'os'
import { dirname, join, resolve, sep } from 'path'
import { promisify } from 'util'
import { AmpPageStatus, formatAmpMessages } from '../build/output/index'
import * as Log from '../build/output/log'
import createSpinner from '../build/spinner'
import { API_ROUTE, SSG_FALLBACK_EXPORT_ERROR } from '../lib/constants'
import { recursiveCopy } from '../lib/recursive-copy'
@ -97,6 +98,7 @@ interface ExportOptions {
threads?: number
pages?: string[]
buildExport?: boolean
statusMessage?: string
}
export default async function exportApp(
@ -104,13 +106,6 @@ export default async function exportApp(
options: ExportOptions,
configuration?: any
): Promise<void> {
function log(message: string): void {
if (options.silent) {
return
}
console.log(message)
}
dir = resolve(dir)
// attempt to load global env values so they are available in next.config.js
@ -136,7 +131,9 @@ export default async function exportApp(
const subFolders = nextConfig.trailingSlash
const isLikeServerless = nextConfig.target !== 'server'
log(`> using build directory: ${distDir}`)
if (!options.silent && !options.buildExport) {
Log.info(`using build directory: ${distDir}`)
}
if (!existsSync(distDir)) {
throw new Error(
@ -213,13 +210,20 @@ export default async function exportApp(
// Copy static directory
if (!options.buildExport && existsSync(join(dir, 'static'))) {
log(' copying "static" directory')
if (!options.silent) {
Log.info('Copying "static" directory')
}
await recursiveCopy(join(dir, 'static'), join(outDir, 'static'))
}
// Copy .next/static directory
if (existsSync(join(distDir, CLIENT_STATIC_FILES_PATH))) {
log(' copying "static build" directory')
if (
!options.buildExport &&
existsSync(join(distDir, CLIENT_STATIC_FILES_PATH))
) {
if (!options.silent) {
Log.info('Copying "static build" directory')
}
await recursiveCopy(
join(distDir, CLIENT_STATIC_FILES_PATH),
join(outDir, '_next', CLIENT_STATIC_FILES_PATH)
@ -228,9 +232,11 @@ export default async function exportApp(
// Get the exportPathMap from the config file
if (typeof nextConfig.exportPathMap !== 'function') {
console.log(
`> No "exportPathMap" found in "${CONFIG_FILE}". Generating map from "./pages"`
)
if (!options.silent) {
Log.info(
`No "exportPathMap" found in "${CONFIG_FILE}". Generating map from "./pages"`
)
}
nextConfig.exportPathMap = async (defaultMap: ExportPathMap) => {
return defaultMap
}
@ -264,7 +270,9 @@ export default async function exportApp(
nextExport: true,
}
log(` launching ${threads} workers`)
if (!options.silent && !options.buildExport) {
Log.info(`Launching ${threads} workers`)
}
const exportPathMap = await nextConfig.exportPathMap(defaultPathMap, {
dev: false,
dir,
@ -322,27 +330,35 @@ export default async function exportApp(
// Warn if the user defines a path for an API page
if (hasApiRoutes) {
log(
chalk.bold.red(`Warning`) +
': ' +
if (!options.silent) {
Log.warn(
chalk.yellow(
`Statically exporting a Next.js application via \`next export\` disables API routes.`
) +
`\n` +
chalk.yellow(
`This command is meant for static-only hosts, and is` +
' ' +
chalk.bold(`not necessary to make your application static.`)
) +
`\n` +
chalk.yellow(
`Pages in your application without server-side data dependencies will be automatically statically exported by \`next build\`, including pages powered by \`getStaticProps\`.`
) +
`\nLearn more: https://err.sh/vercel/next.js/api-routes-static-export`
)
`\n` +
chalk.yellow(
`This command is meant for static-only hosts, and is` +
' ' +
chalk.bold(`not necessary to make your application static.`)
) +
`\n` +
chalk.yellow(
`Pages in your application without server-side data dependencies will be automatically statically exported by \`next build\`, including pages powered by \`getStaticProps\`.`
) +
`\n` +
chalk.yellow(
`Learn more: https://err.sh/vercel/next.js/api-routes-static-export`
)
)
}
}
const progress = !options.silent && createProgress(filteredPaths.length)
const progress =
!options.silent &&
createProgress(
filteredPaths.length,
`${Log.prefixes.info} ${options.statusMessage}`
)
const pagesDataDir = options.buildExport
? outDir
: join(outDir, '_next/data', buildId)
@ -353,7 +369,9 @@ export default async function exportApp(
const publicDir = join(dir, CLIENT_PUBLIC_FILES_PATH)
// Copy public directory
if (!options.buildExport && existsSync(publicDir)) {
log(' copying "public" directory')
if (!options.silent) {
Log.info('Copying "public" directory')
}
await recursiveCopy(publicDir, outDir, {
filter(path) {
// Exclude paths used by pages
@ -476,8 +494,6 @@ export default async function exportApp(
.join('\n\t')}`
)
}
// Add an empty line to the console for the better readability.
log('')
writeFileSync(
join(distDir, EXPORT_DETAIL),

View file

@ -238,9 +238,8 @@ export default function loadConfig(
)
if (Object.keys(userConfig).length === 0) {
console.warn(
chalk.yellow.bold('Warning: ') +
'Detected next.config.js, no exported configuration found. https://err.sh/vercel/next.js/empty-configuration'
Log.warn(
'Detected next.config.js, no exported configuration found. https://err.sh/vercel/next.js/empty-configuration'
)
}

View file

@ -430,12 +430,12 @@ function runTests(dev = false) {
})
} else {
it('should show warning with next export', async () => {
const { stdout } = await nextExport(
const { stderr } = await nextExport(
appDir,
{ outdir: join(appDir, 'out') },
{ stdout: true }
{ stderr: true }
)
expect(stdout).toContain(
expect(stderr).toContain(
'https://err.sh/vercel/next.js/api-routes-static-export'
)
})

View file

@ -19,9 +19,9 @@ describe('Empty configuration', () => {
stderr: true,
stdout: true,
})
expect(stdout).toMatch(/Compiled successfully./)
expect(stdout).toMatch(/Compiled successfully/)
expect(stderr).toMatch(
/Warning: Detected next.config.js, no exported configuration found. https:\/\/err.sh\/vercel\/next.js\/empty-configuration/
/Detected next\.config\.js, no exported configuration found\. https:\/\/err\.sh\/vercel\/next\.js\/empty-configuration/
)
})
@ -38,7 +38,7 @@ describe('Empty configuration', () => {
await killApp(app)
expect(stderr).toMatch(
/Warning: Detected next.config.js, no exported configuration found. https:\/\/err.sh\/vercel\/next.js\/empty-configuration/
/Detected next\.config\.js, no exported configuration found\. https:\/\/err\.sh\/vercel\/next\.js\/empty-configuration/
)
})
})

View file

@ -20,8 +20,8 @@ describe('Promise in next config', () => {
}
`)
const { stdout } = await nextBuild(appDir, [], { stdout: true })
expect(stdout).not.toMatch(/experimental feature/)
const { stderr } = await nextBuild(appDir, [], { stderr: true })
expect(stderr).not.toMatch(/experimental feature/)
})
it('should not show warning with config from object', async () => {
@ -30,8 +30,8 @@ describe('Promise in next config', () => {
target: 'server'
}
`)
const { stdout } = await nextBuild(appDir, [], { stdout: true })
expect(stdout).not.toMatch(/experimental feature/)
const { stderr } = await nextBuild(appDir, [], { stderr: true })
expect(stderr).not.toMatch(/experimental feature/)
})
it('should show warning with config from object with experimental', async () => {
@ -43,8 +43,8 @@ describe('Promise in next config', () => {
}
}
`)
const { stdout } = await nextBuild(appDir, [], { stdout: true })
expect(stdout).toMatch(/experimental feature/)
const { stderr } = await nextBuild(appDir, [], { stderr: true })
expect(stderr).toMatch(/experimental feature/)
})
it('should show warning with config from function with experimental', async () => {
@ -56,7 +56,7 @@ describe('Promise in next config', () => {
}
})
`)
const { stdout } = await nextBuild(appDir, [], { stdout: true })
expect(stdout).toMatch(/experimental feature/)
const { stderr } = await nextBuild(appDir, [], { stderr: true })
expect(stderr).toMatch(/experimental feature/)
})
})

View file

@ -16,8 +16,8 @@ const appDir = path.join(__dirname, '..')
describe('Handles Duplicate Pages', () => {
describe('production', () => {
it('Throws an error during build', async () => {
const { stdout } = await nextBuild(appDir, [], { stdout: true })
expect(stdout).toContain('Duplicate page detected')
const { stderr } = await nextBuild(appDir, [], { stderr: true })
expect(stderr).toContain('Duplicate page detected')
})
})

View file

@ -15,12 +15,12 @@ export default function (context) {
it('Should throw if a route is matched', async () => {
const outdir = join(context.appDir, 'outApi')
const { stdout } = await runNextCommand(
const { stderr } = await runNextCommand(
['export', context.appDir, '--outdir', outdir],
{ stdout: true }
{ stderr: true }
)
expect(stdout).toContain(
expect(stderr).toContain(
'https://err.sh/vercel/next.js/api-routes-static-export'
)
})

View file

@ -15,12 +15,12 @@ export default function (context) {
it('Should throw if a route is matched', async () => {
const outdir = join(context.appDir, 'outApi')
const { stdout } = await runNextCommand(
const { stderr } = await runNextCommand(
['export', context.appDir, '--outdir', outdir],
{ stdout: true }
{ stderr: true }
)
expect(stdout).toContain(
expect(stderr).toContain(
'https://err.sh/vercel/next.js/api-routes-static-export'
)
})

View file

@ -11,7 +11,7 @@ const appDir = join(__dirname, '..')
describe('jsconfig.json', () => {
it('should build normally', async () => {
const res = await await nextBuild(appDir, [], { stdout: true })
expect(res.stdout).toMatch(/Compiled successfully\./)
expect(res.stdout).toMatch(/Compiled successfully/)
})
it('should fail on invalid jsconfig.json', async () => {

View file

@ -19,25 +19,25 @@ describe('no anonymous default export warning', () => {
})
it('show correct warnings for page', async () => {
let stdout = ''
let stderr = ''
const appPort = await findPort()
const app = await launchApp(appDir, appPort, {
env: { __NEXT_TEST_WITH_DEVTOOL: true },
onStdout(msg) {
stdout += msg || ''
onStderr(msg) {
stderr += msg || ''
},
})
const browser = await webdriver(appPort, '/page')
const found = await check(() => stdout, /anonymous/i, false)
const found = await check(() => stderr, /anonymous/i, false)
expect(found).toBeTruthy()
await browser.close()
expect(
getRegexCount(
stdout,
stderr,
/page.js\r?\n.*not preserve local component state\./g
)
).toBe(1)
@ -46,25 +46,25 @@ describe('no anonymous default export warning', () => {
})
it('show correct warnings for child', async () => {
let stdout = ''
let stderr = ''
const appPort = await findPort()
const app = await launchApp(appDir, appPort, {
env: { __NEXT_TEST_WITH_DEVTOOL: true },
onStdout(msg) {
stdout += msg || ''
onStderr(msg) {
stderr += msg || ''
},
})
const browser = await webdriver(appPort, '/child')
const found = await check(() => stdout, /anonymous/i, false)
const found = await check(() => stderr, /anonymous/i, false)
expect(found).toBeTruthy()
await browser.close()
expect(
getRegexCount(
stdout,
stderr,
/Child.js\r?\n.*not preserve local component state\./g
)
).toBe(1)
@ -73,31 +73,31 @@ describe('no anonymous default export warning', () => {
})
it('show correct warnings for both', async () => {
let stdout = ''
let stderr = ''
const appPort = await findPort()
const app = await launchApp(appDir, appPort, {
env: { __NEXT_TEST_WITH_DEVTOOL: true },
onStdout(msg) {
stdout += msg || ''
onStderr(msg) {
stderr += msg || ''
},
})
const browser = await webdriver(appPort, '/both')
const found = await check(() => stdout, /anonymous/i, false)
const found = await check(() => stderr, /anonymous/i, false)
expect(found).toBeTruthy()
await browser.close()
expect(
getRegexCount(
stdout,
stderr,
/Child.js\r?\n.*not preserve local component state\./g
)
).toBe(1)
expect(
getRegexCount(
stdout,
stderr,
/both.js\r?\n.*not preserve local component state\./g
)
).toBe(1)

View file

@ -16,15 +16,11 @@ const appDir = join(__dirname, '../')
describe('no duplicate compile error output', () => {
it('should not show compile error on page refresh', async () => {
let stdout = ''
let stderr = ''
const appPort = await findPort()
const app = await launchApp(appDir, appPort, {
env: { __NEXT_TEST_WITH_DEVTOOL: true },
onStdout(msg) {
stdout += msg || ''
},
onStderr(msg) {
stderr += msg || ''
},
@ -63,12 +59,11 @@ describe('no duplicate compile error output', () => {
const correctMessagesRegex = /error - [^\r\n]+\r?\n[^\r\n]+Unexpected token/g
const totalMessagesRegex = /Unexpected token/g
const correctMessages = getRegexCount(stdout, correctMessagesRegex)
const totalMessages = getRegexCount(stdout, totalMessagesRegex)
const correctMessages = getRegexCount(stderr, correctMessagesRegex)
const totalMessages = getRegexCount(stderr, totalMessagesRegex)
expect(correctMessages).toBeGreaterThanOrEqual(1)
expect(correctMessages).toBe(totalMessages)
expect(stderr).toBe('')
await killApp(app)
})

View file

@ -36,7 +36,7 @@ describe('Non-Standard NODE_ENV', () => {
let output = ''
app = await launchApp(appDir, await findPort(), {
onStdout(msg) {
onStderr(msg) {
output += msg || ''
},
})
@ -52,7 +52,7 @@ describe('Non-Standard NODE_ENV', () => {
env: {
NODE_ENV: 'development',
},
onStdout(msg) {
onStderr(msg) {
output += msg || ''
},
})
@ -69,7 +69,7 @@ describe('Non-Standard NODE_ENV', () => {
NODE_ENV: 'development',
},
{
onStdout(msg) {
onStderr(msg) {
output += msg || ''
},
}
@ -86,7 +86,7 @@ describe('Non-Standard NODE_ENV', () => {
env: {
NODE_ENV: 'abc',
},
onStdout(msg) {
onStderr(msg) {
output += msg || ''
},
})
@ -103,7 +103,7 @@ describe('Non-Standard NODE_ENV', () => {
NODE_ENV: 'abc',
},
{
onStdout(msg) {
onStderr(msg) {
output += msg || ''
},
}