diff --git a/package.json b/package.json index 1b389da8005..403a69db898 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,8 @@ "url-loader": "0.5.7", "webpack": "1.13.1", "webpack-dev-server": "1.14.1", - "whatwg-fetch": "1.0.0" + "whatwg-fetch": "1.0.0", + "pretty-cli": "0.0.14" }, "devDependencies": { "bundle-deps": "1.0.0", diff --git a/scripts/start.js b/scripts/start.js index aa68ab40ff3..50cb36f0153 100644 --- a/scripts/start.js +++ b/scripts/start.js @@ -21,6 +21,7 @@ var detect = require('detect-port'); var prompt = require('./utils/prompt'); var config = require('../config/webpack.config.dev'); var paths = require('../config/paths'); +var cli = require('./utils/cli'); // Tools like Cloud9 rely on this. var DEFAULT_PORT = process.env.PORT || 3000; @@ -40,37 +41,9 @@ if (isSmokeTest) { }; } -// Some custom utilities to prettify Webpack output. -// This is a little hacky. -// It would be easier if webpack provided a rich error object. -var friendlySyntaxErrorLabel = 'Syntax error:'; -function isLikelyASyntaxError(message) { - return message.indexOf(friendlySyntaxErrorLabel) !== -1; -} -function formatMessage(message) { - return message - // Make some common errors shorter: - .replace( - // Babel syntax error - 'Module build failed: SyntaxError:', - friendlySyntaxErrorLabel - ) - .replace( - // Webpack file not found error - /Module not found: Error: Cannot resolve 'file' or 'directory'/, - 'Module not found:' - ) - // Internal stacks are generally useless so we strip them - .replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, '') // at ... ...:x:y - // Webpack loader names obscure CSS filenames - .replace('./~/css-loader!./~/postcss-loader!', ''); -} -function clearConsole() { - // This seems to work best on Windows and other systems. - // The intention is to clear the output so you can focus on most recent build. - process.stdout.write('\x1bc'); -} + + function setupCompiler(port) { // "Compiler" is a low-level interface to Webpack. @@ -82,26 +55,32 @@ function setupCompiler(port) { // bundle, so if you refresh, it'll wait instead of serving the old one. // "invalid" is short for "bundle invalidated", it doesn't imply any errors. compiler.plugin('invalid', function() { - clearConsole(); - console.log('Compiling...'); + cli.clear(); + cli.info({type:'title', + name:'WAIT', + message:'Compiling...\n'}); }); // "done" event fires when Webpack has finished recompiling the bundle. // Whether or not you have warnings or errors, you will get this event. compiler.plugin('done', function(stats) { - clearConsole(); + cli.clear(); var hasErrors = stats.hasErrors(); var hasWarnings = stats.hasWarnings(); if (!hasErrors && !hasWarnings) { - console.log(chalk.green('Compiled successfully!')); - console.log(); - console.log('The app is running at:'); - console.log(); - console.log(' ' + chalk.cyan('http://localhost:' + port + '/')); - console.log(); - console.log('Note that the development build is not optimized.'); - console.log('To create a production build, use ' + chalk.cyan('npm run build') + '.'); + // cli.displayHeader(); + cli.success({type:'title', name:'DONE', message:'Compiled successfully!\n'}); + + cli.buildInfo(stats); + + + cli.info('The app is running at http://localhost:' + port + '/'); + console.log(); + cli.note([ + 'Note that the development build is not optimized.', + 'To create a production build, use ' + chalk.cyan('npm run build') + '.']); + return; } @@ -111,39 +90,14 @@ function setupCompiler(port) { // We use stats.toJson({}, true) to make output more compact and readable: // https://github.com/facebookincubator/create-react-app/issues/401#issuecomment-238291901 var json = stats.toJson({}, true); - var formattedErrors = json.errors.map(message => - 'Error in ' + formatMessage(message) - ); - var formattedWarnings = json.warnings.map(message => - 'Warning in ' + formatMessage(message) - ); + if (hasErrors) { - console.log(chalk.red('Failed to compile.')); - console.log(); - if (formattedErrors.some(isLikelyASyntaxError)) { - // If there are any syntax errors, show just them. - // This prevents a confusing ESLint parsing error - // preceding a much more useful Babel syntax error. - formattedErrors = formattedErrors.filter(isLikelyASyntaxError); - } - formattedErrors.forEach(message => { - console.log(message); - console.log(); - }); - // If errors exist, ignore warnings. - return; + cli.displayErrors('Failed to compile.\n', json.errors); + return; } + if (hasWarnings) { - console.log(chalk.yellow('Compiled with warnings.')); - console.log(); - formattedWarnings.forEach(message => { - console.log(message); - console.log(); - }); - // Teach some ESLint tricks. - console.log('You may use special comments to disable some warnings.'); - console.log('Use ' + chalk.yellow('// eslint-disable-next-line') + ' to ignore the next line.'); - console.log('Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.'); + cli.displayWarnings('Compiled with warnings.\n', json.warnings); } }); } @@ -188,9 +142,14 @@ function addMiddleware(devServer) { })); if (proxy) { if (typeof proxy !== 'string') { - console.log(chalk.red('When specified, "proxy" in package.json must be a string.')); - console.log(chalk.red('Instead, the type of "proxy" was "' + typeof proxy + '".')); - console.log(chalk.red('Either remove "proxy" from package.json, or make it a string.')); + cli.error({type:'title', name:'PROXY', + message:[ + 'When specified, "proxy" in package.json must be a string.', + 'Instead, the type of "proxy" was "' + typeof proxy + '".', + 'Either remove "proxy" from package.json, or make it a string.' + ] + }); + process.exit(1); } @@ -244,12 +203,14 @@ function runDevServer(port) { // Launch WebpackDevServer. devServer.listen(port, (err, result) => { if (err) { - return console.log(err); + return cli.error(err); } - clearConsole(); - console.log(chalk.cyan('Starting the development server...')); - console.log(); + cli.clear(); + cli.displayHeader(); + cli.info({type:'title', + name:'WAIT', + message:'Starting the development server...\n'}); openBrowser(port); }); } @@ -267,7 +228,7 @@ detect(DEFAULT_PORT).then(port => { return; } - clearConsole(); + cli.clear(); var question = chalk.yellow('Something is already running on port ' + DEFAULT_PORT + '.') + '\n\nWould you like to run the app on another port instead?'; diff --git a/scripts/utils/cli-template.js b/scripts/utils/cli-template.js new file mode 100644 index 00000000000..c5bc3b462d0 --- /dev/null +++ b/scripts/utils/cli-template.js @@ -0,0 +1,65 @@ +var chalk = require('chalk'); + +function isArray(v){ + if( Object.prototype.toString.call( v ) === '[object Array]' ) { + return true; + } + return false; +} + +function block(msg){ + return ' '+msg+' '; +} + +var types = { + 'info': {initial: 'I', blockColor:['bgBlue','black'], titleColor:'blue'}, + 'error': {initial: 'E', blockColor:['bgRed','white'], titleColor:'red'}, + 'warning': {initial: 'W', blockColor:['bgYellow','black'], titleColor:'yellow'}, + 'success': {initial: 'S', blockColor:['bgGreen','black'], titleColor:'green'}, + 'note': {initial: 'N', blockColor:['bgBlack','yellow'], titleColor:'yellow'}, +} + +var template = {}; + +// template['log'] = function(content){ +// return content; +// } + +function indentLines(lines, spaces){ + if(isArray(lines)) + return lines.join('\n'+ Array(spaces).join(' ')) + return lines; +} +Object.keys(types).map(function(type){ + var _specs = types[type]; + template[type] = function(content, override){ + var line, specs={}; + Object.assign(specs, _specs, override) + if(typeof content !== 'string'){ + var blk; + if(isArray(content)){ + blk = block(specs.initial) + line = chalk[specs.blockColor[0]][specs.blockColor[1]](blk) + +" " + indentLines(content, blk.length+2) + } else { + + if(content.type === 'title'){ + blk = block(content.name); + line = chalk[specs.blockColor[0]][specs.blockColor[1]](blk) + + " " + chalk[specs.titleColor](indentLines(content.message, blk.length+2)); + } else { + blk = block(specs.initial); + line = chalk[specs.blockColor[0]][specs.blockColor[1]](blk) + +" " + indentLines(content.message, blk.length); + } + } + } else { + line = chalk[specs.blockColor[0]][specs.blockColor[1]](block(_specs.initial)) + +" " + content + } + return line; + } + return type; +}); + +module.exports = template; diff --git a/scripts/utils/cli.js b/scripts/utils/cli.js new file mode 100644 index 00000000000..3e68d0b8604 --- /dev/null +++ b/scripts/utils/cli.js @@ -0,0 +1,101 @@ +var path = require('path'); +var chalk = require('chalk'); + +var pkg = require('../../package.json') + +var friendlySyntaxErrorLabel = 'Syntax error'; +function isLikelyASyntaxError(message) { + return message.indexOf('SyntaxError') !== -1; +} + +function formatMessage(message) { + return message + // Make some common errors shorter: + .replace( + // Babel syntax error + 'Module build failed: SyntaxError:', + friendlySyntaxErrorLabel + ) + .replace( + // Webpack file not found error + /Module not found: Error: Cannot resolve 'file' or 'directory'/, + 'Module not found:' + ) + // Internal stacks are generally useless so we strip them + .replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, '') // at ... ...:x:y + // Webpack loader names obscure CSS filenames + .replace('./~/css-loader!./~/postcss-loader!', ''); +} + +var cli = require('pretty-cli')({ + template: require('./cli-template') +}); + +cli.addCustomMethod('clear', function(){ + process.stdout.write('\x1bc'); +}) + +cli.addCustomMethod('displayHeader', function(){ + cli.log(pkg.name.toUpperCase() +' '+pkg.version + '\n') +}) + +cli.addCustomMethod('buildInfo', function(stats){ + + var buildInfo = []; + try { + var packageData = require(process.cwd() + '/package.json'); + buildInfo.push('Name: '+packageData.name) + buildInfo.push('Version: '+packageData.version) + } catch (e) { + // There was no package.json + return; + } + + buildInfo.push('Compiling time: '+ ((stats.endTime-stats.startTime)/ 1000).toFixed(2)+'ms') + buildInfo.push('HASH: '+ stats.hash+'\n') + cli.info({type:'title', name:'PKG', message:buildInfo}) +}) +cli.addCustomMethod('displayWarnings', function(title, messages){ + if(!messages.length) return; + + cli.warning({type:'title', name:'WARNING', message: title}); + + var rx = path.join(__dirname,'../../')+'.*\n'; + var processDir = path.join(process.cwd(),'../') + messages.forEach(message=>{ + var messageString = formatMessage(message) + .replace(new RegExp(rx,''), '\\033[0m') + .replace(new RegExp(processDir), './'); + cli.warning('Warning in '+ messageString); + }) + + cli.note(['You may use special comments to disable some warnings.', + 'Use ' + chalk.yellow('// eslint-disable-next-line') + ' to ignore the next line.', + 'Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.' + ]); +}) + +cli.addCustomMethod('displayErrors', function(title, messages){ + if(!messages.length) return; + cli.error({type:'title', name:'ERROR', message: title}); + if (messages.some(isLikelyASyntaxError)) { + // If there are any syntax errors, show just them. + // This prevents a confusing ESLint parsing error + // preceding a much more useful Babel syntax error. + messages = messages.filter(isLikelyASyntaxError); + } + + var rx = path.join(__dirname,'../../')+'[^:]*: '; + var processDir = path.join(process.cwd(),'../') + + messages.forEach(message => { + var messageString = formatMessage(message) + .replace(new RegExp(rx,''), ' ') + .replace(new RegExp(processDir), './'); + + cli.error(messageString); + + }); + +}) +module.exports = cli;