UNPKG

chappe

Version:

Developer Docs builder. Write guides in Markdown and references in API Blueprint. Comes with a built-in search engine.

579 lines (461 loc) 13.4 kB
#!/usr/bin/env node /* * chappe * * Copyright 2021, Crisp IM SAS * Author: Valerian Saliou <valerian@valeriansaliou.name> */ "use strict"; // Suppress all warnings from Node, as this is a CLI script process.removeAllListeners("warning"); var fs = require("fs"); var path = require("path"); var ora = require("ora"); var version = require("../package.json").version; var args = require("yargs").argv; /** * Chappe CLI * @class * @classdesc Chappe CLI class. */ class ChappeCLI { /** * Constructor */ constructor() { // Constants this.__context_defaults = { default : { config : "./config.json", assets : "./assets", data : "./data", dist : "./dist", temp : "./.chappe", env : "production", host : "localhost", port : 8080 }, example : { config : "./examples/{{target}}/config.json", assets : "./examples/{{target}}/assets", data : "./examples/{{target}}/data", dist : "./dist", temp : "./.chappe", env : "development", host : "localhost", port : 8080 } }; this.__actions_available = [ "build", "clean", "watch", "serve", "lint" ]; this.__actions_logging = [ "watch", "serve" ]; this.__actions_no_aborts = [ "watch", "serve" ]; this.__spinner_successes = { default : { method : "succeed", text : "Success!" }, clean : { method : "succeed", text : "Cleaned up." }, build : { method : "succeed", text : "Build done!" }, lint : { method : "succeed", text : "Lint passed." }, serve : { method : "start", text : "Now listening..." }, watch : { method : "start", text : "Watching...\n" } }; this.__path_expand_keys = [ "config", "assets", "data", "dist", "temp" ]; this.__env_available = [ "development", "production" ]; // Storage this.__has_setup_gulp_logging = false; } // jscs:disable disallowIdentifierNames /** * Runs the class * @public * @return {undefined} */ run() { // Run help? if (args.help) { return this.__run_help(); } // Run version? if (args.version) { return this.__run_version(); } // Run default (clean or build) this.__run_default(); } /** * Runs help * @private * @return {undefined} */ __run_help() { console.log( "Builds given Chappe documentation resources into static assets.\n\n" + "Available actions:\n" + (this.__format_help_actions(this.__actions_available) + "\n\n") + "Available arguments:\n" + (this.__format_help_argument("config") + "\n") + (this.__format_help_argument("assets") + "\n") + (this.__format_help_argument("data") + "\n") + (this.__format_help_argument("dist") + "\n") + (this.__format_help_argument("temp") + "\n") + (this.__format_help_argument("env") + "\n") + (this.__format_help_argument("host") + "\n") + (this.__format_help_argument("port") + "\n\n") + "Other arguments:\n" + (this.__format_help_argument("quiet") + "\n") + (this.__format_help_argument("verbose") + "\n") + this.__format_help_argument("example") ); process.exit(0); } /** * Runs version * @private * @return {undefined} */ __run_version() { console.log("Chappe CLI v" + version); process.exit(0); } /** * Runs default * @private * @return {undefined} */ __run_default() { let _has_output = ( (args.quiet && !args.verbose) ? false : true ); // Acquire task from action let _task = this.__acquire_action(); // Dump banner? if (_has_output === true) { console.log(this.__dump_banner()); } // Acquire context global.CONTEXT = this.__acquire_context(_task); if (_has_output === true) { console.log( ("Chappe will " + _task + " docs with context:\n") + (this.__dump_context(global.CONTEXT) + "\n") ); } // Setup spinner // Throttle down spinner refresh interval, to ease w/ terminal CPU usage \ // when performing long operations eg. 'watch'. let _spinner = ora({ text : "Working...\n", color : "cyan", interval : 350 }); // Import Gulp instance let _gulp = require("gulp"); // Setup error traps this.__setup_error_traps(_gulp, _spinner, _task); // Setup Gulp logging? (pre-task mode, only if verbose) if (args.verbose) { this.__setup_gulp_logging(_gulp, _spinner); } // Import the Gulpfile let _gulpfile = require("../gulpfile.js"); // Start spinner _spinner.start(); // Build docs _gulpfile[_task]((error) => { // Any error occured? if (error) { // Throw error and stop spinner _spinner.fail("Error:"); console.log(error); _spinner.stop(); process.exit(1); } else { // Show success spinner (depending on task success rule) let _success_rules = ( this.__spinner_successes[_task] || this.__spinner_successes.default ); _spinner[_success_rules.method](_success_rules.text); // Setup Gulp logging? (post-task mode, only for certain actions) if (this.__actions_logging.includes(_task) === true) { this.__setup_gulp_logging(_gulp, _spinner); } } }); } /** * Formats help actions * @private * @param {object} actions * @return {string} Formatted help actions */ __format_help_actions(actions) { let _actions = actions.map((action) => { return (" " + action); }); return _actions.join("\n"); } /** * Formats help argument * @private * @param {string} name * @return {string} Formatted help argument */ __format_help_argument(name) { let _argument = (" --" + name); // Append default value? (if any) let _default_value = this.__context_defaults.default[name]; if (typeof _default_value !== "undefined") { _argument += (" (defaults to: '" + _default_value + "')"); } return _argument; } /** * Dumps the banner * @private * @return {string} Dumped banner */ __dump_banner() { // Generate bundle name let _bundle_name = ("Chappe v" + version); // Read banner file let _buffer = ( fs.readFileSync(path.join(__dirname, "../.banner")) ); // Convert banner to string and inject bundle name let _banner = _buffer.toString().replace("{{bundle}}", _bundle_name); return _banner; } /** * Dumps the context * @private * @param {object} context * @return {string} Dumped context */ __dump_context(context) { let _context = Object.keys(context).map((key) => { return (" " + key + " -> " + context[key]); }); return _context.join("\n"); } /** * Acquires current action * @private * @return {string} Current action */ __acquire_action() { let _action; for (let _i = 0; _i < this.__actions_available.length; _i++) { let _cur_action = this.__actions_available[_i]; if (process.argv.includes(_cur_action) === true) { _action = _cur_action; break; } } return (_action || "build"); } /** * Acquires current context * @private * @param {string} task * @return {object} Current context */ __acquire_context(task) { // Acquire defaults let _defaults = ( this.__context_defaults[(args.example ? "example" : "default")] ); // Inject target in defaults? if (args.example) { _defaults = this.__inject_defaults_target( _defaults, args.example ); } // Generate context // Notice: the values are temporarily represented as arrays, to ease with \ // data normalization steps. let _context = { config : (args.config || _defaults.config).split(","), assets : [(args.assets || _defaults.assets)], data : [(args.data || _defaults.data)], dist : [(args.dist || _defaults.dist)], temp : [(args.temp || _defaults.temp)], env : [(args.env || _defaults.env)] }; // Append serve-related context? if (task === "serve") { _context.host = [(args.host || _defaults.host)]; _context.port = [parseInt((args.port || _defaults.port), 10)]; } // Expand context paths let _base_path = process.cwd(); this.__path_expand_keys.forEach((key) => { let _context_values = _context[key]; for (let _i = 0; _i < _context_values.length; _i++) { // Path is not already in absolute format? (convert to absolute) if (path.isAbsolute(_context_values[_i]) !== true) { _context_values[_i] = ( path.join(_base_path, _context_values[_i]) ); } } }); // Re-join context values as bare strings (from lists) for (let _key in _context) { // Notice, this retains non-string types untouched if (_context[_key].length === 1) { _context[_key] = _context[_key][0]; } else { _context[_key] = _context[_key].join(","); } } // Validate final context if (this.__env_available.includes(_context.env) !== true) { throw new Error( "Environment value not recognized: " + _context.env ); } return _context; } /** * Inject target into defaults * @private * @param {object} defaults * @param {string} target * @return {object} Defaults w/ injections */ __inject_defaults_target(defaults, target) { // Important: create a new defaults object, as not to alter the given one, \ // which could be re-used later. let _injected_defaults = {}; for (let _key in defaults) { let _cur_default = defaults[_key]; if (typeof _cur_default === "string") { _cur_default = _cur_default.replace("{{target}}", target); } _injected_defaults[_key] = _cur_default; } return _injected_defaults; } /** * Setups error traps * @private * @param {object} gulp * @param {object} spinner * @param {string} task * @return {undefined} */ __setup_error_traps(gulp, spinner, task) { // Check if should crash on error let _crash_on_error = ( (this.__actions_no_aborts.includes(task) === true) ? false : true ); // Setup process events process.once("exit", (code) => { if (code > 0) { // Self-kill, because apparently even if calling process.exit(1), the \ // process stays active and sticky in certain cases (due to \ // registered listeners and file descriptors in some Gulp libraries). // Warning: this is a bit hacky! process.exitCode = code; process.kill(process.pid, "SIGKILL"); } }); process.on("uncaughtException", (error) => { // Throw error and stop spinner spinner.fail("Unexpected failure:"); console.log( (error && error.context) ? error.context : error ); spinner.stop(); process.exit(1); }); // Important: setup Gulp error listener, otherwise any error will get \ // uncaught and be handled by 'uncaughtException' at the process-level. gulp.on("error", (event) => { // Freeze spinner w/ failure spinner.fail( "Error in '" + event.name + "':" ); if (event.error) { console.log(event.error); } // Restart the spinner spinner.start(); if (_crash_on_error === true) { process.exit(1); } }); } /** * Setups Gulp logging * @private * @param {object} gulp * @param {object} spinner * @return {undefined} */ __setup_gulp_logging(gulp, spinner) { // Not quiet and not already setup? if (!args.quiet && this.__has_setup_gulp_logging !== true) { this.__has_setup_gulp_logging = true; gulp.on("start", (event) => { // Freeze spinner w/ information // Notice: do not log meta-events eg. '<series>' if (event.name.startsWith("<") !== true) { spinner.info( "Starting '" + event.name + "'..." ); } }); gulp.on("stop", (event) => { // Freeze spinner w/ success // Notice: do not log meta-events eg. '<series>' if (event.name.startsWith("<") !== true) { spinner.succeed( "Finished '" + event.name + "'" ); // Restart the spinner spinner.start(); } }); } } // jscs:enable disallowIdentifierNames } (new ChappeCLI()).run();