From 71c13a67f63379d17b5077a9d7400eb168eda43b Mon Sep 17 00:00:00 2001 From: Kabir Shah Date: Sun, 13 Oct 2019 15:36:55 -0700 Subject: [PATCH] improve CLI UX Adds CLI commands, including a `help` command capable of displaying help messages for individual commands. This will make it easier for people to directly install `moon-cli` and work with it without having to reference the documentation for the previous simple but obscure behavior. There are now three commands, `moon version`, `moon help`, and `moon create`. They all have error messages and help messages to guide users through the process of using them. `moon create` now accepts the template as an option rather than a positional argument, using the `-t` or `--template` option instead. --- build/build.js | 24 ++-- packages/moon-browser/config.js | 2 +- packages/moon-cli/config.js | 2 +- packages/moon-cli/dist/moon-cli.js | 161 +++++++++++++++++++--- packages/moon-cli/dist/moon-cli.min.js | 2 +- packages/moon-cli/src/index.js | 183 ++++++++++++++++++++++--- packages/moon-compiler/config.js | 2 +- packages/moon/config.js | 2 +- packages/mvl/config.js | 2 +- 9 files changed, 329 insertions(+), 51 deletions(-) diff --git a/build/build.js b/build/build.js index bdc290fe..4fb6e6bb 100644 --- a/build/build.js +++ b/build/build.js @@ -5,9 +5,10 @@ const uglify = require("uglify-js"); const gzipSize = require("gzip-size"); const fs = require("fs"); const path = require("path"); -const pkg = require("../package.json"); -const SPACES_RE = / /g; -const ENV_RE = /process\.env\.MOON_ENV/g; +const version = require("../package.json").version; +const spacesRE = / /g; +const versionRE = /process\.env\.MOON_VERSION/g; +const envRE = /process\.env\.MOON_ENV/g; const resolver = { resolveId(id, origin) { @@ -18,11 +19,11 @@ const resolver = { async function build(package) { const options = require(`../packages/${package}/config.js`); const name = options.name; - const exportName = options.exportName; + const nameExport = options.nameExport; const type = options.type; const comment = `${type === "executable" ? "#!/usr/bin/env node\n" : ""}/** - * ${name} v${pkg.version} + * ${name} v${version} * Copyright 2016-2019 Kabir Shah * Released under the MIT License * https://kbrsh.github.io/moon @@ -36,7 +37,7 @@ async function build(package) { eslint(), babel() ], - onwarn: (warning) => { + onwarn: warning => { if (warning.code !== "CIRCULAR_DEPENDENCY") { console.warn(`Rollup [Warn]: ${warning}`); } @@ -44,21 +45,22 @@ async function build(package) { }); let { output } = await bundle.generate({ - name: exportName, + name: nameExport, format: "iife" }); output = output[0].code; - output = output.replace(SPACES_RE, "\t"); + output = output.replace(spacesRE, "\t"); if (type === "module") { - output = fs.readFileSync("./build/wrapper.js").toString().replace("MODULE_NAME", exportName).replace("MODULE_CONTENT", output.split("\n").slice(1, -3).join("\n")); + output = fs.readFileSync("./build/wrapper.js").toString().replace("MODULE_NAME", nameExport).replace("MODULE_CONTENT", output.split("\n").slice(1, -3).join("\n")); } output = output.replace("'use strict'", "\"use strict\""); + output = output.replace(versionRE, `"${version}"`); - const developmentCode = comment + output.replace(ENV_RE, '"development"'); - const productionCode = comment + uglify.minify(output.replace(ENV_RE, '"production"'), { + const developmentCode = comment + output.replace(envRE, '"development"'); + const productionCode = comment + uglify.minify(output.replace(envRE, '"production"'), { output: { ascii_only: true } diff --git a/packages/moon-browser/config.js b/packages/moon-browser/config.js index 5627bb47..b6b84794 100644 --- a/packages/moon-browser/config.js +++ b/packages/moon-browser/config.js @@ -1,5 +1,5 @@ module.exports = { name: "Moon Browser", - exportName: "MoonBrowser", + nameExport: "MoonBrowser", type: "script" }; diff --git a/packages/moon-cli/config.js b/packages/moon-cli/config.js index 47024a2c..97b4fd1c 100644 --- a/packages/moon-cli/config.js +++ b/packages/moon-cli/config.js @@ -1,5 +1,5 @@ module.exports = { name: "Moon CLI", - exportName: "MoonCLI", + nameExport: "MoonCLI", type: "executable" }; diff --git a/packages/moon-cli/dist/moon-cli.js b/packages/moon-cli/dist/moon-cli.js index 167f54c7..6a564b7d 100644 --- a/packages/moon-cli/dist/moon-cli.js +++ b/packages/moon-cli/dist/moon-cli.js @@ -16,21 +16,59 @@ var exec = require("child_process").exec; - var name = process.argv[2]; - var repo = process.argv[3] || "kbrsh/moon-template"; - var archive = { - method: "GET", - host: "api.github.com", - path: "/repos/" + repo + "/tarball/master", - headers: { - "User-Agent": "Node.js" + var parameterRE = /<\w+>/g; + var optionRE = /\[\w+\]|--?\w+/g; + var help = { + version: { + usage: "moon version", + description: "output Moon CLI version" + }, + help: { + usage: "moon help ", + description: "output help message for command", + parameters: { + "": "name of Moon CLI command" + } + }, + create: { + usage: "moon create [options]", + description: "create application in new directory", + parameters: { + "": "name of application and directory" + }, + options: { + "-t, --template /": "GitHub repository to use as template" + } } }; + var repo, name; + + function highlight(string) { + return string.replace(parameterRE, "\x1b[33m$&\x1b[0m").replace(optionRE, "\x1b[36m$&\x1b[0m"); + } + + function table(object) { + var keys = Object.keys(object); + var max = Math.max.apply(null, keys.map(function (key) { + return key.length; + })); + return keys.map(function (key) { + return "\t" + key + " ".repeat(max - key.length + 3) + object[key]; + }).join("\n"); + } function log(type, message) { console.log("\x1B[34m" + type + "\x1B[0m " + message); } + function logHelp(message) { + console.log(highlight(message)); + } + + function error(message) { + console.log("\x1B[31merror\x1B[0m " + message); + } + function replace(content, sub, subNewString) { var index = content.indexOf(sub); @@ -74,7 +112,7 @@ if (err) throw err; log("clean", archivePath); create(targetPath, targetPath); - log("success", "Generated project \"" + name + "\""); + log("success", "Generated application \x1B[36m" + name + "\x1B[0m"); console.log("To start, run:\n\tcd " + name + "\n\tnpm install\n\tnpm run dev"); }); } @@ -95,15 +133,104 @@ } } - log("Moon", "Generating project"); - https.get(archive, function (res) { - if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location !== undefined) { - https.get(res.headers.location, function (redirectRes) { - download(redirectRes); - }); + var argv = process.argv.length === 2 ? ["help"] : process.argv.slice(2); + var commandName = argv[0]; + var commandArguments = []; + var commandOptions = {}; + + for (var i = 1; i < argv.length; i++) { + var commandArgument = argv[i]; + + if (commandArgument[0] === "-") { + for (; i < argv.length; i++) { + var commandOption = argv[i]; + + if (commandOption[0] === "-") { + commandOptions[commandOption] = true; + } else { + commandOptions[argv[i - 1]] = commandOption; + } + } } else { - download(res); + commandArguments.push(commandArgument); } - }); + } + + switch (commandName) { + case "version": + { + logHelp("Moon CLI v" + "1.0.0-beta.4"); + break; + } + + case "help": + { + var commandNameHelp = commandArguments[0]; + + if (commandNameHelp in help) { + var helpCommand = help[commandNameHelp]; + logHelp("Usage: " + helpCommand.usage + "\n\t" + helpCommand.description); + + if ("parameters" in helpCommand) { + logHelp("\nParameters:\n" + table(helpCommand.parameters)); + } + + if ("options" in helpCommand) { + logHelp("\nOptions:\n" + table(helpCommand.options)); + } + } else { + var tableUsageDescription = {}; + + for (var command in help) { + var _helpCommand = help[command]; + tableUsageDescription[_helpCommand.usage] = _helpCommand.description; + } + + logHelp("Usage: moon [options]\n\nCommands:\n" + table(tableUsageDescription)); + } + + break; + } + + case "create": + { + name = commandArguments[0]; + repo = commandOptions["-t"] || commandOptions["--template"] || "kbrsh/moon-template"; + + if (name === undefined || name.length === 0) { + error("Invalid or unknown name.\n\nAttempted to create an application.\n\nReceived an invalid or unknown name.\n\nExpected a valid name. Run \x1B[35mmoon help create\x1B[0m to see usage information."); + } + + if (repo === true) { + error("Invalid or unknown template.\n\nAttempted to create an application.\n\nReceived an invalid or unknown template.\n\nExpected a valid template. Run \x1B[35mmoon help create\x1B[0m to see usage information."); + } + + var archive = { + method: "GET", + host: "api.github.com", + path: "/repos/" + repo + "/tarball/master", + headers: { + "User-Agent": "Moon" + } + }; + log("Moon", "Generating application"); + https.get(archive, function (res) { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location !== undefined) { + https.get(res.headers.location, function (redirectRes) { + download(redirectRes); + }); + } else { + download(res); + } + }); + break; + } + + default: + { + error("Unrecognized command.\n\nAttempted to execute a command.\n\nReceived a command that does not exist:\n\t" + commandName + "\n\nExpected a valid command. Run \x1B[35mmoon help\x1B[0m to see valid commands."); + break; + } + } }()); diff --git a/packages/moon-cli/dist/moon-cli.min.js b/packages/moon-cli/dist/moon-cli.min.js index 67bcaa8c..e31ed2e6 100755 --- a/packages/moon-cli/dist/moon-cli.min.js +++ b/packages/moon-cli/dist/moon-cli.min.js @@ -5,4 +5,4 @@ * Released under the MIT License * https://kbrsh.github.io/moon */ -!function(){"use strict";var c=require("fs"),s=require("path"),n=require("https"),r=require("child_process").exec,u=process.argv[2],o=process.argv[3]||"kbrsh/moon-template",e={method:"GET",host:"api.github.com",path:"/repos/"+o+"/tarball/master",headers:{"User-Agent":"Node.js"}};function l(e,n){console.log("\x1b[34m"+e+"\x1b[0m "+n)}function f(e,n,t){var r=e.indexOf(n);if(-1===r)return e;var o=e.slice(0,r),i=f(e.slice(r+n.length),n,t),a=Buffer.from(t);return Buffer.concat([o,a,i],o.length+a.length+i.length)}function t(e){var n=s.join(__dirname,"moon-template.tar.gz"),t=c.createWriteStream(n);e.on("data",function(e){t.write(e)}),e.on("end",function(){t.end(),l("download",o),function(n){var t=s.join(process.cwd(),u);r("mkdir "+t,function(e){if(e)throw e;r("tar -xzf "+n+" -C "+t+" --strip=1",function(e){if(e)throw e;l("install",t),function(n,t){c.unlink(n,function(e){if(e)throw e;l("clean",n),function e(n,t){var r=c.readdirSync(n);for(var o=0;o/g,r=/\[\w+\]|--?\w+/g,e={version:{usage:"moon version",description:"output Moon CLI version"},help:{usage:"moon help ",description:"output help message for command",parameters:{"":"name of Moon CLI command"}},create:{usage:"moon create [options]",description:"create application in new directory",parameters:{"":"name of application and directory"},options:{"-t, --template /":"GitHub repository to use as template"}}};function i(n){var e=Object.keys(n),t=Math.max.apply(null,e.map(function(e){return e.length}));return e.map(function(e){return"\t"+e+" ".repeat(t-e.length+3)+n[e]}).join("\n")}function l(e,n){console.log("\x1b[34m"+e+"\x1b[0m "+n)}function p(e){console.log(function(e){return e.replace(t,"\x1b[33m$&\x1b[0m").replace(r,"\x1b[36m$&\x1b[0m")}(e))}function u(e){console.log("\x1b[31merror\x1b[0m "+e)}function d(e,n,t){var o=e.indexOf(n);if(-1===o)return e;var a=e.slice(0,o),r=d(e.slice(o+n.length),n,t),i=Buffer.from(t);return Buffer.concat([a,i,r],a.length+i.length+r.length)}function f(e){var n=m.join(__dirname,"moon-template.tar.gz"),t=s.createWriteStream(n);e.on("data",function(e){t.write(e)}),e.on("end",function(){t.end(),l("download",o),function(n){var t=m.join(process.cwd(),c);a("mkdir "+t,function(e){if(e)throw e;a("tar -xzf "+n+" -C "+t+" --strip=1",function(e){if(e)throw e;l("install",t),function(n,t){s.unlink(n,function(e){if(e)throw e;l("clean",n),function e(n,t){var o=s.readdirSync(n);for(var a=0;a [options]\n\nCommands:\n"+i(M))}break;case"create":c=g[0],o=w["-t"]||w["--template"]||"kbrsh/moon-template",void 0!==c&&0!==c.length||u("Invalid or unknown name.\n\nAttempted to create an application.\n\nReceived an invalid or unknown name.\n\nExpected a valid name. Run \x1b[35mmoon help create\x1b[0m to see usage information."),!0===o&&u("Invalid or unknown template.\n\nAttempted to create an application.\n\nReceived an invalid or unknown template.\n\nExpected a valid template. Run \x1b[35mmoon help create\x1b[0m to see usage information.");var I={method:"GET",host:"api.github.com",path:"/repos/"+o+"/tarball/master",headers:{"User-Agent":"Moon"}};l("Moon","Generating application"),n.get(I,function(e){300<=e.statusCode&&e.statusCode<400&&void 0!==e.headers.location?n.get(e.headers.location,function(e){f(e)}):f(e)});break;default:u("Unrecognized command.\n\nAttempted to execute a command.\n\nReceived a command that does not exist:\n\t"+h+"\n\nExpected a valid command. Run \x1b[35mmoon help\x1b[0m to see valid commands.")}}(); \ No newline at end of file diff --git a/packages/moon-cli/src/index.js b/packages/moon-cli/src/index.js index 736e6490..1457444b 100755 --- a/packages/moon-cli/src/index.js +++ b/packages/moon-cli/src/index.js @@ -3,21 +3,57 @@ const path = require("path"); const https = require("https"); const exec = require("child_process").exec; -const name = process.argv[2]; -const repo = process.argv[3] || "kbrsh/moon-template"; -const archive = { - method: "GET", - host: "api.github.com", - path: `/repos/${repo}/tarball/master`, - headers: { - "User-Agent": "Node.js" +const parameterRE = /<\w+>/g; +const optionRE = /\[\w+\]|--?\w+/g; + +const help = { + version: { + usage: "moon version", + description: "output Moon CLI version" + }, + help: { + usage: "moon help ", + description: "output help message for command", + parameters: { + "": "name of Moon CLI command" + } + }, + create: { + usage: "moon create [options]", + description: "create application in new directory", + parameters: { + "": "name of application and directory" + }, + options: { + "-t, --template /": "GitHub repository to use as template" + } } }; +let repo, name; + +function highlight(string) { + return string.replace(parameterRE, "\x1b[33m$&\x1b[0m").replace(optionRE, "\x1b[36m$&\x1b[0m"); +} + +function table(object) { + const keys = Object.keys(object); + const max = Math.max.apply(null, keys.map(key => key.length)); + return keys.map(key => "\t" + key + " ".repeat(max - key.length + 3) + object[key]).join("\n"); +} + function log(type, message) { console.log(`\x1b[34m${type}\x1b[0m ${message}`); } +function logHelp(message) { + console.log(highlight(message)); +} + +function error(message) { + console.log(`\x1b[31merror\x1b[0m ${message}`); +} + function replace(content, sub, subNewString) { const index = content.indexOf(sub); @@ -69,7 +105,7 @@ function clean(archivePath, targetPath) { log("clean", archivePath); create(targetPath, targetPath); - log("success", `Generated project "${name}"`); + log("success", `Generated application \x1b[36m${name}\x1b[0m`); console.log(`To start, run: cd ${name} npm install @@ -93,14 +129,127 @@ function create(currentPath, targetPath) { } } -log("Moon", "Generating project"); +const argv = process.argv.length === 2 ? ["help"] : process.argv.slice(2); +const commandName = argv[0]; +const commandArguments = []; +const commandOptions = {}; -https.get(archive, (res) => { - if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location !== undefined) { - https.get(res.headers.location, (redirectRes) => { - download(redirectRes); - }); +for (let i = 1; i < argv.length; i++) { + const commandArgument = argv[i]; + + if (commandArgument[0] === "-") { + for (; i < argv.length; i++) { + const commandOption = argv[i]; + + if (commandOption[0] === "-") { + commandOptions[commandOption] = true; + } else { + commandOptions[argv[i - 1]] = commandOption; + } + } } else { - download(res); + commandArguments.push(commandArgument); + } +} + +switch (commandName) { + case "version": { + logHelp(`Moon CLI v${process.env.MOON_VERSION}`); + break; } -}); + + case "help": { + const commandNameHelp = commandArguments[0]; + + if (commandNameHelp in help) { + const helpCommand = help[commandNameHelp]; + + logHelp(`Usage: ${helpCommand.usage} + ${helpCommand.description}`); + + if ("parameters" in helpCommand) { + logHelp(` +Parameters: +${table(helpCommand.parameters)}`); + } + + if ("options" in helpCommand) { + logHelp(` +Options: +${table(helpCommand.options)}`); + } + } else { + const tableUsageDescription = {}; + + for (const command in help) { + const helpCommand = help[command]; + tableUsageDescription[helpCommand.usage] = helpCommand.description; + } + + logHelp(`Usage: moon [options] + +Commands: +${table(tableUsageDescription)}`); + } + break; + } + + case "create": { + name = commandArguments[0]; + repo = commandOptions["-t"] || commandOptions["--template"] || "kbrsh/moon-template"; + + if (name === undefined || name.length === 0) { + error(`Invalid or unknown name. + +Attempted to create an application. + +Received an invalid or unknown name. + +Expected a valid name. Run \x1b[35mmoon help create\x1b[0m to see usage information.`); + } + + if (repo === true) { + error(`Invalid or unknown template. + +Attempted to create an application. + +Received an invalid or unknown template. + +Expected a valid template. Run \x1b[35mmoon help create\x1b[0m to see usage information.`); + } + + const archive = { + method: "GET", + host: "api.github.com", + path: `/repos/${repo}/tarball/master`, + headers: { + "User-Agent": "Moon" + } + }; + + log("Moon", "Generating application"); + + https.get(archive, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location !== undefined) { + https.get(res.headers.location, (redirectRes) => { + download(redirectRes); + }); + } else { + download(res); + } + }); + break; + } + + default: { + error(`Unrecognized command. + +Attempted to execute a command. + +Received a command that does not exist: + ${commandName} + +Expected a valid command. Run \x1b[35mmoon help\x1b[0m to see valid commands.`); + break; + } +} diff --git a/packages/moon-compiler/config.js b/packages/moon-compiler/config.js index da0a0a0d..64edf14b 100644 --- a/packages/moon-compiler/config.js +++ b/packages/moon-compiler/config.js @@ -1,5 +1,5 @@ module.exports = { name: "Moon Compiler", - exportName: "MoonCompiler", + nameExport: "MoonCompiler", type: "module" }; diff --git a/packages/moon/config.js b/packages/moon/config.js index f9ce76fd..336584eb 100644 --- a/packages/moon/config.js +++ b/packages/moon/config.js @@ -1,5 +1,5 @@ module.exports = { name: "Moon", - exportName: "Moon", + nameExport: "Moon", type: "module" }; diff --git a/packages/mvl/config.js b/packages/mvl/config.js index 218ef516..a48919e2 100644 --- a/packages/mvl/config.js +++ b/packages/mvl/config.js @@ -1,5 +1,5 @@ module.exports = { name: "Moon MVL", - exportName: "MoonMVL", + nameExport: "MoonMVL", type: "module" };