Skip to content

Prettier console output and template system #461

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
119 changes: 40 additions & 79 deletions scripts/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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;
}

Expand All @@ -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);
}
});
}
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
});
}
Expand All @@ -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?';
Expand Down
65 changes: 65 additions & 0 deletions scripts/utils/cli-template.js
Original file line number Diff line number Diff line change
@@ -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;
101 changes: 101 additions & 0 deletions scripts/utils/cli.js
Original file line number Diff line number Diff line change
@@ -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;