UNPKG

kss

Version:

The Node.js port of KSS: A methodology for documenting CSS and building style guides

259 lines (233 loc) 9.28 kB
'use strict'; /** * The `kss/lib/cli` module is a wrapper around the code used by the * `bin/kss` command line utility. * * ``` * const cli = require('kss/lib/cli'); * ``` * * @module kss/lib/cli */ const Promise = require('bluebird'), kss = require('./kss'), KssBuilderBase = require('../builder/base'), path = require('path'), version = require('../package.json').version, yargs = require('yargs'); const fs = Promise.promisifyAll(require('fs-extra')); /** * Parses command line arguments in `opts.argv` and outputs messages and errors * on `opts.stdout` and `opts.stderr`, respectively. * * @param {Object} opts The `stdout`, `stderr` and `argv` options to use. * @returns {Promise.<KssStyleGuide|null>} A `Promise` object resolving to a * `KssStyleGuide` object, or to `null` if the clone option is used. */ const cli = function(opts) { // First 2 args are "node" and path to kss script; we don't need them. const args = opts.argv.slice(2) || /* istanbul ignore next */ []; const supportedBuilders = [ 'builder/twig', 'builder/nunjucks' ]; // Set up a logging function for any messages we need to send to stdout. // We will set up the error function after we know what options to use. const reportMessage = function() { let message = ''; for (let i = 0; i < arguments.length; i++) { message += arguments[i]; } opts.stdout.write(message + '\n'); }; // If the demo is requested, load the settings from its config file. if (args.indexOf('--demo') !== -1) { // Add the configuration file to the raw arguments list. args.push('--config', path.join(__dirname, '../demo/kss-config.json')); if (args.indexOf('--json') === -1) { args.push('--verbose'); reportMessage('WELCOME to the kss demo! We’ve turned on the --verbose flag so you can see what kss is doing.'); } } const cliOptionDefinitions = { 'config': { group: 'File locations:', alias: 'c', config: true, configParser: (configPath) => { const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); const configDirectory = path.dirname(configPath); config.base = config.base ? path.resolve(configDirectory, config.base) : configDirectory; config.homepage = path.resolve(config.base, config.homepage || 'homepage.md'); if (config.source) { if (Array.isArray(config.source)) { config.source = config.source.map(x => path.resolve(config.base, x)); } else { config.source = path.resolve(config.base, config.source); } } return config; }, multiple: false, describe: 'Load the kss options from a json file' }, 'demo': { multiple: false, boolean: true, describe: 'Builds a KSS demo.', default: false }, // Prevent yargs from complaining about JSON comments in the config file. '//': { describe: 'Comments in JSON files will be ignored' } }; // We need to know which builder to use, so we do a quick first parse of the // arguments using yargs. let options = yargs(args).options( // We merge the CLI option definitions with the default KssBuilderBase // option definitions. (new KssBuilderBase()).addOptionDefinitions(cliOptionDefinitions).getOptionDefinitions() ).argv; // Check if there are settings coming from a JSON config file. We need to note // the config file’s values and the directory where it is located. let configFileOptions = {}, configFileDirectory = '', checkBuilderPath = Promise.resolve(); if (options.config) { let configFilePath = path.resolve(options.config); configFileOptions = require(configFilePath); configFileDirectory = path.dirname(configFilePath); // First, ensure the path to the builder is relative to the config file's // location. Later, we will do the same for other paths in the config file. if (configFileOptions.builder) { options.builder = path.resolve(configFileDirectory, configFileOptions.builder); checkBuilderPath = fs.statAsync(options.builder).then(stats => { if (!stats.isDirectory()) { throw new Error(); } return Promise.resolve(); }).catch(() => { // If the resolved builder path does not work, check if the a supported // builder was desired. if (supportedBuilders.indexOf(configFileOptions.builder) > -1) { options.builder = path.resolve(__dirname, '..', configFileOptions.builder); return Promise.resolve(); } else { throw new Error('The builder path, "' + options.builder + '", is not a directory.'); } }); } } // Set up an error handler for Promised tasks in this module; kss() will // handle its own errors, so we don't want to double-catch/report its errors. const reportError = function(error) { // Make sure the standard error handler is writable. // istanbul ignore else if (opts && opts.stderr && opts.stderr.write) { // Show the full error stack if the verbose option is used twice or more. opts.stderr.write(((error.stack && options.verbose > 1) ? error.stack : error) + '\n'); } else { // If the standard output for errors is not there, use console.error(). console.error(error); } }; // Confirm this is a compatible builder. return checkBuilderPath.then(() => { return KssBuilderBase.loadBuilder(KssBuilderBase.builderResolve(options.builder)); }).catch(error => { return Promise.reject(error).catch(error => { reportError(error); throw error; }); }).then(builder => { builder.addOptionDefinitions(cliOptionDefinitions); // After the builder is loaded, we finally know all the option definitions. // So we re-run yargs one last time with all the yarg definitions we need. options = yargs(args) .options(builder.getOptionDefinitions()) // Make a --help option available. .usage('Usage: kss [options]') .help('help') .alias('help', 'h') .alias('help', '?') .wrap(yargs.terminalWidth()) // Make a --version option available. .version('version', version) // Complain if the user tries to configure a non-existent option. .strict() .argv; // If no arguments given, display help and exit. if (args.length === 0) { yargs.showHelp(reportMessage); return Promise.resolve(); } // All paths from the config file are relative to the file. for (let key in configFileOptions) { if (configFileOptions.hasOwnProperty(key) && builder.getOptionDefinitions()[key] && builder.getOptionDefinitions()[key].path) { if (options[key] instanceof Array) { /* eslint-disable no-loop-func */ options[key] = options[key].map(value => { return path.resolve(configFileDirectory, value); }); /* eslint-enable no-loop-func */ } else if (key !== 'builder' || supportedBuilders.indexOf(options[key]) === -1) { options[key] = path.resolve(configFileDirectory, options[key]); } } } // Check for source and destination set as unnamed parameters. if (options._.length > 0) { let positionalParams = options._; // Check for a second unnamed parameter, the destination. if (positionalParams.length > 1) { options.destination = positionalParams[1]; } // The source directory is the first unnamed parameter. if (!(options.source instanceof Array)) { options.source = (typeof options.source === 'undefined') ? [] : [options.source]; } options.source.unshift(positionalParams[0]); } // If we are building the demo, copy the styles.css file to the destination. let demo = true; if (options.demo && !options.json) { // We save the Promise for the end of this function. demo = fs.copyAsync( path.resolve(__dirname, '../demo/styles.css'), path.resolve(options.destination, 'styles.css'), {clobber: true} ).catch(/* istanbul ignore next @TODO change back to an arrow function when istanbul adds support */ function(error) { reportError(error); return Promise.reject(error); }); } // Clean up the settings by removing object properties that yargs adds, but // that we don't need for kss(). ['config', '_', '//', 'help', 'h', '?', 'version', '$0'].forEach(key => delete options[key]); let optionDefinitions = builder.getOptionDefinitions(); for (let key in optionDefinitions) { if (optionDefinitions[key].alias) { delete options[optionDefinitions[key].alias]; } } for (let key in options) { if (typeof options[key] === 'undefined') { delete options[key]; } } // Pass on cli()'s stdout/stderr reporters to kss(). options.logFunction = reportMessage; options.logErrorFunction = reportError; return Promise.all([ demo, kss(options).then(styleGuide => { if (options.json) { reportMessage(JSON.stringify(styleGuide)); } return styleGuide; }) ]); }); }; module.exports = cli;