Do not clear the console (#6758)

* Do not clear the console

Its rude to clear the console, you may be sharing output with other processes even in tty mode.

* Remove unused dependency

* Dedupe and cleanup dev output without clearing

* use logError

* Remove exit handler

* Add next helper

* Add log helpers

* Switch store to log helpers and a shallow object compare

* Update other files to use new logging utility

* request => build

* Update ready on messages

* Use case insensitive matching
This commit is contained in:
Justin Chase 2019-04-09 10:52:03 -05:00 committed by Joe Haddad
parent 1c305ab36e
commit 52dd42a6bb
10 changed files with 84 additions and 83 deletions

View file

@ -22,7 +22,8 @@
"prepublish": "lerna run prepublish", "prepublish": "lerna run prepublish",
"publish-canary": "lerna version prerelease --preid canary --force-publish && release --pre", "publish-canary": "lerna version prerelease --preid canary --force-publish && release --pre",
"publish-stable": "lerna version --force-publish", "publish-stable": "lerna version --force-publish",
"lint-staged": "lint-staged" "lint-staged": "lint-staged",
"next": "./packages/next/dist/bin/next"
}, },
"pre-commit": "lint-staged", "pre-commit": "lint-staged",
"lint-staged": { "lint-staged": {

View file

@ -1,39 +0,0 @@
export function onExit(fn: Function) {
function exit(signal: string = '') {
try {
if (listeners.length) {
fn()
}
} finally {
while (listeners.length) {
const { event, handler } = listeners.shift()!
process.removeListener(event, handler)
}
if (signal) {
process.kill(process.pid, signal)
}
}
}
const listeners = [
{ event: 'SIGINT', handler: () => exit('SIGINT') },
{ event: 'SIGHUP', handler: () => exit('SIGHUP') },
{ event: 'SIGQUIT', handler: () => exit('SIGQUIT') },
{ event: 'SIGTERM', handler: () => exit('SIGTERM') },
{
event: 'uncaughtException',
handler: (error: Error) => {
console.error('Got uncaughtException:', error)
exit()
process.exit(1)
},
},
{ event: 'exit', handler: () => exit() },
]
for (const { event, handler } of listeners) {
process.on(event as any, handler)
}
}

View file

@ -0,0 +1,34 @@
import chalk from 'chalk'
const prefixes = {
wait: chalk`[ {cyan wait} ] `,
error: chalk`[ {red error} ]`,
warn: chalk`[ {yellow warn} ] `,
ready: chalk`[ {green ready} ]`,
info: chalk`[ {cyan {dim info}} ] `,
event: chalk`[ {magenta event} ]`,
}
export function wait(...message: string[]) {
console.log(prefixes.wait, ...message)
}
export function error(...message: string[]) {
console.log(prefixes.error, ...message)
}
export function warn(...message: string[]) {
console.log(prefixes.warn, ...message)
}
export function ready(...message: string[]) {
console.log(prefixes.ready, ...message)
}
export function info(...message: string[]) {
console.log(prefixes.info, ...message)
}
export function event(...message: string[]) {
console.log(prefixes.event, ...message)
}

View file

@ -1,9 +1,8 @@
import chalk from 'chalk'
import createStore from 'next/dist/compiled/unistore' import createStore from 'next/dist/compiled/unistore'
import readline from 'readline'
import { onExit } from './exit'
import stripAnsi from 'strip-ansi' import stripAnsi from 'strip-ansi'
import * as Log from './log'
export type OutputState = export type OutputState =
| { bootstrap: true; appUrl: string | null } | { bootstrap: true; appUrl: string | null }
| ({ bootstrap: false; appUrl: string | null } & ( | ({ bootstrap: false; appUrl: string | null } & (
@ -16,62 +15,66 @@ export type OutputState =
export const store = createStore<OutputState>({ appUrl: null, bootstrap: true }) export const store = createStore<OutputState>({ appUrl: null, bootstrap: true })
process.stdout.write('\n'.repeat(process.stdout.rows || 1)) let lastStore: OutputState = {} as any
process.stdout.write('\u001b[?25l') function hasStoreChanged(nextStore: OutputState) {
onExit(() => { if (
process.stdout.write('\u001b[?25h') [...new Set([...Object.keys(lastStore), ...Object.keys(nextStore)])].every(
}) key => Object.is((lastStore as any)[key], (nextStore as any)[key])
)
) {
return false
}
lastStore = nextStore
return true
}
store.subscribe(state => { store.subscribe(state => {
if (process.stdout.isTTY) { if (!hasStoreChanged(state)) {
readline.cursorTo(process.stdout, 0, 0) return
readline.clearScreenDown(process.stdout)
} }
if (state.bootstrap) { if (state.bootstrap) {
console.log(chalk.cyan('Starting the development server ...')) Log.wait('starting the development server ...')
if (state.appUrl) { Log.info(`waiting on ${state.appUrl!} ...`)
console.log()
console.log(` > Waiting on ${state.appUrl!}`)
}
return return
} }
if (state.loading) { if (state.loading) {
console.log('Compiling ...') Log.wait('compiling ...')
return return
} }
if (state.errors) { if (state.errors) {
console.log(chalk.red('Failed to compile.')) Log.error(state.errors[0])
console.log()
const cleanError = stripAnsi(state.errors[0]) const cleanError = stripAnsi(state.errors[0])
if (cleanError.indexOf('SyntaxError') > -1) { if (cleanError.indexOf('SyntaxError') > -1) {
const matches = cleanError.match(/\[.*\]=/) const matches = cleanError.match(/\[.*\]=/)
if (matches) { if (matches) {
for (const match of matches) { for (const match of matches) {
const prop = (match.split(']').shift() || '').substr(1) const prop = (match.split(']').shift() || '').substr(1)
console.log(`AMP bind syntax [${prop}]='' is not supported in JSX, use 'data-amp-bind-${prop}' instead. https://err.sh/zeit/next.js/amp-bind-jsx-alt`) console.log(
`AMP bind syntax [${prop}]='' is not supported in JSX, use 'data-amp-bind-${prop}' instead. https://err.sh/zeit/next.js/amp-bind-jsx-alt`
)
} }
console.log()
return return
} }
} }
console.log(state.errors[0])
return return
} }
if (state.warnings) { if (state.warnings) {
console.log(chalk.yellow('Compiled with warnings.')) Log.warn(state.warnings.join('\n\n'))
console.log() Log.info(`ready on ${state.appUrl!}`)
console.log(state.warnings.join('\n\n'))
return return
} }
console.log(chalk.green('Compiled successfully!'))
if (state.appUrl) { if (state.appUrl) {
console.log() Log.ready('compiled successfully')
console.log(` > Ready on ${state.appUrl!}`) if (state.appUrl) {
Log.info(`ready on ${state.appUrl!}`)
}
} }
console.log()
console.log('Note that pages will be compiled when you first load them.')
}) })

View file

@ -6,6 +6,7 @@ import { PHASE_DEVELOPMENT_SERVER } from 'next-server/constants'
import ErrorDebug from './error-debug' import ErrorDebug from './error-debug'
import AmpHtmlValidator from 'amphtml-validator' import AmpHtmlValidator from 'amphtml-validator'
import { ampValidation } from '../build/output/index' import { ampValidation } from '../build/output/index'
import * as Log from '../build/output/log'
const React = require('react') const React = require('react')
@ -176,7 +177,7 @@ export default class DevServer extends Server {
const out = await super.renderErrorToHTML(err, req, res, pathname, query) const out = await super.renderErrorToHTML(err, req, res, pathname, query)
return out return out
} catch (err2) { } catch (err2) {
if (!this.quiet) console.error(err2) if (!this.quiet) Log.error(err2)
res.statusCode = 500 res.statusCode = 500
return super.renderErrorToHTML(err2, req, res, pathname, query) return super.renderErrorToHTML(err2, req, res, pathname, query)
} }

View file

@ -8,6 +8,7 @@ import { ROUTE_NAME_REGEX, IS_BUNDLED_PAGE_REGEX } from 'next-server/constants'
import { stringify } from 'querystring' import { stringify } from 'querystring'
import { findPageFile } from './lib/find-page-file' import { findPageFile } from './lib/find-page-file'
import { isWriteable } from '../build/is-writeable' import { isWriteable } from '../build/is-writeable'
import * as Log from '../build/output/log'
const ADDED = Symbol('added') const ADDED = Symbol('added')
const BUILDING = Symbol('building') const BUILDING = Symbol('building')
@ -51,6 +52,7 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
let reloading = false let reloading = false
let stopped = false let stopped = false
let reloadCallbacks = new EventEmitter() let reloadCallbacks = new EventEmitter()
let lastEntry = null
for (const compiler of compilers) { for (const compiler of compilers) {
compiler.hooks.make.tapPromise('NextJsOnDemandEntries', (compilation) => { compiler.hooks.make.tapPromise('NextJsOnDemandEntries', (compilation) => {
@ -60,7 +62,7 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
const { name, absolutePagePath } = entries[page] const { name, absolutePagePath } = entries[page]
const pageExists = await isWriteable(absolutePagePath) const pageExists = await isWriteable(absolutePagePath)
if (!pageExists) { if (!pageExists) {
console.warn('Page was removed', page) Log.event('page was removed', page)
delete entries[page] delete entries[page]
return return
} }
@ -164,11 +166,10 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
const entryInfo = entries[page] const entryInfo = entries[page]
let toSend let toSend
// If there's no entry. // If there's no entry, it may have been invalidated and needs to be re-built.
// Then it seems like an weird issue.
if (!entryInfo) { if (!entryInfo) {
const message = `Client pings, but there's no entry for page: ${page}` if (page !== lastEntry) Log.event(`client pings, but there's no entry for page: ${page}`)
console.error(message) lastEntry = page
return { invalid: true } return { invalid: true }
} }
@ -257,7 +258,7 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
} }
} }
console.log(`> Building page: ${normalizedPage}`) Log.event(`build page: ${normalizedPage}`)
entries[normalizedPage] = { name, absolutePagePath, status: ADDED } entries[normalizedPage] = { name, absolutePagePath, status: ADDED }
doneCallbacks.once(normalizedPage, handleCallback) doneCallbacks.once(normalizedPage, handleCallback)
@ -364,7 +365,7 @@ function disposeInactiveEntries (devMiddleware, entries, lastAccessPages, maxIna
disposingPages.forEach((page) => { disposingPages.forEach((page) => {
delete entries[page] delete entries[page]
}) })
console.log(`> Disposing inactive page(s): ${disposingPages.join(', ')}`) Log.event(`disposing inactive page(s): ${disposingPages.join(', ')}`)
devMiddleware.invalidate() devMiddleware.invalidate()
} }
} }

View file

@ -75,7 +75,7 @@ describe('CLI Usage', () => {
test('custom directory', async () => { test('custom directory', async () => {
const port = await findPort() const port = await findPort()
const output = await runNextCommandDev([dir, '--port', port], true) const output = await runNextCommandDev([dir, '--port', port], true)
expect(output).toMatch(/Ready on/) expect(output).toMatch(/ready on/i)
}) })
test('--port', async () => { test('--port', async () => {

View file

@ -32,7 +32,7 @@ const startServer = async (optEnv = {}) => {
optEnv optEnv
) )
server = await initNextServerScript(scriptPath, /Ready on/, env, /ReferenceError: options is not defined/) server = await initNextServerScript(scriptPath, /ready on/i, env, /ReferenceError: options is not defined/)
} }
describe('Custom Server', () => { describe('Custom Server', () => {

View file

@ -26,7 +26,7 @@ const startServer = async (optEnv = {}) => {
optEnv optEnv
) )
server = await initNextServerScript(scriptPath, /Ready on/, env) server = await initNextServerScript(scriptPath, /ready on/i, env)
} }
describe('FileSystemPublicRoutes', () => { describe('FileSystemPublicRoutes', () => {

View file

@ -123,7 +123,7 @@ export function runNextCommandDev (argv, stdOut) {
function handleStdout (data) { function handleStdout (data) {
const message = data.toString() const message = data.toString()
if (/> Ready on/.test(message)) { if (/ready on/i.test(message)) {
resolve(stdOut ? message : instance) resolve(stdOut ? message : instance)
} }
process.stdout.write(message) process.stdout.write(message)