#!/usr/bin/env node // Remove all Node warnings before doing anything else process.removeAllListeners('warning'); const prettyCLI = require('@tryghost/pretty-cli'); const ui = require('@tryghost/pretty-cli').ui; const _ = require('lodash'); const chalk = require('chalk'); const gscan = require('../lib'); const ghostVersions = require('../lib/utils').versions; const levels = { error: chalk.red, warning: chalk.yellow, recommendation: chalk.yellow, feature: chalk.green }; const cliOptions = { format: 'cli' }; prettyCLI .configure({ name: 'gscan' }) .groupOrder([ 'Sources:', 'Utilities:', 'Commands:', 'Arguments:', 'Required Options:', 'Options:', 'Global Options:' ]) .positional('', { paramsDesc: 'Theme folder or .zip file path', mustExist: true }) .boolean('-z, --zip', { desc: 'Theme path points to a zip file' }) .boolean('-1, --v1', { desc: 'Check theme for Ghost 1.0 compatibility' }) .boolean('-2, --v2', { desc: 'Check theme for Ghost 2.0 compatibility' }) .boolean('-3, --v3', { desc: 'Check theme for Ghost 3.0 compatibility' }) .boolean('-4, --v4', { desc: 'Check theme for Ghost 4.0 compatibility' }) .boolean('-5, --v5', { desc: 'Check theme for Ghost 5.0 compatibility' }) .boolean('-c, --canary', { desc: 'Check theme for upcoming Ghost version compatibility' }) .boolean('-f, --fatal', { desc: 'Only show fatal errors that prevent upgrading Ghost' }) .boolean('--verbose', { desc: 'Output check details' }) .array('--labs', { desc: 'a list of labs flags' }) .parseAndExit() .then((argv) => { if (argv.v1) { cliOptions.checkVersion = 'v1'; } else if (argv.v2) { cliOptions.checkVersion = 'v2'; } else if (argv.v3) { cliOptions.checkVersion = 'v3'; } else if (argv.v4) { cliOptions.checkVersion = 'v4'; } else if (argv.v5) { cliOptions.checkVersion = 'v5'; } else if (argv.canary) { cliOptions.checkVersion = 'canary'; } else { cliOptions.checkVersion = ghostVersions.default; } cliOptions.verbose = argv.verbose; cliOptions.onlyFatalErrors = argv.fatal; if (argv.labs) { cliOptions.labs = {}; argv.labs.forEach((flag) => { cliOptions.labs[flag] = true; }); } if (cliOptions.onlyFatalErrors) { ui.log(chalk.bold('\nChecking theme compatibility (fatal issues only)...')); } else { ui.log(chalk.bold('\nChecking theme compatibility...')); } if (argv.zip) { gscan.checkZip(argv.themePath, cliOptions) .then(theme => outputResults(theme, cliOptions)) .catch((error) => { ui.log(error); }); } else { gscan.check(argv.themePath, cliOptions) .then(theme => outputResults(theme, cliOptions)) .catch((err) => { ui.log(err.message); if (err.code === 'ENOTDIR') { ui.log('Did you mean to add the -z flag to read a zip file?'); } }); } }); function outputResult(result, options) { ui.log(levels[result.level](`- ${_.capitalize(result.level)}:`), result.rule); if (options.verbose) { ui.log(`${chalk.bold('\nDetails:')} ${result.details}`); } if (result.failures && result.failures.length) { if (options.verbose) { ui.log(''); // extra line-break ui.log(`${chalk.bold('Affected Files:')}`); result.failures.forEach((failure) => { let message = failure.ref; if (failure.message) { message += ` - ${failure.message}`; } ui.log(message); }); } else { ui.log(`${chalk.bold('Affected Files:')} ${_.map(result.failures, 'ref')}`); } } ui.log(''); // extra line-break } function getSummary(theme, options) { let summaryText = ''; const errorCount = theme.results.error.length; const warnCount = theme.results.warning.length; const pluralize = require('pluralize'); const checkSymbol = '\u2713'; if (errorCount === 0 && warnCount === 0) { if (options.onlyFatalErrors) { summaryText = `${chalk.green(checkSymbol)} Your theme has no fatal compatibility issues with Ghost ${theme.checkedVersion}`; } else { summaryText = `${chalk.green(checkSymbol)} Your theme is compatible with Ghost ${theme.checkedVersion}`; } } else { summaryText = `Your theme has`; if (!_.isEmpty(theme.results.error)) { summaryText += chalk.red.bold(` ${pluralize('error', theme.results.error.length, true)}`); } if (!_.isEmpty(theme.results.error) && !_.isEmpty(theme.results.warning)) { summaryText += ' and'; } if (!_.isEmpty(theme.results.warning)) { summaryText += chalk.yellow.bold(` ${pluralize('warning', theme.results.warning.length, true)}`); } summaryText += '!'; // NOTE: had to subtract the number of 'invisible' formating symbols // needs update if formatting above changes const hiddenSymbols = 38; summaryText += '\n' + _.repeat('-', (summaryText.length - hiddenSymbols)); } return summaryText; } function outputResults(theme, options) { try { theme = gscan.format(theme, options); } catch (err) { ui.log.error('Error formating result, some results may be missing.'); ui.log.error(err); } let errorCount = theme.results.error.length; ui.log('\n' + getSummary(theme, options)); if (!_.isEmpty(theme.results.error)) { ui.log(chalk.red.bold('\nErrors')); ui.log(chalk.red.bold('------')); ui.log(chalk.red('Important to fix, functionality may be degraded.\n')); _.each(theme.results.error, rule => outputResult(rule, options)); } if (!_.isEmpty(theme.results.warning)) { ui.log(chalk.yellow.bold('\nWarnings')); ui.log(chalk.yellow.bold('--------')); _.each(theme.results.warning, rule => outputResult(rule, options)); } if (!_.isEmpty(theme.results.recommendation)) { ui.log(chalk.yellow.bold('\nRecommendations')); ui.log(chalk.yellow.bold('---------------')); _.each(theme.results.recommendation, rule => outputResult(rule, options)); } ui.log(`\nGet more help at ${chalk.cyan.underline('https://ghost.org/docs/themes/')}`); ui.log(`You can also check theme compatibility at ${chalk.cyan.underline('https://gscan.ghost.org/')}`); // The CLI feature is mainly used to run gscan programatically in tests within themes. // Exiting with error code for warnings, causes the test to fail, even tho a theme // upload via Ghost Admin would be possible without showing errors/warning. // See also here: https://github.com/TryGhost/Blog/pull/41#issuecomment-484525754 // TODO: make failing for warnings configurable by e. g. passing an option, so we can // disable it for the usage with tests if (errorCount > 0) { process.exit(1); } else { process.exit(0); } }