diff --git a/package-lock.json b/package-lock.json index 7cac3b77..63404cc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,11 @@ "version": "0.1.0", "license": "Apache-2.0", "dependencies": { + "chalk": "4.1.2", "debug": "4.3.4", "execa": "5.1.1", + "ora": "5.4.1", + "pluralize": "8.0.0", "which": "2.0.2", "yargs": "17.5.1" }, @@ -21,9 +24,11 @@ "@types/debug": "4.1.7", "@types/mocha": "9.1.1", "@types/node": "18.0.4", + "@types/pluralize": "0.0.29", "@types/sinon": "10.0.13", "@types/which": "2.0.1", "@types/yargs": "17.0.10", + "cross-env": "7.0.3", "eslint": "8.13.0", "eslint-config-prettier": "8.5.0", "eslint-config-standard": "17.0.0-1", @@ -38,6 +43,7 @@ "prettier": "2.6.2", "rewiremock": "3.14.3", "sinon": "14.0.0", + "strict-event-emitter-types": "2.0.0", "typescript": "4.7.4", "unexpected": "12.0.4", "unexpected-sinon": "11.1.0", @@ -196,6 +202,12 @@ "integrity": "sha512-M0+G6V0Y4YV8cqzHssZpaNCqvYwlCiulmm0PwpNLF55r/+cT8Ol42CHRU1SEaYFH2rTwiiE1aYg/2g2rrtGdPA==", "dev": true }, + "node_modules/@types/pluralize": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.29.tgz", + "integrity": "sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA==", + "dev": true + }, "node_modules/@types/sinon": { "version": "10.0.13", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.13.tgz", @@ -312,6 +324,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -504,7 +528,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -529,6 +552,52 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -746,7 +815,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -820,7 +888,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, "dependencies": { "restore-cursor": "^3.1.0" }, @@ -828,6 +895,17 @@ "node": ">=8" } }, + "node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-truncate": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", @@ -904,6 +982,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1016,6 +1102,24 @@ "sha.js": "^2.4.8" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1073,6 +1177,14 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "node_modules/defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==", + "dependencies": { + "clone": "^1.0.2" + } + }, "node_modules/define-properties": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", @@ -2159,7 +2271,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2326,7 +2437,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -2398,8 +2508,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", @@ -2541,6 +2650,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -2659,7 +2776,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, "engines": { "node": ">=10" }, @@ -2938,7 +3054,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -3594,6 +3709,28 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", @@ -3784,6 +3921,14 @@ "node": ">=0.10" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "engines": { + "node": ">=4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4060,7 +4205,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -4338,11 +4482,16 @@ "xtend": "^4.0.0" } }, + "node_modules/strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==", + "dev": true + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -4351,7 +4500,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -4452,7 +4600,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -4580,18 +4727,6 @@ "node": ">=4" } }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", @@ -4814,8 +4949,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/util/node_modules/inherits": { "version": "2.0.3", @@ -4835,6 +4969,14 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5173,6 +5315,12 @@ "integrity": "sha512-M0+G6V0Y4YV8cqzHssZpaNCqvYwlCiulmm0PwpNLF55r/+cT8Ol42CHRU1SEaYFH2rTwiiE1aYg/2g2rrtGdPA==", "dev": true }, + "@types/pluralize": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.29.tgz", + "integrity": "sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA==", + "dev": true + }, "@types/sinon": { "version": "10.0.13", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.13.tgz", @@ -5263,6 +5411,14 @@ "dev": true, "requires": { "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } } }, "ansi-regex": { @@ -5427,8 +5583,7 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "binary-extensions": { "version": "2.2.0", @@ -5436,6 +5591,37 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -5628,7 +5814,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5681,11 +5866,15 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, "requires": { "restore-cursor": "^3.1.0" } }, + "cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==" + }, "cli-truncate": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", @@ -5740,6 +5929,11 @@ "wrap-ansi": "^7.0.0" } }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==" + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5846,6 +6040,15 @@ "sha.js": "^2.4.8" } }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5889,6 +6092,14 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==", + "requires": { + "clone": "^1.0.2" + } + }, "define-properties": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", @@ -6709,8 +6920,7 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "has-property-descriptors": { "version": "1.0.0", @@ -6822,8 +7032,7 @@ "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { "version": "5.2.0", @@ -6866,8 +7075,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.8", @@ -6964,6 +7172,11 @@ "is-extglob": "^2.1.1" } }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==" + }, "is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -7036,8 +7249,7 @@ "is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" }, "is-weakref": { "version": "1.0.2", @@ -7261,7 +7473,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, "requires": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -7770,6 +7981,22 @@ "word-wrap": "^1.2.3" } }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + } + }, "os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", @@ -7912,6 +8139,11 @@ "integrity": "sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA==", "dev": true }, + "pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==" + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8120,7 +8352,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, "requires": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -8330,11 +8561,16 @@ "xtend": "^4.0.0" } }, + "strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==", + "dev": true + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "requires": { "safe-buffer": "~5.2.0" }, @@ -8342,8 +8578,7 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" } } }, @@ -8408,7 +8643,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -8511,12 +8745,6 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true - }, "typescript": { "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", @@ -8713,8 +8941,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "v8-compile-cache": { "version": "2.3.0", @@ -8728,6 +8955,14 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "requires": { + "defaults": "^1.0.3" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 91054885..262c7312 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "prepare": "husky install && npm run build", "test:smoke": "node ./src/cli.js --version", "test": "mocha \"test/**/*.spec.js\"", - "posttest": "markdownlint-cli2 \"*.md\" && eslint ." + "posttest": "markdownlint-cli2 \"*.md\" && eslint .", + "update-snapshots": "cross-env UNEXPECTED_SNAPSHOT=1 npm test" }, "lint-staged": { "*.js": [ @@ -56,9 +57,11 @@ "@types/debug": "4.1.7", "@types/mocha": "9.1.1", "@types/node": "18.0.4", + "@types/pluralize": "0.0.29", "@types/sinon": "10.0.13", "@types/which": "2.0.1", "@types/yargs": "17.0.10", + "cross-env": "7.0.3", "eslint": "8.13.0", "eslint-config-prettier": "8.5.0", "eslint-config-standard": "17.0.0-1", @@ -73,6 +76,7 @@ "prettier": "2.6.2", "rewiremock": "3.14.3", "sinon": "14.0.0", + "strict-event-emitter-types": "2.0.0", "typescript": "4.7.4", "unexpected": "12.0.4", "unexpected-sinon": "11.1.0", @@ -82,8 +86,11 @@ "node": ">=14" }, "dependencies": { + "chalk": "4.1.2", "debug": "4.3.4", "execa": "5.1.1", + "ora": "5.4.1", + "pluralize": "8.0.0", "which": "2.0.2", "yargs": "17.5.1" }, diff --git a/src/cli.js b/src/cli.js index 6d3efcdd..20691c9e 100755 --- a/src/cli.js +++ b/src/cli.js @@ -1,8 +1,11 @@ #!/usr/bin/env node +const pluralize = require('pluralize'); const yargs = require('yargs/yargs'); -const {smoke} = require('./index.js'); - +const {version} = require('../package.json'); +const {Smoker, events} = require('./index.js'); +const ora = require('ora'); +const {blue, white} = require('chalk'); const BEHAVIOR_GROUP = 'Behavior:'; /** @@ -73,9 +76,14 @@ async function main(args) { group: BEHAVIOR_GROUP, normalize: true, }, - quiet: { + verbose: { + type: 'boolean', + describe: 'Print output from npm', + group: BEHAVIOR_GROUP, + }, + bail: { type: 'boolean', - describe: 'Suppress output from `npm`', + describe: 'When running scripts, halt on first error', group: BEHAVIOR_GROUP, }, linger: { @@ -100,7 +108,97 @@ async function main(args) { ...(argv.scripts ?? []), ]; - await smoke(scripts, argv); + const smoker = new Smoker(scripts, argv); + + const spinner = ora(); + + smoker + .on(events.SMOKE_BEGIN, () => { + console.error( + `šŸ’Ø ${blue('midnight-smoker')} ${white(`v${version}`)}` + ); + }) + .on(events.FIND_NPM_BEGIN, () => { + spinner.start('Looking for npm...'); + }) + .on(events.FIND_NPM_OK, (path) => { + spinner.succeed(`Found npm at ${path}`); + }) + .on(events.FIND_NPM_FAILED, (err) => { + spinner.fail(`Could not find npm: ${err.message}`); + process.exitCode = 1; + }) + .on(events.PACK_BEGIN, () => { + /** @type {string} */ + let what; + if (argv.workspace?.length) { + what = pluralize('workspace', argv.workspace.length, true); + } else if (argv.all) { + what = 'all workspaces'; + if (argv.includeRoot) { + what += ' (and the workspace root)'; + } + } else { + what = 'current project'; + } + spinner.start(`Packing ${what}...`); + }) + .on(events.PACK_OK, (packItems) => { + spinner.succeed( + `Packed ${pluralize('package', packItems.length, true)}` + ); + }) + .on(events.PACK_FAILED, (err) => { + spinner.fail(err.message); + process.exitCode = 1; + }) + .on(events.INSTALL_BEGIN, (packItems) => { + spinner.start( + `Installing from ${pluralize( + 'tarball', + packItems.length, + true + )}...` + ); + }) + .on(events.INSTALL_FAILED, (err) => { + spinner.fail(err.message); + process.exitCode = 1; + }) + .on(events.INSTALL_OK, (packItems) => { + spinner.succeed( + `Installed ${pluralize('package', packItems.length, true)}` + ); + }) + .on(events.RUN_SCRIPTS_BEGIN, ({total}) => { + spinner.start(`Running script 0/${total}...`); + }) + .on(events.RUN_SCRIPT_BEGIN, ({current, total}) => { + spinner.text = `Running script ${current}/${total}...`; + }) + .on(events.RUN_SCRIPT_FAILED, () => { + process.exitCode = 1; + }) + .on(events.RUN_SCRIPTS_OK, ({total}) => { + spinner.succeed( + `Successfully ran ${pluralize('script', total, true)}` + ); + }) + .on(events.RUN_SCRIPTS_FAILED, ({total, executed, failures}) => { + spinner.fail( + `${failures} of ${total} ${pluralize('script', total)} failed` + ); + process.exitCode = 1; + }) + .on(events.SMOKE_FAILED, (err) => { + spinner.fail(err.message); + process.exitCode = 1; + }) + .on(events.SMOKE_OK, () => { + spinner.succeed('Lovey-dovey! šŸ’–'); + }); + + await smoker.smoke(); } ) .help() diff --git a/src/index.js b/src/index.js index fc7c45b6..53d232ff 100644 --- a/src/index.js +++ b/src/index.js @@ -3,11 +3,57 @@ const debug = require('debug')('midnight-smoker'); const fs = require('node:fs/promises'); const path = require('node:path'); const {tmpdir} = require('node:os'); -const {node: execa} = require('execa'); -const console = require('node:console'); +const execa = require('execa'); +const {EventEmitter} = require('node:events'); const TMP_DIR_PREFIX = 'midnight-smoker-'; +const { + SMOKE_BEGIN, + SMOKE_OK, + SMOKE_FAILED, + FIND_NPM_BEGIN, + FIND_NPM_FAILED, + FIND_NPM_OK, + PACK_BEGIN, + PACK_FAILED, + PACK_OK, + INSTALL_BEGIN, + INSTALL_FAILED, + INSTALL_OK, + RUN_NPM_BEGIN, + RUN_NPM_FAILED, + RUN_NPM_OK, + RUN_SCRIPTS_BEGIN, + RUN_SCRIPTS_FAILED, + RUN_SCRIPTS_OK, + RUN_SCRIPT_BEGIN, + RUN_SCRIPT_FAILED, + RUN_SCRIPT_OK, +} = (exports.events = /** @type {const} */ ({ + SMOKE_BEGIN: 'SmokeBegin', + SMOKE_OK: 'SmokeOk', + SMOKE_FAILED: 'SmokeFailed', + FIND_NPM_BEGIN: 'FindNpmBegin', + FIND_NPM_FAILED: 'FindNpmFailed', + FIND_NPM_OK: 'FindNpmOk', + PACK_BEGIN: 'PackBegin', + PACK_FAILED: 'PackFailed', + PACK_OK: 'PackOk', + INSTALL_BEGIN: 'InstallBegin', + INSTALL_FAILED: 'InstallFailed', + INSTALL_OK: 'InstallOk', + RUN_NPM_BEGIN: 'RunNpmBegin', + RUN_NPM_OK: 'RunNpmOk', + RUN_NPM_FAILED: 'RunNpmFailed', + RUN_SCRIPTS_BEGIN: 'RunScriptsBegin', + RUN_SCRIPTS_FAILED: 'RunScriptsFailed', + RUN_SCRIPTS_OK: 'RunScriptsOk', + RUN_SCRIPT_BEGIN: 'RunScriptBegin', + RUN_SCRIPT_FAILED: 'RunScriptFailed', + RUN_SCRIPT_OK: 'RunScriptOk', +})); + /** * Trims all strings in an array and removes empty strings. * Returns empty array if input is falsy. @@ -34,7 +80,14 @@ function pathToPackageName(dirpath) { return dirs[dirs.length - 1]; } -class Smoker { +function createStrictEventEmitterClass() { + const TypedEmitter = /** @type { {new(): TSmokerEmitter} } */ ( + /** @type {unknown} */ (EventEmitter) + ); + return TypedEmitter; +} + +class Smoker extends createStrictEventEmitterClass() { /** * @type {string[]} */ @@ -57,7 +110,7 @@ class Smoker { /** * @type {boolean} */ - #quiet = false; + #verbose = false; /** @type {boolean} */ #clean = false; @@ -74,12 +127,16 @@ class Smoker { /** @type {string[]} */ #extraNpmInstallArgs; + /** @type {boolean} */ + #bail = false; + /** * * @param {string|string[]} scripts * @param {SmokerOptions} [opts] */ constructor(scripts, opts = {}) { + super(); if (typeof scripts === 'string') { scripts = [scripts]; } @@ -88,11 +145,12 @@ class Smoker { this.#force = Boolean(opts.force); this.#clean = Boolean(opts.clean); - this.#quiet = Boolean(opts.quiet); + this.#verbose = Boolean(opts.verbose); this.#includeWorkspaceRoot = Boolean(opts.includeRoot); if (this.#includeWorkspaceRoot) { opts.all = true; } + this.#bail = Boolean(opts.bail); this.#allWorkspaces = Boolean(opts.all); this.#workspaces = normalizeArray(opts.workspace); if (this.#allWorkspaces && this.#workspaces.length) { @@ -101,16 +159,20 @@ class Smoker { ); } this.#extraNpmInstallArgs = normalizeArray(opts.installArgs); - this.opts = Object.freeze(opts); } async smoke() { + this.emit(SMOKE_BEGIN); try { const packItems = await this.pack(); debug('(smoke) Received %d packed packages', packItems.length); await this.install(packItems); - await this.runScript(packItems); + await this.runScripts(packItems); + this.emit(SMOKE_OK); + } catch (err) { + this.emit(SMOKE_FAILED, /** @type {Error} */ (err)); + throw err; } finally { await this.cleanup(); } @@ -128,10 +190,22 @@ class Smoker { this.#npmPath = this.opts.npm.trim(); return this.#npmPath; } - const npmPath = await which('npm'); - debug('(findNpm) Found npm at %s', npmPath); - this.#npmPath = npmPath; - return npmPath; + this.emit(FIND_NPM_BEGIN); + try { + const npmPath = await which('npm'); + // using #runNpm here would be recursive + const {stdout: version} = await execa(process.execPath, [ + npmPath, + '--version', + ]); + debug('(findNpm) Found npm %s at %s', version, npmPath); + this.#npmPath = npmPath; + this.emit(FIND_NPM_OK, npmPath); + return npmPath; + } catch (err) { + this.emit(FIND_NPM_FAILED, /** @type {Error} */ (err)); + throw err; + } } /** @@ -140,6 +214,7 @@ class Smoker { * @returns {Promise} */ async #cleanWorkingDirectory(wd) { + // TODO EMIT try { await fs.rm(wd, {recursive: true}); } catch (e) { @@ -156,6 +231,7 @@ class Smoker { * @returns {Promise} */ async #assertNoWorkingDirectory(wd) { + // TODO EMIT try { await fs.stat(wd); } catch { @@ -170,6 +246,7 @@ class Smoker { * @returns {Promise} */ async #createTempDirectory() { + // TODO EMIT try { const prefix = path.join(tmpdir(), TMP_DIR_PREFIX); return await fs.mkdtemp(prefix); @@ -183,6 +260,7 @@ class Smoker { * @returns {Promise} */ async createWorkingDirectory() { + // TODO EMIT if (this.#cwd) { return this.#cwd; } @@ -215,6 +293,7 @@ class Smoker { */ async pack() { const npmPath = await this.findNpm(); + this.emit(PACK_BEGIN); const cwd = await this.createWorkingDirectory(); let packArgs = [ @@ -222,7 +301,6 @@ class Smoker { '--json', `--pack-destination=${cwd}`, '--foreground-scripts=false', // suppress output of lifecycle scripts so json can be parsed - '--silent', // XXX needed? ]; if (this.#workspaces.length) { packArgs = [ @@ -236,33 +314,40 @@ class Smoker { } } - debug('(pack) Executing: %s %s', npmPath, packArgs.join(' ')); - const proc = execa(npmPath, packArgs); - - if (!this.#quiet) { - proc.stdout?.on('data', (data) => { - console.error(String(data)); - }); + /** @type {execa.ExecaReturnValue} */ + let value; + try { + debug('(pack) Executing: %s %s', npmPath, packArgs.join(' ')); + value = await this.#runNpm(packArgs); + } catch (err) { + this.emit(PACK_FAILED, /** @type {execa.ExecaError} */ (err)); + throw err; } - const {exitCode, stdout: packOutput} = await proc; - if (exitCode) { - throw new Error(`"npm pack" failed with exit code ${exitCode}`); + if (value.exitCode) { + debug('(pack) Failed: %O', value); + const error = new Error( + `"npm pack" failed with exit code ${value.exitCode}` + ); + this.emit(PACK_FAILED, error); + throw error; } /** @type {import('../static').NpmPackItem[]} */ let parsed; + const {stdout: packOutput} = value; try { parsed = JSON.parse(packOutput); - } catch (err) { + } catch { debug('(pack) Failed to parse JSON: %s', packOutput); - const {message} = /** @type {SyntaxError} */ (err); - throw new Error( - `Failed to parse JSON output from "npm pack": ${message}` + const error = new SyntaxError( + `Failed to parse JSON output from "npm pack": ${packOutput}` ); + this.emit(PACK_FAILED, error); + throw error; } - const result = parsed.map(({filename, name}) => { + const packItems = parsed.map(({filename, name}) => { // workaround for https://github.com/npm/cli/issues/3405 filename = filename.replace(/^@(.+?)\//, '$1-'); return { @@ -270,8 +355,59 @@ class Smoker { installPath: path.join(cwd, 'node_modules', name), }; }); - debug('(pack) Packed %d packages', result.length); - return result; + debug('(pack) Packed %d packages', packItems.length); + + this.emit(PACK_OK, packItems); + return packItems; + } + + /** + * + * @param {string[]} args + * @param {execa.Options} [options] + */ + async #runNpm(args, options = {}) { + const npmPath = await this.findNpm(); + const command = `${process.execPath} ${npmPath} ${args.join(' ')}`; + this.emit(RUN_NPM_BEGIN, { + command, + options, + }); + const opts = {...options}; + + /** @type {execa.ExecaChildProcess} */ + let proc; + + try { + proc = execa(process.execPath, [npmPath, ...args], opts); + } catch (err) { + this.emit(RUN_NPM_FAILED, /** @type {execa.ExecaError} */ (err)); + throw err; + } + + if (this.#verbose) { + proc.stdout?.pipe(process.stdout); + proc.stderr?.pipe(process.stderr); + } + + /** + * @type {execa.ExecaReturnValue|undefined} + */ + let value; + /** @type {execa.ExecaError & NodeJS.ErrnoException|undefined} */ + let error; + try { + value = await proc; + this.emit(RUN_NPM_OK, {command, options, value}); + return value; + } catch (e) { + error = /** @type {execa.ExecaError & NodeJS.ErrnoException} */ (e); + if (error.code === 'ENOENT') { + throw new Error(`Could not find "node" at ${process.execPath}`); + } + this.emit(RUN_NPM_FAILED, error); + throw error; + } } /** @@ -280,12 +416,12 @@ class Smoker { * @returns {Promise} */ async install(packItems) { + this.emit(INSTALL_BEGIN, packItems); if (!packItems) { throw new TypeError('(install) "packItems" is required'); } if (packItems.length) { const extraArgs = this.#extraNpmInstallArgs; - const npmPath = await this.findNpm(); const cwd = await this.createWorkingDirectory(); const installArgs = [ 'install', @@ -293,82 +429,173 @@ class Smoker { ...packItems.map(({tarballFilepath}) => tarballFilepath), ]; - const proc = execa(npmPath, installArgs, { - cwd, - }); - - if (!this.#quiet) { - proc.stdout?.on('data', (data) => { - console.error(String(data)); + /** @type {execa.ExecaReturnValue} */ + let value; + try { + value = await this.#runNpm(installArgs, { + cwd, }); + } catch (err) { + this.emit(INSTALL_FAILED, /** @type {execa.ExecaError} */ (err)); + throw err; } - - const {exitCode: installExitCode} = await proc; - - if (installExitCode) { - throw new Error( - `"npm install" failed with exit code ${installExitCode}` + if (value.exitCode) { + debug('(install) Failed: %O', value); + const error = new Error( + `"npm install" failed with exit code ${value.exitCode}: ${value.stdout}` ); + this.emit(INSTALL_FAILED, error); + throw error; } + this.emit(INSTALL_OK, packItems); + debug('(install) Installed %d packages', packItems.length); - return; + } else { + debug('(install) No packed items; no packages to install'); } - - debug('(install) No packed items; no packages to install'); } /** * Runs the script for each package in `packItems` * @param {PackItem[]} packItems - * @returns {Promise} + * @returns {Promise} */ - async runScript(packItems) { + async runScripts(packItems) { if (!packItems) { throw new TypeError('(install) "packItems" is required'); } - if (packItems.length) { - const scripts = this.scripts; - const npmPath = await this.findNpm(); - - for (const script of scripts) { - const execArgs = ['run-script', script]; - - for await (const {installPath: cwd} of packItems) { - debug('(pack) Executing: %s %s', npmPath, execArgs.join(' ')); - const proc = execa(npmPath, execArgs, { - cwd, - }); - const pkgName = pathToPackageName(cwd); - - if (!this.#quiet) { - proc.stdout?.on('data', (data) => { - console.error(String(data)); - }); - } - const {exitCode, stderr} = await proc; - if (exitCode) { - if (/missing script:/i.test(stderr)) { - throw new Error( - `npm was unable to find script "${script}" in package "${pkgName}"` + const scripts = this.scripts; + const npmPath = await this.findNpm(); + const scriptCount = scripts.length; + const total = packItems.length * scriptCount; + this.emit(RUN_SCRIPTS_BEGIN, {scripts, packItems, total}); + /** @type {RunScriptResult[]} */ + const results = []; + + /** + * + * @param {string} pkgName + * @param {string} script + * @param {execa.ExecaReturnValue|execa.ExecaError} value + * @param {number} current + * @param {number} total + */ + const handleScriptReturnValue = ( + pkgName, + script, + value, + current, + total + ) => { + const result = { + pkgName, + script, + ...value, + }; + results.push(result); + if (value.failed && this.#bail) { + if (/missing script:/i.test(value.stderr)) { + this.emit(RUN_SCRIPT_FAILED, {error: value, current, total}); + return new Error( + `Script "${script}" in package "${pkgName}" failed; npm was unable to find this script` + ); + } + + return new Error( + `Script "${script}" in package "${pkgName}" failed with exit code ${value.exitCode}: ${value.all}` + ); + } else if (value.failed) { + this.emit(RUN_SCRIPT_FAILED, {error: value, current, total}); + debug( + `(runScripts) Script "%s" in package "%s" failed; continuing...`, + script, + pkgName + ); + } else { + this.emit(RUN_SCRIPT_OK, { + value, + current, + total, + }); + debug( + '(runScripts) Successfully executed script %s in package %s', + script, + pkgName + ); + } + }; + if (total) { + for (const [currentScriptIdx, script] of Object.entries(scripts)) { + const npmArgs = ['run-script', script]; + try { + for await (const [pkgIdx, {installPath: cwd}] of Object.entries( + packItems + )) { + const pkgName = pathToPackageName(cwd); + const current = Number(pkgIdx) + Number(currentScriptIdx); + this.emit(RUN_SCRIPT_BEGIN, { + script, + cwd, + npmArgs, + pkgName, + total, + current, + }); + debug('(pack) Executing: %s %s', npmPath, npmArgs.join(' ')); + + /** @type {execa.ExecaReturnValue} */ + let value; + + try { + value = await this.#runNpm(npmArgs, {cwd}); + } catch (err) { + throw handleScriptReturnValue( + pkgName, + script, + /** @type {execa.ExecaError} */ (err), + current, + total ); } - throw new Error( - `npm script "${script}" failed with exit code ${exitCode}: ${stderr}` - ); - } else { - debug( - '(runScript) Successfully executed script %s in package %s', + const err = handleScriptReturnValue( + pkgName, script, - pkgName + value, + current, + total ); + if (err) { + throw err; + } + } + } finally { + const failures = results.reduce( + (acc, {failed}) => acc + Number(failed), + 0 + ); + if (failures) { + this.emit(RUN_SCRIPTS_FAILED, { + total, + executed: results.length, + failures, + results, + }); + } else { + this.emit(RUN_SCRIPTS_OK, { + total, + executed: results.length, + failures, + results, + }); } } } - return; + } else { + debug('(runScripts) No packed items; no scripts to run'); } - debug('(runScript) No packed items; no scripts to run'); + return results; } /** @@ -397,4 +624,7 @@ exports.smoke = async function smoke(scripts, opts = {}) { * @typedef {import('../static').SmokerOptions} SmokerOptions * @typedef {import('../static').PackItem} PackItem * @typedef {import('../static').PackOptions} PackOptions + * @typedef {import('../static').RunScriptResult} RunScriptResult + * @typedef {import('../static').Events} Events + * @typedef {import('../static').TSmokerEmitter} TSmokerEmitter */ diff --git a/static.d.ts b/static.d.ts index 25484bf5..cde97d51 100644 --- a/static.d.ts +++ b/static.d.ts @@ -1,4 +1,6 @@ -import type {Smoker} from './src'; +import {ExecaError, ExecaReturnValue, Options} from 'execa'; +import {StrictEventEmitter} from 'strict-event-emitter-types'; +import {EventEmitter} from 'events'; /** * JSON output of `npm pack` @@ -36,7 +38,6 @@ export interface PackOptions { allWorkspaces?: boolean; includeWorkspaceRoot?: boolean; silent?: boolean; - } /** @@ -81,8 +82,48 @@ export interface SmokerOptions { */ npm?: string; /** - * If `true`, suppress output from `npm` + * If `true`, show output from `npm` + */ + verbose?: boolean; + /** + * If `true`, leave temp dir intact after exit */ - quiet?: boolean; - + linger?: boolean; + /** + * If `true`, halt at first failure + */ + bail?: boolean; +} + +export interface RunScriptResult extends ExecaReturnValue { + pkgName: string; + script: string; +} + +export interface Events { + SmokeBegin: void; + SmokeOk: void; + SmokeFailed: (err: Error) => void; + FindNpmBegin: void; + FindNpmOk: string; + FindNpmFailed: (err: Error) => void; + + PackBegin: void; + PackFailed: SyntaxError|ExecaError|Error; + PackOk: PackItem[]; + RunNpmBegin: {command: string; options: Options}; + RunNpmFailed: ExecaError + RunNpmOk: {command: string, options: Options, value: ExecaReturnValue}; + InstallBegin: PackItem[]; + InstallFailed: ExecaError|Error; + InstallOk: PackItem[]; + RunScriptsBegin: {scripts: string[], packItems: PackItem[], total: number} + RunScriptsFailed: {total: number, executed: number, failures: number, results: ExecaReturnValue[]}; + RunScriptsOk: {total: number, executed: number, failures: number, results: ExecaReturnValue[]}; + RunScriptBegin: {script: string, cwd: string, npmArgs: string[], pkgName: string, total: number, current: number}; + RunScriptFailed: {error: ExecaReturnValue|ExecaError; total: number, current: number} + RunScriptOk: {value: ExecaReturnValue, current: number, total: number} + } + +export type TSmokerEmitter = StrictEventEmitter; diff --git a/test/cli.spec.js b/test/cli.spec.js new file mode 100644 index 00000000..64d4d079 --- /dev/null +++ b/test/cli.spec.js @@ -0,0 +1,75 @@ +const {node: execa} = require('execa'); +const path = require('path'); +const expect = require('unexpected') + .clone() + .use(require('unexpected-snapshot')); + +/** @type {string} */ +let CLI_PATH; +/** @type {string} */ +let CWD; + +if (process.env.WALLABY_PROJECT_DIR) { + CLI_PATH = path.join(process.env.WALLABY_PROJECT_DIR, 'src', 'cli.js'); + CWD = process.env.WALLABY_PROJECT_DIR; +} else { + CLI_PATH = require.resolve('../src/cli.js'); + CWD = path.join(__dirname, '..'); +} + +/** + * @param {string[]} args + * @param {import('execa').NodeOptions} [opts] + */ +async function run(args, opts = {}) { + const {stdout, stderr, exitCode} = await execa(CLI_PATH, args, { + cwd: CWD, + ...opts, + env: opts.env ? opts.env : {DEBUG: ''}, + }); + return {stdout, stderr, exitCode}; +} + +describe('midnight-smoker CLI', function () { + describe('--version', function () { + it('should print version and exit', async function () { + this.timeout('5s'); + const actual = await run(['--version']); + expect(actual, 'to equal snapshot', { + stdout: '0.1.0', + stderr: '', + exitCode: 0, + }); + }); + }); + + it('should show help text', async function () { + this.timeout('5s'); + + const actual = await run(['test:smoke', '--help']); + expect(actual, 'to equal snapshot', { + stdout: + "smoker