diff --git a/lib/build.js b/lib/build.js index b96f1096b4..9587da5542 100644 --- a/lib/build.js +++ b/lib/build.js @@ -8,13 +8,14 @@ module.exports = async function build (sourceDir, cliOptions = {}) { const readline = require('readline') const escape = require('escape-html') + const logger = require('./util/logger') const prepare = require('./prepare') const createClientConfig = require('./webpack/createClientConfig') const createServerConfig = require('./webpack/createServerConfig') const { createBundleRenderer } = require('vue-server-renderer') const { normalizeHeadTag, applyUserWebpackConfig } = require('./util') - process.stdout.write('Extracting site metadata...') + logger.wait('\nExtracting site metadata...') const options = await prepare(sourceDir) if (cliOptions.outDir) { options.outDir = cliOptions.outDir @@ -22,7 +23,7 @@ module.exports = async function build (sourceDir, cliOptions = {}) { const { outDir } = options if (path.resolve() === outDir) { - return console.error(chalk.red('Unexpected option: outDir cannot be set to the current working directory.\n')) + return console.error(logger.error(chalk.red('Unexpected option: outDir cannot be set to the current working directory.\n'), false)) } await fs.remove(outDir) @@ -64,7 +65,7 @@ module.exports = async function build (sourceDir, cliOptions = {}) { .join('\n ') // render pages - console.log('Rendering static HTML...') + logger.wait('Rendering static HTML...') for (const page of options.siteData.pages) { await renderPage(page) } @@ -78,7 +79,7 @@ module.exports = async function build (sourceDir, cliOptions = {}) { readline.cursorTo(process.stdout, 0) if (options.siteConfig.serviceWorker) { - console.log('Generating service worker...') + logger.wait('\nGenerating service worker...') const wbb = require('workbox-build') wbb.generateSW({ swDest: path.resolve(outDir, 'service-worker.js'), @@ -89,7 +90,7 @@ module.exports = async function build (sourceDir, cliOptions = {}) { // DONE. const relativeDir = path.relative(process.cwd(), outDir) - console.log(`\n${chalk.green('Success!')} Generated static files in ${chalk.cyan(relativeDir)}.`) + logger.success(`\n${chalk.green('Success!')} Generated static files in ${chalk.cyan(relativeDir)}.\n`) // --- helpers --- @@ -150,7 +151,7 @@ module.exports = async function build (sourceDir, cliOptions = {}) { try { html = await renderer.renderToString(context) } catch (e) { - console.error(chalk.red(`Error rendering ${pagePath}:`)) + console.error(logger.error(chalk.red(`Error rendering ${pagePath}:`), false)) throw e } const filename = decodeURIComponent(pagePath.replace(/\/$/, '/index.html').replace(/^\//, '')) diff --git a/lib/dev.js b/lib/dev.js index 8e91c32d9c..dbc7c1c187 100644 --- a/lib/dev.js +++ b/lib/dev.js @@ -9,21 +9,22 @@ module.exports = async function dev (sourceDir, cliOptions = {}) { const mount = require('koa-mount') const serveStatic = require('koa-static') const history = require('connect-history-api-fallback') - const portfinder = require('portfinder') const prepare = require('./prepare') + const logger = require('./util/logger') const HeadPlugin = require('./webpack/HeadPlugin') + const DevLogPlugin = require('./webpack/DevLogPlugin') const createClientConfig = require('./webpack/createClientConfig') const { applyUserWebpackConfig } = require('./util') const { frontmatterEmitter } = require('./webpack/markdownLoader') - process.stdout.write('Extracting site metadata...') + logger.wait('\nExtracting site metadata...') const options = await prepare(sourceDir) // setup watchers to update options and dynamically generated files const update = () => { prepare(sourceDir).catch(err => { - console.error(chalk.red(err.stack)) + console.error(logger.error(chalk.red(err.stack), false)) }) } @@ -67,6 +68,17 @@ module.exports = async function dev (sourceDir, cliOptions = {}) { tags: options.siteConfig.head || [] }]) + const port = await resolvePort(cliOptions.port || options.siteConfig.port) + const { host, displayHost } = await resolveHost(cliOptions.host || options.siteConfig.host) + + config + .plugin('vuepress-log') + .use(DevLogPlugin, [{ + port, + displayHost, + publicPath: options.publicPath + }]) + config = config.toConfig() const userConfig = options.siteConfig.configureWebpack if (userConfig) { @@ -74,30 +86,6 @@ module.exports = async function dev (sourceDir, cliOptions = {}) { } const compiler = webpack(config) - // webpack-serve hot updates doesn't work properly over 0.0.0.0 on Windows, - // but localhost does not allow visiting over network :/ - const defaultHost = process.platform === 'win32' ? 'localhost' : '0.0.0.0' - const host = cliOptions.host || options.siteConfig.host || defaultHost - const displayHost = host === defaultHost && process.platform !== 'win32' - ? 'localhost' - : host - portfinder.basePort = cliOptions.port || options.siteConfig.port || 8080 - const port = await portfinder.getPortPromise() - - let isFirst = true - compiler.hooks.done.tap('vuepress', () => { - if (isFirst) { - isFirst = false - console.log( - `\n VuePress dev server listening at ${ - chalk.cyan(`http://${displayHost}:${port}${options.publicPath}`) - }\n` - ) - } else { - const time = new Date().toTimeString().match(/^[\d:]+/)[0] - console.log(` ${chalk.gray(`[${time}]`)} ${chalk.green('✔')} successfully compiled.`) - } - }) const nonExistentDir = path.resolve(__dirname, 'non-existent') await serve({ @@ -128,3 +116,24 @@ module.exports = async function dev (sourceDir, cliOptions = {}) { } }) } + +function resolveHost (host) { + // webpack-serve hot updates doesn't work properly over 0.0.0.0 on Windows, + // but localhost does not allow visiting over network :/ + const defaultHost = process.platform === 'win32' ? 'localhost' : '0.0.0.0' + host = host || defaultHost + const displayHost = host === defaultHost && process.platform !== 'win32' + ? 'localhost' + : host + return { + displayHost, + host + } +} + +async function resolvePort (port) { + const portfinder = require('portfinder') + portfinder.basePort = port || 8080 + port = await portfinder.getPortPromise() + return port +} diff --git a/lib/eject.js b/lib/eject.js index 4d242f44d1..e96b00fd84 100644 --- a/lib/eject.js +++ b/lib/eject.js @@ -1,6 +1,7 @@ const fs = require('fs-extra') const path = require('path') const chalk = require('chalk') +const logger = require('./util/logger') module.exports = async (dir) => { const source = path.resolve(__dirname, 'default-theme') @@ -11,5 +12,5 @@ module.exports = async (dir) => { const content = await fs.readFile(styleConfig, 'utf-8') const transformed = content.split('\n').slice(0, -2).join('\n') await fs.writeFile(styleConfig, transformed) - console.log(`Copied default theme into ${chalk.cyan(target)}.`) + logger.success(`\nCopied default theme into ${chalk.cyan(target)}.\n`) } diff --git a/lib/markdown/highlight.js b/lib/markdown/highlight.js index 1c429ee2b1..ea739d4d54 100644 --- a/lib/markdown/highlight.js +++ b/lib/markdown/highlight.js @@ -2,6 +2,7 @@ const chalk = require('chalk') const prism = require('prismjs') const loadLanguages = require('prismjs/components/index') const escapeHtml = require('escape-html') +const logger = require('../util/logger') // required to make embedded highlighting work... loadLanguages(['markup', 'css', 'javascript']) @@ -32,7 +33,7 @@ module.exports = (str, lang) => { try { loadLanguages([lang]) } catch (e) { - console.log(chalk.yellow(`[vuepress] Syntax highlight for language "${lang}" is not supported.`)) + logger.warn(chalk.yellow(`[vuepress] Syntax highlight for language "${lang}" is not supported.`)) } } if (prism.languages[lang]) { diff --git a/lib/util/logger.js b/lib/util/logger.js new file mode 100644 index 0000000000..9fd1a9a296 --- /dev/null +++ b/lib/util/logger.js @@ -0,0 +1,48 @@ +const chalk = require('chalk') + +const logger = {} + +const logTypes = { + success: { + color: 'green', + label: 'DONE' + }, + error: { + color: 'red', + label: 'FAIL' + }, + warn: { + color: 'yellow', + label: 'WARN' + }, + tip: { + color: 'cyan', + label: 'TIP' + }, + wait: { + color: 'blue', + label: 'WAIT' + } +} + +const getLoggerFn = (color, label) => (msg, log = true) => { + let newLine = false + if (msg.startsWith('\n')) { + if (log) msg = msg.slice(1) + newLine = true + } + msg = chalk.reset.inverse.bold[color](` ${label} `) + ' ' + msg + if (log) { + console.log(newLine ? '\n' + msg : msg) + } else { + return msg + } +} + +for (const type in logTypes) { + const { color, label } = logTypes[type] + logger[type] = getLoggerFn(color, label) +} + +module.exports = logger +module.exports.getLoggerFn = getLoggerFn diff --git a/lib/webpack/DevLogPlugin.js b/lib/webpack/DevLogPlugin.js new file mode 100644 index 0000000000..8481b54dde --- /dev/null +++ b/lib/webpack/DevLogPlugin.js @@ -0,0 +1,29 @@ +const chalk = require('chalk') +const logger = require('../util/logger') + +module.exports = class DevLogPlugin { + constructor (options) { + this.options = options + } + + apply (compiler) { + let isFirst = true + compiler.hooks.done.tap('vuepress-log', stats => { + clearScreen() + + const { displayHost, port, publicPath } = this.options + const time = new Date().toTimeString().match(/^[\d:]+/)[0] + + logger.success(`\n${chalk.gray(`[${time}]`)} Build ${chalk.italic(stats.hash.slice(0, 6))} finished in ${stats.endTime - stats.startTime} ms!`) + if (isFirst) { + isFirst = false + console.log(`\n${chalk.gray('>')} VuePress dev server listening at ${chalk.cyan(`http://${displayHost}:${port}${publicPath}`)}`) + } + }) + compiler.hooks.invalid.tap('vuepress-log', clearScreen) + } +} + +function clearScreen () { + process.stdout.write('\x1Bc') +}