diff --git a/package-lock.json b/package-lock.json index 1d08faba..ef3c6fab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@dojo/cli", - "version": "0.6.2", + "version": "0.6.3-pre", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -117,7 +117,7 @@ "dev": true, "requires": { "@types/express": "4.0.39", - "@types/node": "8.10.0" + "@types/node": "8.10.3" } }, "@types/chai": { @@ -132,7 +132,7 @@ "integrity": "sha512-F9OalGhk60p/DnACfa1SWtmVTMni0+w9t/qfb5Bu7CsurkEjZFN7Z+ii/VGmYpaViPz7o3tBahRQae9O7skFlQ==", "dev": true, "requires": { - "@types/node": "8.10.0" + "@types/node": "8.10.3" } }, "@types/configstore": { @@ -171,7 +171,7 @@ "integrity": "sha512-7dsoPp7r33mdPcxukSrs9WVQgI96fiqffC7K4XvXJnSrTtJov3x1CJUgid7msJ8yCbfkmXJoKJ3HXyQIwZD0NQ==", "dev": true, "requires": { - "@types/node": "8.10.0" + "@types/node": "8.10.3" } }, "@types/express": { @@ -192,7 +192,7 @@ "dev": true, "requires": { "@types/events": "1.2.0", - "@types/node": "8.10.0" + "@types/node": "8.10.3" } }, "@types/fs-extra": { @@ -201,7 +201,7 @@ "integrity": "sha512-h3wnflb+jMTipvbbZnClgA2BexrT4w0GcfoCz5qyxd0IRsbqhLSyesM6mqZTAnhbVmhyTm5tuxfRu9R+8l+lGw==", "dev": true, "requires": { - "@types/node": "8.10.0" + "@types/node": "8.10.3" } }, "@types/glob": { @@ -212,7 +212,7 @@ "requires": { "@types/events": "1.2.0", "@types/minimatch": "3.0.3", - "@types/node": "8.10.0" + "@types/node": "8.10.3" } }, "@types/globby": { @@ -230,7 +230,7 @@ "integrity": "sha512-fKrWJ+uFq9j3tP2RLm9cY7Z50LhhPnSHQCliCZP5lPAWC7TydnU+BcLR0KQIHe9Gbn1oGfkRIq3u56MNCC1qyw==", "dev": true, "requires": { - "@types/node": "8.10.0" + "@types/node": "8.10.3" } }, "@types/handlebars": { @@ -372,9 +372,9 @@ "dev": true }, "@types/node": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.0.tgz", - "integrity": "sha512-7IGHZQfRfa0bCd7zUBVUGFKFn31SpaLDFfNoCAqkTGQO5JlHC9BwQA/CG9KZlABFxIUtXznyFgechjPQEGrUTg==", + "version": "8.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.3.tgz", + "integrity": "sha512-vjiRZkhKEyZndtFOz/FtIp0CqPbgOOki8o9IcPOLTqlzcnvFLToYdERshLaI6TCz7pDWoKlmvgftqB4xlltn9g==", "dev": true }, "@types/platform": { @@ -389,7 +389,7 @@ "integrity": "sha1-m1htZalH3qiMS8JNoLkF/pUgoNU=", "dev": true, "requires": { - "@types/node": "8.10.0" + "@types/node": "8.10.3" } }, "@types/rx": { @@ -540,13 +540,13 @@ "integrity": "sha1-32E73biCJe0JzlyDX2INyq8VXms=", "dev": true, "requires": { - "@types/node": "8.10.0" + "@types/node": "8.10.3" } }, "@types/sinon": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.0.tgz", - "integrity": "sha512-rvgY5bK5ZBRJPuJF0vJI+NC2gt+lakobTa8pnDS/oRH2gk/tooeDEel8piZA8Ng6pxq0A5QGzilIFSyashP6jw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.1.tgz", + "integrity": "sha512-DK4YtH30I67k4klURIBS4VAe1aBISfS9lgNlHFkibSmKem2tLQc5VkKoJreT3dCJAd+xRyCS8bx1o97iq3yUVg==", "dev": true }, "@types/source-map": { @@ -567,7 +567,7 @@ "integrity": "sha512-9a7C5VHh+1BKblaYiq+7Tfc+EOmjMdZaD1MYtkQjSoxgB69tBjW98ry6SKsi4zEIWztLOMRuL87A3bdT/Fc/4w==", "dev": true, "requires": { - "@types/node": "8.10.0" + "@types/node": "8.10.3" } }, "@types/update-notifier": { @@ -582,7 +582,7 @@ "integrity": "sha512-+30f9gcx24GZRD9EqqiQM+I5pRf/MJiJoEqp2X62QRwfEjdqyn9mPmjxZAEXBUVunWotE5qkadIPqf2MMcDYNw==", "dev": true, "requires": { - "@types/node": "8.10.0" + "@types/node": "8.10.3" } }, "@types/yargs": { @@ -868,7 +868,7 @@ "dev": true, "requires": { "browserslist": "1.7.7", - "caniuse-db": "1.0.30000820", + "caniuse-db": "1.0.30000824", "normalize-range": "0.1.2", "num2fraction": "1.2.2", "postcss": "5.2.18", @@ -977,14 +977,14 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "2.5.3", + "core-js": "2.5.4", "regenerator-runtime": "0.11.1" }, "dependencies": { "core-js": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", - "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.4.tgz", + "integrity": "sha1-8si/GB8qgLkvNgEhQpzmOi8K6uA=", "dev": true } } @@ -1207,8 +1207,8 @@ "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", "dev": true, "requires": { - "caniuse-db": "1.0.30000820", - "electron-to-chromium": "1.3.40" + "caniuse-db": "1.0.30000824", + "electron-to-chromium": "1.3.42" } }, "buffer": { @@ -1275,15 +1275,15 @@ "dev": true, "requires": { "browserslist": "1.7.7", - "caniuse-db": "1.0.30000820", + "caniuse-db": "1.0.30000824", "lodash.memoize": "4.1.2", "lodash.uniq": "4.5.0" } }, "caniuse-db": { - "version": "1.0.30000820", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000820.tgz", - "integrity": "sha1-fCDiXOoXaLJhtyT4LjpqJTqqFGg=", + "version": "1.0.30000824", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000824.tgz", + "integrity": "sha1-u6P/QlKW4EyqN/5CYlkganBWVRs=", "dev": true }, "capture-stack-trace": { @@ -1469,6 +1469,12 @@ "number-is-nan": "1.0.1" } }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -1621,7 +1627,7 @@ "requires": { "buffer-from": "1.0.0", "inherits": "2.0.3", - "readable-stream": "2.3.5", + "readable-stream": "2.3.6", "typedarray": "0.0.6" } }, @@ -2266,9 +2272,9 @@ "integrity": "sha512-QIDZL54fyV8MDcAsO91BMH1ft2qGGaHIJsJIA/+t+7uvXol1dm413fPcUgUb4k8F/9457rx4/KFE4XfDifrQxQ==" }, "electron-to-chromium": { - "version": "1.3.40", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.40.tgz", - "integrity": "sha1-H71tl779crim+SHcONIkE9L2/d8=", + "version": "1.3.42", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.42.tgz", + "integrity": "sha1-lcM78B0MxAVVauyJn+Yf1NduoPk=", "dev": true }, "elegant-spinner": { @@ -2464,6 +2470,12 @@ "integrity": "sha512-fjVFjW9yhqMhVGwRExCXLhJKrLlkYSaxNWdyc9rmHlrVZbk35YHH312dFd7191uQeXkI3mKLZTIbSvIeFwFemg==", "dev": true }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", + "dev": true + }, "statuses": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", @@ -2487,12 +2499,12 @@ } }, "external-editor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.1.0.tgz", - "integrity": "sha512-E44iT5QVOUJBKij4IIV3uvxuNlbKS38Tw1HiupxEIHPv9qtC2PrDYohbXV5U+1jnfIXttny8gUhj+oZvflFlzA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", "requires": { "chardet": "0.4.2", - "iconv-lite": "0.4.19", + "iconv-lite": "0.4.21", "tmp": "0.0.33" } }, @@ -2946,7 +2958,7 @@ "grunt-known-options": "1.1.0", "grunt-legacy-log": "1.0.1", "grunt-legacy-util": "1.0.0", - "iconv-lite": "0.4.19", + "iconv-lite": "0.4.21", "js-yaml": "3.5.5", "minimatch": "3.0.4", "nopt": "3.0.6", @@ -3723,7 +3735,7 @@ "dev": true, "requires": { "inherits": "2.0.3", - "statuses": "1.4.0" + "statuses": "1.5.0" } }, "http-parser-js": { @@ -3790,9 +3802,12 @@ } }, "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", + "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "requires": { + "safer-buffer": "2.1.2" + } }, "icss-replace-symbols": { "version": "1.1.0", @@ -3865,7 +3880,7 @@ "chalk": "2.3.2", "cli-cursor": "2.1.0", "cli-width": "2.2.0", - "external-editor": "2.1.0", + "external-editor": "2.2.0", "figures": "2.0.0", "lodash": "4.17.5", "mute-stream": "0.0.7", @@ -3916,7 +3931,7 @@ "diff": "3.2.0", "express": "4.15.5", "glob": "7.1.2", - "http-errors": "1.6.2", + "http-errors": "1.6.3", "istanbul-lib-coverage": "1.1.2", "istanbul-lib-hook": "1.0.7", "istanbul-lib-instrument": "1.7.5", @@ -3945,7 +3960,7 @@ "content-type": "1.0.4", "debug": "2.6.7", "depd": "1.1.2", - "http-errors": "1.6.2", + "http-errors": "1.6.3", "iconv-lite": "0.4.15", "on-finished": "2.3.0", "qs": "6.4.0", @@ -3975,21 +3990,21 @@ "dev": true }, "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "dev": true, "requires": { - "depd": "1.1.1", + "depd": "1.1.2", "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": "1.3.1" + "setprototypeof": "1.1.0", + "statuses": "1.5.0" }, "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true } } @@ -4787,7 +4802,7 @@ "log-update": "1.0.2", "ora": "0.2.3", "p-map": "1.2.0", - "rxjs": "5.5.7", + "rxjs": "5.5.8", "stream-to-observable": "0.2.0", "strip-ansi": "3.0.1" }, @@ -7283,16 +7298,16 @@ } }, "readable-stream": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", - "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "1.0.2", "inherits": "2.0.3", "isarray": "1.0.0", "process-nextick-args": "2.0.0", "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", + "string_decoder": "1.1.1", "util-deprecate": "1.0.2" } }, @@ -7683,9 +7698,9 @@ } }, "rxjs": { - "version": "5.5.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.7.tgz", - "integrity": "sha512-Hxo2ac8gRQjwjtKgukMIwBRbq5+KAeEV5hXM4obYBOAghev41bDQWgFH4svYiU9UnQ5kNww2LgfyBdevCd2HXA==", + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.8.tgz", + "integrity": "sha512-Bz7qou7VAIoGiglJZbzbXa4vpX5BmTTN2Dj/se6+SwADtw4SihqBIiEa7VmTXJ8pynvq0iFr5Gx9VLyye1rIxQ==", "dev": true, "requires": { "symbol-observable": "1.0.1" @@ -7704,6 +7719,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "samsam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", @@ -7751,7 +7771,7 @@ "escape-html": "1.0.3", "etag": "1.8.1", "fresh": "0.5.2", - "http-errors": "1.6.2", + "http-errors": "1.6.3", "mime": "1.3.4", "ms": "2.0.0", "on-finished": "2.3.0", @@ -7760,21 +7780,21 @@ }, "dependencies": { "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "dev": true, "requires": { - "depd": "1.1.1", + "depd": "1.1.2", "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": "1.3.1" + "setprototypeof": "1.1.0", + "statuses": "1.5.0" }, "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true } } @@ -7817,9 +7837,9 @@ "dev": true }, "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", "dev": true }, "shebang-command": { @@ -7870,9 +7890,9 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "sinon": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.4.8.tgz", - "integrity": "sha512-EWZf/D5BN/BbDFPmwY2abw6wgELVmk361self+lcwEmVw0WWUxURp2S/YoDB2WG/xurFVzKQglMARweYRWM6Hw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", + "integrity": "sha512-trdx+mB0VBBgoYucy6a9L7/jfQOmvGeaKZT4OOJ+lPAtI8623xyGr8wLiE4eojzBS8G9yXbhx42GHUOVLr4X2w==", "dev": true, "requires": { "@sinonjs/formatio": "2.0.0", @@ -7893,10 +7913,12 @@ } }, "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "requires": { + "is-fullwidth-code-point": "2.0.0" + } }, "slide": { "version": "1.1.6", @@ -7982,9 +8004,9 @@ "dev": true }, "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, "stream-combiner": { @@ -8032,9 +8054,9 @@ } }, "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "5.1.1" } @@ -8163,7 +8185,7 @@ "requires": { "bl": "1.2.2", "end-of-stream": "1.4.1", - "readable-stream": "2.3.5", + "readable-stream": "2.3.6", "xtend": "4.0.1" }, "dependencies": { @@ -8173,7 +8195,7 @@ "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "dev": true, "requires": { - "readable-stream": "2.3.5", + "readable-stream": "2.3.6", "safe-buffer": "5.1.1" } } @@ -8375,7 +8397,7 @@ "findup-sync": "0.3.0", "glob": "7.1.2", "optimist": "0.6.1", - "resolve": "1.6.0", + "resolve": "1.7.0", "semver": "5.5.0", "tslib": "1.8.1", "tsutils": "1.9.1" @@ -8388,9 +8410,9 @@ "dev": true }, "resolve": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.6.0.tgz", - "integrity": "sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.0.tgz", + "integrity": "sha512-QdgZ5bjR1WAlpLaO5yHepFvC+o3rCr6wpfE2tpJNMkXdulf2jKomQBdNRQITF3ZKHNlT71syG98yQP03gasgnA==", "dev": true, "requires": { "path-parse": "1.0.5" @@ -9497,7 +9519,7 @@ "requires": { "graceful-fs": "4.1.11", "minimatch": "3.0.4", - "readable-stream": "2.3.5", + "readable-stream": "2.3.6", "set-immediate-shim": "1.0.1" } }, @@ -9578,7 +9600,7 @@ "integrity": "sha1-qHGcQXsIDAEtNJeyjiKKwJdF/fI=", "dev": true, "requires": { - "@types/node": "8.10.0" + "@types/node": "8.10.3" } }, "@types/minimatch": { @@ -9687,15 +9709,15 @@ "thenify": "3.3.0", "throat": "3.2.0", "touch": "1.0.0", - "typescript": "2.7.2", + "typescript": "2.8.1", "xtend": "4.0.1", "zip-object": "0.1.0" }, "dependencies": { "typescript": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.2.tgz", - "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.8.1.tgz", + "integrity": "sha512-Ao/f6d/4EPLq0YwzsQz8iXflezpTkQzqAyenTiw4kCUGr1uPiFLC3+fZ+gMZz6eeI/qdRUqvC+HxIJzUAzEFdg==" } } }, diff --git a/package.json b/package.json index 74b0efdc..f28a8eab 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,8 @@ "globby": "^6.0.0", "inquirer": "^4.0.2", "pkg-dir": "^2.0.0", + "slice-ansi": "^1.0.0", + "string-width": "^2.1.1", "tslib": "~1.8.1", "typings-core": "^2.3.3", "update-notifier": "^2.3.0", @@ -75,10 +77,10 @@ "grunt-tslint": "5.0.1", "husky": "0.14.3", "intern": "~4.1.0", - "mockery": "^2.1.0", - "sinon": "^4.1.3", "lint-staged": "6.0.0", + "mockery": "^2.1.0", "prettier": "1.9.2", + "sinon": "^4.1.3", "tslint": "5.2.0", "typescript": "~2.6.1" }, diff --git a/src/CommandHelper.ts b/src/CommandHelper.ts index d99eac41..17673aa5 100644 --- a/src/CommandHelper.ts +++ b/src/CommandHelper.ts @@ -2,12 +2,8 @@ import * as yargs from 'yargs'; import { ConfigurationHelperFactory } from './configurationHelper'; import HelperFactory from './Helper'; import template from './template'; -import { CommandHelper, Command, CommandsMap } from './interfaces'; - -function getCommand(commandsMap: CommandsMap, group: string, commandName?: string): Command | undefined { - const commandKey = commandName ? `${group}-${commandName}` : group; - return commandsMap.get(commandKey); -} +import { CommandHelper, GroupMap } from './interfaces'; +import { getCommand } from './command'; export type RenderFilesConfig = { src: string; @@ -19,12 +15,12 @@ export type RenderFilesConfig = { * allowing commands to call one another. Provides 'run' and 'exists' functions */ export class SingleCommandHelper implements CommandHelper { - private _commandsMap: CommandsMap; + private _groupMap: GroupMap; private _configurationFactory: ConfigurationHelperFactory; private _context: any; - constructor(commandsMap: CommandsMap, context: any, configurationHelperFactory: ConfigurationHelperFactory) { - this._commandsMap = commandsMap; + constructor(commandsMap: GroupMap, context: any, configurationHelperFactory: ConfigurationHelperFactory) { + this._groupMap = commandsMap; this._context = context; this._configurationFactory = configurationHelperFactory; } @@ -36,18 +32,25 @@ export class SingleCommandHelper implements CommandHelper { } run(group: string, commandName?: string, args?: yargs.Argv): Promise { - const command = getCommand(this._commandsMap, group, commandName); - if (command) { - const helper = new HelperFactory(this, yargs, this._context, this._configurationFactory); - return command.run(helper.sandbox(group, command.name), args); - } else { + try { + const command = getCommand(this._groupMap, group, commandName); + if (command) { + const helper = new HelperFactory(this, yargs, this._context, this._configurationFactory); + return command.run(helper.sandbox(group, command.name), args); + } else { + return Promise.reject(new Error('The command does not exist')); + } + } catch { return Promise.reject(new Error('The command does not exist')); } } exists(group: string, commandName?: string) { - const command = getCommand(this._commandsMap, group, commandName); - return !!command; + try { + return !!getCommand(this._groupMap, group, commandName); + } catch { + return false; + } } } diff --git a/src/allCommands.ts b/src/allCommands.ts index 0489af8d..27a278af 100644 --- a/src/allCommands.ts +++ b/src/allCommands.ts @@ -1,46 +1,23 @@ import { loadCommands, enumerateInstalledCommands, enumerateBuiltInCommands } from './loadCommands'; -import { LoadedCommands } from './interfaces'; +import { GroupMap } from './interfaces'; import { initCommandLoader, createBuiltInCommandLoader } from './command'; import config from './config'; -const commands: LoadedCommands = { - commandsMap: new Map(), - yargsCommandNames: new Map() -}; - -let loaded = false; - -export function reset(): void { - commands.commandsMap = new Map(); - commands.yargsCommandNames = new Map(); - loaded = false; -} - -export async function loadExternalCommands(): Promise { +export async function loadExternalCommands(): Promise { const installedCommandLoader = initCommandLoader(config.searchPrefixes); const installedCommandsPaths = await enumerateInstalledCommands(config); return await loadCommands(installedCommandsPaths, installedCommandLoader); } -export async function loadBuiltInCommands(): Promise { +export async function loadBuiltInCommands(): Promise { const builtInCommandLoader = createBuiltInCommandLoader(); const builtInCommandsPaths = await enumerateBuiltInCommands(config); return await loadCommands(builtInCommandsPaths, builtInCommandLoader); } -export default async function loadAllCommands(): Promise { - if (loaded) { - return Promise.resolve(commands); - } - +export default async function loadAllCommands(): Promise { const builtInCommands = await loadBuiltInCommands(); const installedCommands = await loadExternalCommands(); - commands.commandsMap = new Map([...installedCommands.commandsMap, ...builtInCommands.commandsMap]); - commands.yargsCommandNames = new Map([ - ...installedCommands.yargsCommandNames, - ...builtInCommands.yargsCommandNames - ]); - loaded = true; - return Promise.resolve(commands); + return Promise.resolve(new Map([...installedCommands, ...builtInCommands])); } diff --git a/src/command.ts b/src/command.ts index 68165171..47f84fc9 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,5 +1,4 @@ -import { Command, CommandWrapper, CommandsMap } from './interfaces'; -const cliui = require('cliui'); +import { Command, CommandWrapper, GroupMap } from './interfaces'; /** * Function to create a loader instance, this allows the config to be injected @@ -16,7 +15,7 @@ export function initCommandLoader(searchPrefixes: string[]): (path: string) => C try { const command = convertModuleToCommand(module); - const { description, register, run, alias, eject } = command; + const { description, register, run, alias, eject, global = false } = command; // derive the group and name from the module directory name, e.g. dojo-cli-group-name const [, group, name] = commandRegExp.exec(path); @@ -26,6 +25,8 @@ export function initCommandLoader(searchPrefixes: string[]): (path: string) => C alias, description, register, + installed: true, + global: group === 'create' && name === 'app' ? true : global, run, path, eject @@ -46,12 +47,14 @@ export function createBuiltInCommandLoader(): (path: string) => CommandWrapper { try { const command = convertModuleToCommand(module); // derive the name and group of the built in commands from the command itself (these are optional props) - const { name = '', group = '', alias, description, register, run } = command; + const { name = '', group = '', alias, description, register, run, global = false } = command; return { name, group, alias, + global, + installed: true, description, register, run, @@ -75,24 +78,19 @@ export function convertModuleToCommand(module: any): Command { } } -export function getGroupDescription(commandNames: Set, commands: CommandsMap): string { - const numCommands = commandNames.size; - if (numCommands > 1) { - return getMultiCommandDescription(commandNames, commands); - } else { - const { description } = commands.get(Array.from(commandNames.keys())[0]); - return description; +export function getCommand(groupMap: GroupMap, groupName: string, commandName?: string) { + const commandMap = groupMap.get(groupName); + if (!commandMap) { + throw new Error(`Unable to find command group: ${groupName}`); } -} - -function getMultiCommandDescription(commandNames: Set, commands: CommandsMap): string { - const descriptions = Array.from(commandNames.keys(), (commandName) => { - const { name, description } = commands.get(commandName); - return `${name} \t${description}`; - }); - const ui = cliui({ - width: 80 - }); - ui.div(descriptions.join('\n')); - return ui.toString(); + if (commandName) { + const command = commandMap.get(commandName); + if (!command) { + throw new Error(`Unable to find command: ${commandName} for group: ${groupName}`); + } + return command; + } + return [...commandMap.values()].find((wrapper) => { + return !!wrapper.default; + })!; } diff --git a/src/commands/eject.ts b/src/commands/eject.ts index e934313e..1c2de86c 100644 --- a/src/commands/eject.ts +++ b/src/commands/eject.ts @@ -52,18 +52,20 @@ async function run(helper: Helper, args: EjectArgs): Promise { throw Error('Aborting eject'); } return loadExternalCommands().then(async (commands) => { + let toEject = new Set(); + commands.forEach((commandMap, group) => { + toEject = [...commandMap.values()].reduce((toEject, command) => { + if (isEjectableCommandWrapper(command)) { + toEject.add(command); + } + return toEject; + }, new Set()); + }); const npmPackages: NpmPackage = { dependencies: {}, devDependencies: {} }; - const toEject = [...commands.commandsMap.values()].reduce((toEject, command) => { - if (isEjectableCommandWrapper(command)) { - toEject.add(command); - } - return toEject; - }, new Set()); - if (toEject.size) { const allHints: string[] = []; [...toEject].forEach((command) => { @@ -106,5 +108,6 @@ export default { group: 'eject', description: 'disconnect your project from dojo cli commands', register, + global: false, run }; diff --git a/src/commands/init.ts b/src/commands/init.ts index 01482ce0..5f0577a7 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -19,14 +19,16 @@ async function run(helper: Helper, args: {}) { json = JSON.parse(file); } - const { commandsMap } = await loadExternalCommands(); + const groupMap = await loadExternalCommands(); const values = []; - for (let [, value] of commandsMap.entries()) { - const name = `${value.group}-${value.name}`; - if (values.indexOf(value) === -1 && json[name] === undefined) { - json[name] = {}; - values.push(value); + for (let [, commandMap] of groupMap.entries()) { + for (let [, value] of commandMap.entries()) { + const name = `${value.group}-${value.name}`; + if (values.indexOf(name) === -1 && json[name] === undefined) { + json[name] = {}; + values.push(name); + } } } @@ -39,5 +41,6 @@ export default { group: 'init', description: 'create a .dojorc file', run, + global: false, register: () => {} }; diff --git a/src/commands/version.ts b/src/commands/version.ts index f58e57cc..c93f3b1c 100644 --- a/src/commands/version.ts +++ b/src/commands/version.ts @@ -1,4 +1,4 @@ -import { Helper, OptionsHelper, CommandsMap, NpmPackageDetails } from '../interfaces'; +import { Helper, OptionsHelper, GroupMap, NpmPackageDetails } from '../interfaces'; import { join } from 'path'; import { Argv } from 'yargs'; import chalk from 'chalk'; @@ -154,7 +154,7 @@ function readPackageDetails(packageDir: string): PackageDetails { * @param {CommandsMap} commandsMap * @returns {{name, version, group}[]} */ -function buildVersions(commandsMap: CommandsMap): ModuleVersion[] { +function buildVersions(groupMap: GroupMap): ModuleVersion[] { /* * commandsMap comes in as a map of [command-name, command]. The command has a default command, * the map will actually contain two entries for one command, on for the default command, one for the real, @@ -163,11 +163,13 @@ function buildVersions(commandsMap: CommandsMap): ModuleVersion[] { * Loop over commandsMap and create a new map with one entry per command, then loop over each entry and extract * the package details. */ - const consolidatedCommands = [ - ...new Map( - [...commandsMap].map(([, command]) => <[string, string]>[command.path, command.group]) - ) - ]; + + const consolidatedCommands = []; + for (let [, commandMap] of groupMap.entries()) { + for (let [, value] of commandMap.entries()) { + consolidatedCommands.push([value.path, value.group]); + } + } const versionInfo = consolidatedCommands .map(([path, group]) => { @@ -195,10 +197,10 @@ function buildVersions(commandsMap: CommandsMap): ModuleVersion[] { * @param {boolean} checkOutdated should we check if there is a later stable version available for the command * @returns {string} the stdout output */ -function createVersionsString(commandsMap: CommandsMap, checkOutdated: boolean): Promise { +function createVersionsString(groupMap: GroupMap, checkOutdated: boolean): Promise { const packagePath = pkgDir.sync(__dirname); const myPackageDetails = readPackageDetails(packagePath); // fetch the cli's package details - const versions: ModuleVersion[] = buildVersions(commandsMap); + const versions: ModuleVersion[] = buildVersions(groupMap); if (checkOutdated) { return areCommandsOutdated(versions).then( (commandVersions: ModuleVersion[]) => createOutput(myPackageDetails, commandVersions), @@ -213,8 +215,8 @@ function createVersionsString(commandsMap: CommandsMap, checkOutdated: boolean): function run(helper: Helper, args: VersionArgs): Promise { return allCommands() - .then((commands) => { - return createVersionsString(commands.commandsMap, args.outdated); + .then((groupMap) => { + return createVersionsString(groupMap, args.outdated); }) .then(console.log); } @@ -224,5 +226,7 @@ export default { group: 'version', description: 'provides version information for all installed commands and the cli itself', register, + global: true, + installed: true, run }; diff --git a/src/help.ts b/src/help.ts new file mode 100644 index 00000000..0571eb6f --- /dev/null +++ b/src/help.ts @@ -0,0 +1,216 @@ +import { GroupMap, CommandWrapper } from './interfaces'; +import chalk from 'chalk'; +import { isRequiredOption } from './validation'; +import { Options } from 'yargs'; + +const stringWidth = require('string-width'); +const sliceAnsi = require('slice-ansi'); + +const dojoArt = ` + .. + ';,,'.. + .colllc,. .''''''... .,,,. .. .,,,. + ...',:loooc. ...''''''.. ;x' ;;' ':;,'.'',:;. :o. .:;,'..',;:. + .'''. .... .',;'.. ..'. 'l' .c: :c. ,l' ,l. ,l' 'l; + .,,,. ..;::,'.. ';. 'l' .l, ,l. :c. ,l. .l, 'l. + .'..;;,. .,:cc;'... ,:' 'l' .l; :l. ,l. ,l. 'l. .l, + ...;;;. .':clc;. .;:. 'l' ,l. 'l' c: ,l. .l; ;l. + .:c:,..',:coooc,. .';;' 'l' .;c' 'c,. .c:. ,l. .c:. .;c. + .:okOOkkxo:,.. ........',,,'. ,o:,,,,,,;'. ;;,,,,;; ,l. ';;,,,,;;' + .''',;;,.. .... ........ ,l. + ', .''. .c: + .'.. .;c;. .. + .. .`; + +function addOptionPrefix(optionKey: string): string { + return stringWidth(optionKey) === 1 ? `-${optionKey}` : `--${optionKey}`; +} + +function getOptionDescription(options: Options): string | undefined { + if (options.describe) { + return options.describe; + } + if (options.description) { + return options.description; + } + if (options.desc) { + return options.desc; + } + if (options.defaultDescription) { + return options.defaultDescription; + } + return undefined; +} + +function createPadding(text: string, paddingLength: number, paddingChar = ' '): string { + return sliceAnsi(paddingChar.repeat(paddingLength), 0, paddingLength - stringWidth(text)); +} + +function formatHeader(group: string = '', command: string = '[]') { + return `${dojoArt} +${chalk.bold('Usage:')} + + $ ${chalk.green('dojo')} ${chalk.green(group)} ${chalk.dim.green(command)} [] [--help]`; +} + +function capitalize(value: string) { + return `${value.charAt(0).toUpperCase()}${value.slice(1)}`; +} + +function isGlobalCommand(commandWrapper: CommandWrapper): boolean { + return commandWrapper.installed && commandWrapper.global; +} + +function isProjectCommand(commandWrapper: CommandWrapper): boolean { + return commandWrapper.installed && !commandWrapper.global; +} + +function isCommandForGroup(group: string) { + return (commandWrapper: CommandWrapper) => { + return commandWrapper.group === group; + }; +} + +function isNpmCommand(commandWrapper: CommandWrapper): boolean { + return !commandWrapper.installed; +} + +function formatHelpOutput( + groupMap: GroupMap, + commandPredicate: (commandWrapper: CommandWrapper) => boolean, + showDefault = false +) { + let output = ''; + let hasGroup = false; + let commandOptionHelp = ''; + groupMap.forEach((commandMap, group) => { + let groupOutput = ` ${chalk.green(group)} ${createPadding(group, 8)}`; + if (hasGroup) { + groupOutput = `\n${groupOutput}`; + } + + let hasCommand = false; + const filteredCommandMap = [...commandMap.values()].filter(commandPredicate); + filteredCommandMap.forEach((commandWrapper) => { + const { name, description, default: isDefault } = commandWrapper; + if (hasCommand) { + groupOutput = `${groupOutput}\n${' '.repeat(11)}`; + } + groupOutput = `${groupOutput} ${chalk.dim.green(name)}`; + groupOutput = `${groupOutput}${createPadding(name, 10)}`; + groupOutput = `${groupOutput} ${capitalize(description)}`; + if (isDefault && showDefault && filteredCommandMap.length > 1) { + groupOutput = `${groupOutput} (Default)`; + } + hasCommand = true; + if (isDefault && showDefault) { + commandOptionHelp = `\n${formatCommandOptions(commandWrapper)}`; + } + }); + if (hasCommand) { + output = `${output}${groupOutput}`; + hasGroup = true; + } + }); + + if (commandOptionHelp) { + output = `${output}\n${commandOptionHelp}`; + } + return output; +} + +function formatCommandOptions(commandWrapper: CommandWrapper, isDefaultCommand = true) { + const { register } = commandWrapper; + let commandOptionHelp = `${chalk.bold(`Command Options:`)}\n`; + if (isDefaultCommand) { + commandOptionHelp = `${chalk.bold('Default Command Options')}\n`; + } + + if (!commandWrapper.installed) { + return `${commandOptionHelp}\n To install this command run ${chalk.green(`${commandWrapper.path}`)}`; + } + + register( + (key, options) => { + let optionKeys = `${addOptionPrefix(chalk.green(key))}`; + if (options.alias) { + const aliases = Array.isArray(options.alias) ? options.alias : [options.alias]; + optionKeys = aliases.reduce((result, alias) => { + if (alias.length === 1) { + return `${addOptionPrefix(chalk.green(alias))}, ${result}`; + } + return `${result}, ${addOptionPrefix(chalk.green(alias))}`; + }, optionKeys); + } + commandOptionHelp = `${commandOptionHelp}\n ${optionKeys}`; + commandOptionHelp = `${commandOptionHelp} ${createPadding(optionKeys, 20)}`; + const description = getOptionDescription(options); + if (description) { + commandOptionHelp = `${commandOptionHelp}${capitalize(description)}`; + } + if (options.choices) { + commandOptionHelp = `${commandOptionHelp} [choices: "${options.choices + .map((choice: any) => chalk.yellow(choice)) + .join('", "')}"]`; + } + if (options.default) { + commandOptionHelp = `${commandOptionHelp} [default: "${chalk.yellow(options.default)}"]`; + } + if (options.type) { + commandOptionHelp = `${commandOptionHelp} [type: "${chalk.yellow(options.type)}"]`; + } + if (isRequiredOption(options)) { + commandOptionHelp = `${commandOptionHelp} [${chalk.yellow('required')}]`; + } + }, + null as any + ); + return commandOptionHelp; +} + +function formatMainHelp(groupMap: GroupMap) { + return `${formatHeader()} + +${chalk.bold('Global Commands:')} + +${formatHelpOutput(groupMap, isGlobalCommand)} + +${chalk.bold('Project Commands:')} + +${formatHelpOutput(groupMap, isProjectCommand)} + +${chalk.bold('Installable Commands:')} + +${formatHelpOutput(groupMap, isNpmCommand)} +`; +} + +function formatGroupHelp(groupMap: GroupMap, group: string) { + return `${formatHeader(group)} + +${chalk.bold('Commands:')} + +${formatHelpOutput(groupMap, isCommandForGroup(group), true)} +`; +} + +function formatCommandHelp(groupMap: GroupMap, group: string, command: string) { + const commandWrapper = groupMap.get(group)!.get(command)!; + return `${formatHeader(group, command)} + +${chalk.bold('Description:')} + +${' '}${capitalize(commandWrapper.description)} + +${formatCommandOptions(commandWrapper, false)} +`; +} + +export function formatHelp(argv: any, groupMap: GroupMap): string { + if (!argv._ || argv._.length === 0) { + return formatMainHelp(groupMap); + } else if (argv._.length === 1) { + return formatGroupHelp(groupMap, argv._[0]); + } + return formatCommandHelp(groupMap, argv._[0], argv._[1]); +} diff --git a/src/index.ts b/src/index.ts index 5a282326..04126aa2 100755 --- a/src/index.ts +++ b/src/index.ts @@ -8,12 +8,6 @@ import commandLoader from './allCommands'; import installableCommands, { mergeInstalledCommandsWithAvailableCommands } from './installableCommands'; const pkgDir = require('pkg-dir'); -/** - * Runs the CLI - * - Sets up the update notifier to check for updates of the cli - * - Loads commands - * - Registers commands and subcommands using yargs (which runs the specified command) - */ async function init() { try { const packagePath = pkgDir.sync(__dirname); @@ -26,7 +20,7 @@ async function init() { const allCommands = await commandLoader(); const mergedCommands = mergeInstalledCommandsWithAvailableCommands(allCommands, availableCommands); - registerCommands(yargs, mergedCommands.commandsMap, mergedCommands.yargsCommandNames); + registerCommands(yargs, mergedCommands); } catch (err) { console.log(`Commands are not available: ${err}`); } diff --git a/src/installableCommands.ts b/src/installableCommands.ts index 8e400764..797de35c 100644 --- a/src/installableCommands.ts +++ b/src/installableCommands.ts @@ -1,9 +1,9 @@ import * as execa from 'execa'; import { join } from 'path'; const spawn: any = require('cross-spawn'); -import { NpmPackageDetails, LoadedCommands, YargsCommandNames, CommandsMap, CommandWrapper } from './interfaces'; +import { NpmPackageDetails, CommandWrapper, GroupMap } from './interfaces'; import * as Configstore from 'configstore'; -import { isEjected, setDefaultGroup } from './loadCommands'; +import { isEjected } from './loadCommands'; import chalk from 'chalk'; const INITIAL_TIMEOUT = 3000; @@ -53,47 +53,22 @@ async function search(timeout: number = 0): Promise { - const installedGroup = installedCommands.yargsCommandNames.get(groupName) || []; - const installableGroup = installableCommandPrompts.yargsCommandNames.get(groupName) || []; - const mergedSets = new Set([...installableGroup, ...installedGroup]); - mergedCommandNames.set(groupName, mergedSets); - return mergedCommandNames; - }, - new Map() as YargsCommandNames - ); - - return { - commandsMap: new Map([...installableCommandPrompts.commandsMap, ...installedCommands.commandsMap]), - yargsCommandNames: mergedYargsCommandNames - }; -} - -export function createInstallableCommandPrompts(availableCommands: NpmPackageDetails[]): LoadedCommands { - const commandsMap: CommandsMap = new Map(); - const yargsCommandNames: YargsCommandNames = new Map(); +): GroupMap { const regEx = /@dojo\/cli-([^-]+)-(.+)/; availableCommands.forEach((command) => { const [, group, name] = regEx.exec(command.name) as string[]; - const compositeKey = `${group}-${name}`; const installCommand = `npm i ${command.name}`; + const commandWrapper: CommandWrapper = { name, group, path: installCommand, description: command.description, + global: false, + installed: false, register: () => {}, run: () => { console.log(`\nTo install this command run ${chalk.green(installCommand)}\n`); @@ -102,24 +77,17 @@ export function createInstallableCommandPrompts(availableCommands: NpmPackageDet }; if (!isEjected(group, name)) { - if (!commandsMap.has(group)) { - setDefaultGroup(commandsMap, group, commandWrapper); - yargsCommandNames.set(group, new Set()); - } - - if (!commandsMap.has(compositeKey)) { - commandsMap.set(compositeKey, commandWrapper); + if (!groupMap.has(group)) { + commandWrapper.default = true; + groupMap.set(group, new Map()); } - const groupCommandNames = yargsCommandNames.get(group); - if (groupCommandNames) { - groupCommandNames.add(compositeKey); + const subCommandsMap = groupMap.get(group)!; + if (!subCommandsMap.has(name)) { + subCommandsMap.set(name, commandWrapper); } } }); - return { - commandsMap, - yargsCommandNames - }; + return groupMap; } diff --git a/src/interfaces.d.ts b/src/interfaces.d.ts index 87ca909a..715b4e2a 100644 --- a/src/interfaces.d.ts +++ b/src/interfaces.d.ts @@ -87,6 +87,7 @@ export interface Command { name?: string; group?: string; alias?: Alias[] | Alias; + global?: boolean; } export interface CommandError { @@ -94,17 +95,15 @@ export interface CommandError { message: string; } -export type YargsCommandNames = Map>; - -export type LoadedCommands = { - commandsMap: CommandsMap; - yargsCommandNames: YargsCommandNames; -}; - export interface CommandWrapper extends Command { name: string; group: string; path: string; + global: boolean; + installed: boolean; + default?: boolean; } -export type CommandsMap = Map; +export type CommandMap = Map; + +export type GroupMap = Map; diff --git a/src/loadCommands.ts b/src/loadCommands.ts index 3d6b6923..68700f02 100644 --- a/src/loadCommands.ts +++ b/src/loadCommands.ts @@ -1,12 +1,8 @@ import * as globby from 'globby'; import { resolve as pathResolve, join } from 'path'; -import { CliConfig, CommandsMap, CommandWrapper, LoadedCommands, YargsCommandNames } from './interfaces'; +import { CliConfig, CommandWrapper, GroupMap } from './interfaces'; import configurationHelper from './configurationHelper'; -export function setDefaultGroup(commandsMap: CommandsMap, commandName: string, commandWrapper: CommandWrapper) { - commandsMap.set(commandName, commandWrapper); -} - export function isEjected(groupName: string, command: string): boolean { const config: any = configurationHelper.sandbox(groupName, command).get(); return config && config['ejected']; @@ -49,34 +45,24 @@ export async function enumerateBuiltInCommands(config: CliConfig): Promise CommandWrapper): Promise { - return new Promise((resolve, reject) => { - const commandsMap: CommandsMap = new Map(); - const yargsCommandNames: YargsCommandNames = new Map(); +export async function loadCommands(paths: string[], load: (path: string) => CommandWrapper): Promise { + return new Promise((resolve, reject) => { + const specialCommandsMap: GroupMap = new Map(); paths.forEach((path) => { try { const commandWrapper = load(path); const { group, name } = commandWrapper; - let compositeKey = group; - if (name) { - compositeKey = `${group}-${name}`; - } if (!isEjected(group, name)) { - if (!commandsMap.has(group)) { - // First of each type will be 'default' for now - setDefaultGroup(commandsMap, group, commandWrapper); - yargsCommandNames.set(group, new Set()); + if (!specialCommandsMap.has(group)) { + commandWrapper.default = true; + specialCommandsMap.set(group, new Map()); } - if (!commandsMap.has(compositeKey)) { - commandsMap.set(compositeKey, commandWrapper); - } - - const groupCommandNames = yargsCommandNames.get(group); - if (groupCommandNames) { - groupCommandNames.add(compositeKey); + const commandsMap = specialCommandsMap.get(group)!; + if (!specialCommandsMap.get(group)!.has(name)) { + commandsMap.set(name, commandWrapper); } } } catch (error) { @@ -85,9 +71,6 @@ export async function loadCommands(paths: string[], load: (path: string) => Comm } }); - resolve({ - commandsMap, - yargsCommandNames - }); + resolve(specialCommandsMap); }); } diff --git a/src/registerCommands.ts b/src/registerCommands.ts index d3a3e2da..b46509fe 100644 --- a/src/registerCommands.ts +++ b/src/registerCommands.ts @@ -1,18 +1,21 @@ import chalk from 'chalk'; import { Argv, Options } from 'yargs'; -import { getGroupDescription } from './command'; import CommandHelper from './CommandHelper'; import configurationHelperFactory from './configurationHelper'; import HelperFactory from './Helper'; -import { CommandError, CommandsMap, CommandWrapper, YargsCommandNames } from './interfaces'; -import { helpUsage, helpEpilog } from './text'; +import { CommandError, CommandWrapper, GroupMap, CommandMap } from './interfaces'; +import { formatHelp } from './help'; +import { createOptionValidator } from './validation'; +import { getCommand } from './command'; + +const requireOptions = { + demand: false, + demandOption: false, + requiresArg: false, + require: false, + required: false +}; -/** - * General purpose error handler for commands. If the command has an exit code, it is considered - * critical and we exit immediately. Otherwise we just let things run their course. - * - * @param error - */ function reportError(error: CommandError) { let exitCode = 1; if (error.exitCode !== undefined) { @@ -102,179 +105,101 @@ function parseAliases(aliases: Aliases, key: string, optionAlias: string | strin return aliases; } -/** - * Registers groups and initiates registration of commands - * - * @param yargs Yargs instance - * @param helper Helper instance - * @param groupName the name of the group - * @param commandOptions The set of commandOption keys - * @param commandsMap The map of composite keys to commands - */ -function registerGroups( - yargs: Argv, - helper: HelperFactory, - groupName: string, - commandOptions: Set, - commandsMap: CommandsMap -): void { - const groupDescription = getGroupDescription(commandOptions, commandsMap); - const defaultCommand = commandsMap.get(groupName); - const defaultCommandAvailable = !!(defaultCommand && defaultCommand.register && defaultCommand.run); - const defaultCommandName = defaultCommand && defaultCommand.name; +function registerGroups(yargs: Argv, helper: HelperFactory, groupName: string, commandMap: CommandMap): void { + const groupMap = new Map().set(groupName, commandMap); + const defaultCommand = getCommand(groupMap, groupName); let aliases: Aliases = {}; - yargs.command( groupName, - groupDescription, + false, (subYargs: Argv) => { - if (defaultCommandAvailable) { + if (defaultCommand) { defaultCommand.register((key: string, options: Options) => { aliases = parseAliases(aliases, key, options.alias); - subYargs.option(key, { - group: `Default Command Options ('${defaultCommand.name}')`, - ...options - }); - }, helper.sandbox(groupName, defaultCommandName)); + subYargs.option(key, { ...options, ...requireOptions }); + }, helper.sandbox(groupName, defaultCommand.name)); } - registerCommands(subYargs, helper, groupName, commandOptions, commandsMap); - return subYargs; + + registerCommands(subYargs, helper, groupName, commandMap); + return subYargs + .option('h', { + alias: 'help' + }) + .showHelpOnFail(false, formatHelp({ _: [groupName] }, groupMap)) + .strict(); }, (argv: any) => { - // argv._ is an array of commands. - // if `dojo example` was called, it will only be size one, - // so we call default command, else, the subcommand will - // have been ran and we don't want to run the default. - if (defaultCommandAvailable && argv._.length === 1) { + if (defaultCommand && argv._.length === 1) { + if (argv.h || argv.help) { + console.log(formatHelp(argv, groupMap)); + return Promise.resolve({}); + } + const args = getOptions( aliases, - helper.sandbox(groupName, defaultCommandName).configuration.get(), + helper.sandbox(groupName, defaultCommand.name).configuration.get(), argv ); - return defaultCommand.run(helper.sandbox(groupName, defaultCommandName), args).catch(reportError); + return defaultCommand.run(helper.sandbox(groupName, defaultCommand.name), args).catch(reportError); } } ); } -/** - * Register commands - * - * @param yargs Yargs instance - * @param helper Helper instance - * @param groupName the name of the group - * @param commandOptions The set of commandOption keys - * @param commandsMap The map of composite keys to commands - */ -function registerCommands( - yargs: Argv, - helper: HelperFactory, - groupName: string, - commandOptions: Set, - commandsMap: CommandsMap -): void { - [...commandOptions] - .filter((command: string) => { - return `${groupName}-` !== command; - }) - .forEach((command: string) => { - const { name, description, register, run } = commandsMap.get(command); - let aliases: Aliases = {}; - yargs - .command( - name, - description, - (optionsYargs: Argv) => { - register((key: string, options: Options) => { - aliases = parseAliases(aliases, key, options.alias); - optionsYargs.option(key, options); - }, helper.sandbox(groupName, name)); - return optionsYargs; - }, - (argv: any) => { - const args = getOptions(aliases, helper.sandbox(groupName, name).configuration.get(), argv); - return run(helper.sandbox(groupName, name), args).catch(reportError); - } - ) - .strict(); - }); -} - -/** - * Registers command aliases as new groups - * - * @param yargs Yargs instance - * @param helper Helper instance - * @param commandOptions The set of commandOption keys - * @param commandsMap The map of composite keys to commands - */ -function registerAliases( - yargs: Argv, - helper: HelperFactory, - commandOptions: Set, - commandsMap: CommandsMap -): void { - [...commandOptions].forEach((command: string) => { - const { run, register, alias: aliases, group } = commandsMap.get(command); - if (aliases) { - (Array.isArray(aliases) ? aliases : [aliases]).forEach((alias) => { - const { name, description, options: aliasOpts } = alias; - let aliases: Aliases = {}; - yargs.command( - name, - description || '', - (aliasYargs: Argv) => { - register((key: string, options: Options) => { - if (!aliasOpts || !aliasOpts.some((option) => option.option === key)) { - aliases = parseAliases(aliases, key, options.alias); - aliasYargs.option(key, options); - } - }, helper.sandbox(group, name)); - return aliasYargs; - }, - (argv: any) => { - if (aliasOpts) { - argv = aliasOpts.reduce((accumulator, option) => { - return { - ...accumulator, - [option.option]: option.value - }; - }, argv); - } - const args = getOptions(aliases, helper.sandbox(group, name).configuration.get(), argv); - return run(helper.sandbox(group, name), args).catch(reportError); - } - ); - }); - } +function registerCommands(yargs: Argv, helper: HelperFactory, groupName: string, commandMap: CommandMap): void { + [...commandMap.values()].forEach((command: CommandWrapper) => { + const { name, register, run } = command; + let aliases: Aliases = {}; + const groupMap = new Map().set(groupName, commandMap); + yargs.command( + name, + false, + (optionsYargs: Argv) => { + register((key: string, options: Options) => { + aliases = parseAliases(aliases, key, options.alias); + optionsYargs.option(key, { ...options, ...requireOptions }); + }, helper.sandbox(groupName, name)); + + return optionsYargs.showHelpOnFail(false, formatHelp({ _: [groupName, name] }, groupMap)).strict(); + }, + (argv: any) => { + if (argv.h || argv.help) { + console.log(formatHelp(argv, groupMap)); + return Promise.resolve({}); + } + const args = getOptions(aliases, helper.sandbox(groupName, name).configuration.get(), argv); + return run(helper.sandbox(groupName, name), args).catch(reportError); + } + ); }); } -/** - * Registers commands and subcommands using yargs. Receives a CommandsMap of commands and - * a map of YargsCommandNames which links composite keys to groups. - * Subcommands have to be registered when a group is registered, this is a restriction of - * yargs. - * @param yargs Yargs instance - * @param commandsMap The map of composite keys to commands - * @param yargsCommandNames Map of groups and names to composite keys - */ -export default function(yargs: Argv, commandsMap: CommandsMap, yargsCommandNames: YargsCommandNames): void { +export default function(yargs: Argv, groupMap: GroupMap): void { const helperContext = {}; - - const commandHelper = new CommandHelper(commandsMap, helperContext, configurationHelperFactory); + const commandHelper = new CommandHelper(groupMap, helperContext, configurationHelperFactory); const helperFactory = new HelperFactory(commandHelper, yargs, helperContext, configurationHelperFactory); - yargsCommandNames.forEach((commandOptions, commandName) => { - registerGroups(yargs, helperFactory, commandName, commandOptions, commandsMap); - registerAliases(yargs, helperFactory, commandOptions, commandsMap); + groupMap.forEach((commandMap, group) => { + registerGroups(yargs, helperFactory, group, commandMap); }); yargs .demand(1, '') - .usage(helpUsage) - .epilog(helpEpilog) - .help('h') - .alias('h', 'help') + .command( + '$0', + false, + (dojoYargs: Argv) => { + dojoYargs.option('h', { + alias: 'help' + }); + return dojoYargs; + }, + (argv: any) => { + console.log(formatHelp(argv, groupMap)); + } + ) + .check(createOptionValidator(groupMap), true) + .help(false) + .showHelpOnFail(false) .strict().argv; } diff --git a/src/text.ts b/src/text.ts deleted file mode 100644 index ea8f96cb..00000000 --- a/src/text.ts +++ /dev/null @@ -1,13 +0,0 @@ -import chalk from 'chalk'; - -export const helpUsage = `${chalk.bold('dojo help')} - -Usage: dojo [options] - -Hey there, here are all the things you can do with @dojo/cli:`; - -export const helpEpilog = `For more information on any of these commands just run them with '-h'. - -e.g. 'dojo build -h' will give you the help for the 'build' group of commands. - -If a non-builtin command (e.g. build) appears missing from the command list, please ensure it is listed in package.json and correctly installed.`; diff --git a/src/validation.ts b/src/validation.ts new file mode 100644 index 00000000..022f8c11 --- /dev/null +++ b/src/validation.ts @@ -0,0 +1,37 @@ +import chalk from 'chalk'; +import { Options } from 'yargs'; +import { GroupMap } from './interfaces'; +import { getCommand } from './command'; + +export function isRequiredOption(options: Options): boolean { + return !!(options.demand || options.demandOption || options.require || options.requiresArg || options.required); +} + +export function createOptionValidator(groupMap: GroupMap) { + return (argv: any) => { + if (argv.h || argv.help || argv._.length === 0) { + return true; + } + const groupName: string = argv._[0]; + const commandName: string = argv._[1]; + let validationError = ''; + + const command = getCommand(groupMap, groupName, commandName); + + command.register( + (key, options) => { + if (argv[key] === undefined && isRequiredOption(options)) { + if (!validationError) { + validationError = `\n${chalk.bold.red('Error(s):')}`; + } + validationError = `${validationError}\n Required option '${chalk.redBright(key)}' not provided`; + } + }, + null as any + ); + if (validationError) { + throw new Error(validationError); + } + return true; + }; +} diff --git a/tarballs/dojo-interfaces-0.2.2.tgz b/tarballs/dojo-interfaces-0.2.2.tgz deleted file mode 100644 index dddf7201..00000000 Binary files a/tarballs/dojo-interfaces-0.2.2.tgz and /dev/null differ diff --git a/tests/support/testHelper.ts b/tests/support/testHelper.ts index 5ac3a2fd..35136925 100644 --- a/tests/support/testHelper.ts +++ b/tests/support/testHelper.ts @@ -1,17 +1,15 @@ import { CommandWrapper } from '../../src/interfaces'; import { stub, spy } from 'sinon'; -export type GroupDef = [ - { - groupName: string; - commands: [ - { - commandName: string; - fails?: boolean; - } - ]; - } -]; +export type GroupDef = { + groupName: string; + commands: [ + { + commandName: string; + fails?: boolean; + } + ]; +}[]; export interface CommandWrapperConfig { group?: string; @@ -20,24 +18,31 @@ export interface CommandWrapperConfig { path?: string; runs?: boolean; eject?: boolean; + installed?: boolean; + global?: boolean; } -export function getCommandsMap(groupDef: GroupDef, registerMock?: Function) { - const commands = new Map(); +export function getGroupMap(groupDef: GroupDef, registerMock?: Function) { + const groupMap = new Map(); if (registerMock === undefined) { registerMock = (compositeKey: string) => { - return (func: Function) => { - func('key', {}); - return compositeKey; - }; + const registerStub = stub(); + registerStub.yields('key', {}).returns(compositeKey); + return registerStub; }; } - + let isDefault = false; groupDef.forEach((group) => { + let commandMap = groupMap.get(group.groupName); + if (!commandMap) { + commandMap = new Map(); + groupMap.set(group.groupName, commandMap); + isDefault = true; + } group.commands.forEach((command) => { const compositeKey = `${group.groupName}-${command.commandName}`; const runSpy = spy( - () => (command.fails ? Promise.reject(new Error(compositeKey)) : Promise.resolve(compositeKey)) + () => (command.fails ? Promise.reject(new Error('test error message')) : Promise.resolve(compositeKey)) ); const commandWrapper = { name: command.commandName, @@ -45,16 +50,18 @@ export function getCommandsMap(groupDef: GroupDef, registerMock?: Function) { description: compositeKey, register: registerMock!(compositeKey), runSpy, + default: isDefault, run: runSpy }; - commands.set(compositeKey, commandWrapper); + isDefault = false; + commandMap.set(command.commandName, commandWrapper); }); }); - return commands; + return groupMap; } -const yargsFunctions = ['demand', 'usage', 'epilog', 'help', 'alias', 'strict', 'option']; +const yargsFunctions = ['demand', 'usage', 'epilog', 'help', 'alias', 'strict', 'option', 'check', 'showHelpOnFail']; export function getYargsStub(aliases: any = {}) { const yargsStub: any = { parsed: { @@ -80,13 +87,24 @@ export function getCommandWrapper(name: string, runs: boolean = true) { } export function getCommandWrapperWithConfiguration(config: CommandWrapperConfig): CommandWrapper { - const { group = '', name = '', description = '', path = '', runs = false, eject = false } = config; + const { + group = '', + name = '', + description = '', + path = '', + runs = false, + eject = false, + global = false, + installed = false + } = config; const commandWrapper: CommandWrapper = { group, name, description, path, + global, + installed, register: stub().returns('registered'), run: stub().returns(runs ? 'success' : 'error') }; diff --git a/tests/unit/CommandHelper.ts b/tests/unit/CommandHelper.ts index fed9db8e..029c59ac 100644 --- a/tests/unit/CommandHelper.ts +++ b/tests/unit/CommandHelper.ts @@ -3,7 +3,7 @@ const { assert } = intern.getPlugin('chai'); import { SinonStub, stub } from 'sinon'; import * as mockery from 'mockery'; -import { getCommandsMap, GroupDef } from '../support/testHelper'; +import { getGroupMap, GroupDef } from '../support/testHelper'; import configurationHelperFactory from '../../src/configurationHelper'; const groupDef: GroupDef = [ @@ -16,7 +16,7 @@ const groupDef: GroupDef = [ commands: [{ commandName: 'command1' }, { commandName: 'failcommand', fails: true }] } ]; -let commandsMap: any; +let groupMap: any; let commandHelper: any; const templateStub: SinonStub = stub(); @@ -33,9 +33,9 @@ registerSuite('CommandHelper', { default: templateStub }); - commandsMap = getCommandsMap(groupDef); + groupMap = getGroupMap(groupDef); const commandHelperCtor = require('../../src/CommandHelper').default; - commandHelper = new commandHelperCtor(commandsMap, context, configurationHelperFactory); + commandHelper = new commandHelperCtor(groupMap, context, configurationHelperFactory); }, afterEach() { @@ -44,14 +44,14 @@ registerSuite('CommandHelper', { }, tests: { 'Should set commandsMap and context'() { - assert.strictEqual(commandsMap, commandHelper._commandsMap); + assert.strictEqual(groupMap, commandHelper._groupMap); assert.strictEqual(context, commandHelper._context); }, 'Should return exists = true when a queried command exists'() { assert.isTrue(commandHelper.exists('group1', 'command1')); }, 'Should accept composite key for query and return exists = true when a command exists'() { - assert.isTrue(commandHelper.exists('group1-command1')); + assert.isTrue(commandHelper.exists('group1')); }, 'Should return exists = false when a queried command does not exist'() { assert.isFalse(commandHelper.exists('group3', 'command3')); @@ -59,7 +59,7 @@ registerSuite('CommandHelper', { 'Should run a command that exists and return a promise that resolves'() { const key = 'group1-command1'; return commandHelper - .run(key) + .run('group1', 'command1') .then((response: string) => { assert.equal(key, response); }) @@ -70,9 +70,9 @@ registerSuite('CommandHelper', { 'Should run a command that exists with args and return a promise that resolves'() { const key = 'group1-command1'; return commandHelper - .run(key, undefined, 'args') + .run('group1', undefined, 'args') .then((response: string) => { - const mockCommand = commandsMap.get(key); + const mockCommand = groupMap.get('group1')!.get('command1')!; assert.isTrue(mockCommand.runSpy.called); assert.equal(mockCommand.runSpy.getCall(0).args[1], 'args'); assert.equal(key, response); @@ -82,15 +82,30 @@ registerSuite('CommandHelper', { }); }, 'Should run a command that exists and return a rejected promise when it fails'() { - const key = 'group2-failcommand'; return commandHelper - .run(key) + .run('group2', 'failcommand') .then( (response: string) => { assert.fail(null, null, 'Should not have resolved'); }, (error: Error) => { - assert.equal(key, error.message); + assert.equal('test error message', error.message); + } + ) + .catch(() => { + assert.fail(null, null, 'commandHelper.run should not have rejected promise'); + }); + }, + 'Should not run a group that does not exist and return a rejected promise'() { + const expectedErrorMsg = 'The command does not exist'; + return commandHelper + .run('nogroup') + .then( + (response: string) => { + assert.fail(null, null, 'Should not have resolved'); + }, + (error: Error) => { + assert.equal(expectedErrorMsg, error.message); } ) .catch(() => { @@ -98,10 +113,9 @@ registerSuite('CommandHelper', { }); }, 'Should not run a command that does not exist and return a rejected promise'() { - const key = 'nogroup-nocommand'; const expectedErrorMsg = 'The command does not exist'; return commandHelper - .run(key) + .run('nogroup', 'nocommand') .then( (response: string) => { assert.fail(null, null, 'Should not have resolved'); diff --git a/tests/unit/all.ts b/tests/unit/all.ts index b4cf5b39..aabbb26a 100644 --- a/tests/unit/all.ts +++ b/tests/unit/all.ts @@ -5,11 +5,12 @@ import './configurationHelper'; import './config'; import './index'; import './installableCommands'; +import './help'; import './loadCommands'; import './npmInstall'; import './registerCommands'; -import './text'; import './template'; +import './validation'; import './updateNotifier'; import './commands/eject'; import './commands/init'; diff --git a/tests/unit/allCommands.ts b/tests/unit/allCommands.ts index c03f407a..27edc051 100644 --- a/tests/unit/allCommands.ts +++ b/tests/unit/allCommands.ts @@ -18,13 +18,11 @@ describe('AllCommands', () => { mockCommand = mockModule.getMock('./command'); mockLoadCommands = mockModule.getMock('./loadCommands'); - mockLoadCommands.loadCommands = sandbox.stub().resolves({ - commandsMap: new Map([ - ['key1', { name: 'a', group: 'c', path: 'as' }], - ['key2', { name: 'b', group: 'd', path: 'asas' }] - ]), - yargsCommandNames: new Map([['key3', new Set(['a', 'b'])], ['key4', new Set(['d', 'e'])]]) - }); + const groupCCommandMap = new Map().set('a', { name: 'a', group: 'c', path: 'as' }); + const groupDCommandMap = new Map().set('b', { name: 'b', group: 'd', path: 'asas' }); + const groupMap = new Map().set('c', groupCCommandMap).set('d', groupDCommandMap); + + mockLoadCommands.loadCommands = sandbox.stub().resolves(groupMap); moduleUnderTest = mockModule.getModuleUnderTest(); }); @@ -60,109 +58,4 @@ describe('AllCommands', () => { assert.fail(null, null, 'moduleUnderTest.run should not have rejected promise'); }); }); - - it('should perform initialsation only once', () => { - return moduleUnderTest - .default() - .then(function() { - assert.isTrue( - mockCommand.createBuiltInCommandLoader.calledOnce, - 'should call builtin command loader once' - ); - assert.isTrue(mockCommand.initCommandLoader.calledOnce, 'should call installed command loader once'); - assert.isTrue( - mockLoadCommands.enumerateBuiltInCommands.calledOnce, - 'should call builtin command enumerator once' - ); - assert.isTrue( - mockLoadCommands.enumerateInstalledCommands.calledOnce, - 'should call installed command enumerator once' - ); - assert.isTrue(mockLoadCommands.loadCommands.calledTwice, 'should call load commands twice'); - - moduleUnderTest - .default() - .then(function() { - assert.isTrue( - mockCommand.createBuiltInCommandLoader.calledOnce, - 'should call builtin command loader once only' - ); - assert.isTrue( - mockCommand.initCommandLoader.calledOnce, - 'should call installed command loader once only' - ); - assert.isTrue( - mockLoadCommands.enumerateBuiltInCommands.calledOnce, - 'should call builtin command enumerator once only' - ); - assert.isTrue( - mockLoadCommands.enumerateInstalledCommands.calledOnce, - 'should call installed command enumerator once only' - ); - assert.isTrue( - mockLoadCommands.loadCommands.calledTwice, - 'should call load commands twice only' - ); - }) - .catch(() => { - assert.fail(null, null, 'moduleUnderTest.run should not have rejected promise'); - }); - }) - .catch(() => { - assert.fail(null, null, 'moduleUnderTest.run should not have rejected promise'); - }); - }); - it('should reset the command cache', () => { - return moduleUnderTest - .default() - .then(function() { - assert.isTrue( - mockCommand.createBuiltInCommandLoader.calledOnce, - 'should call builtin command loader once' - ); - assert.isTrue(mockCommand.initCommandLoader.calledOnce, 'should call installed command loader once'); - assert.isTrue( - mockLoadCommands.enumerateBuiltInCommands.calledOnce, - 'should call builtin command enumerator once' - ); - assert.isTrue( - mockLoadCommands.enumerateInstalledCommands.calledOnce, - 'should call installed command enumerator once' - ); - assert.isTrue(mockLoadCommands.loadCommands.calledTwice, 'should call load commands twice only'); - - moduleUnderTest.reset(); - moduleUnderTest - .default() - .then(function() { - assert.isTrue( - mockCommand.createBuiltInCommandLoader.calledTwice, - 'should call builtin command loader once' - ); - assert.isTrue( - mockCommand.initCommandLoader.calledTwice, - 'should call installed command loader once' - ); - assert.isTrue( - mockLoadCommands.enumerateBuiltInCommands.calledTwice, - 'should call builtin command enumerator once' - ); - assert.isTrue( - mockLoadCommands.enumerateInstalledCommands.calledTwice, - 'should call installed command enumerator once' - ); - assert.equal( - mockLoadCommands.loadCommands.callCount, - 4, - 'should call load commands twice only' - ); - }) - .catch(() => { - assert.fail(null, null, 'moduleUnderTest.run should not have rejected promise'); - }); - }) - .catch(() => { - assert.fail(null, null, 'moduleUnderTest.run should not have rejected promise'); - }); - }); }); diff --git a/tests/unit/bin/dojo.ts b/tests/unit/bin/dojo.ts index b45d4346..552444d0 100644 --- a/tests/unit/bin/dojo.ts +++ b/tests/unit/bin/dojo.ts @@ -4,7 +4,7 @@ const { assert } = intern.getPlugin('chai'); import MockModule from '../../support/MockModule'; import * as sinon from 'sinon'; -import { CommandsMap, CommandWrapper, LoadedCommands } from '../../../src/interfaces'; +import { CommandMap, CommandWrapper, GroupMap } from '../../../src/interfaces'; import { getCommandWrapperWithConfiguration } from '../../support/testHelper'; describe('cli .bin', () => { @@ -28,19 +28,15 @@ describe('cli .bin', () => { mockYargs.ctor.alias = sandbox.stub().returns(mockYargs.ctor); mockYargs.ctor.strict = sandbox.stub().returns(mockYargs.ctor); mockAllCommands = mockModule.getMock('./allCommands'); - const commands: LoadedCommands = { - commandsMap: new Map(), - yargsCommandNames: new Map() - }; const installedCommandWrapper1 = getCommandWrapperWithConfiguration({ group: 'eject', name: '' }); - const commandMap: CommandsMap = new Map([['eject', installedCommandWrapper1]]); - commands.commandsMap = commandMap; + const commandMap: CommandMap = new Map([['eject', installedCommandWrapper1]]); + const groupMap: GroupMap = new Map([['eject', commandMap]]); mockAllCommandsPromise = new Promise((resolve) => setTimeout(() => { - resolve(commands); + resolve(groupMap); }, 1000) ); mockAllCommands.default = sandbox.stub().resolves(mockAllCommandsPromise); diff --git a/tests/unit/command.ts b/tests/unit/command.ts index 70543a91..13fd885d 100644 --- a/tests/unit/command.ts +++ b/tests/unit/command.ts @@ -1,13 +1,14 @@ const { registerSuite } = intern.getInterface('object'); const { assert } = intern.getPlugin('chai'); -import { getCommandsMap, GroupDef } from '../support/testHelper'; import * as command from '../../src/command'; import * as expectedCommand from '../support/test-prefix-foo-bar'; import * as expectedBuiltInCommand from '../support/commands/test-prefix-foo-bar'; import expectedEsModuleCommand from '../support/esmodule-prefix-foo-bar'; +import { CommandWrapper } from '../../src/interfaces'; +import { getCommand } from '../../src/command'; const testGroup = 'foo'; const testName = 'bar'; @@ -16,19 +17,36 @@ const testEsModuleSearchPrefixes = ['esmodule-prefix']; const testEsModuleFailSearchPrefixes = ['esmodule-fail']; const testSearchPrefixesDashedNames = ['dash-names']; let commandWrapper: any; -const groupDef: GroupDef = [ - { - groupName: 'group1', - commands: [{ commandName: 'command1' }] - }, - { - groupName: 'group2', - commands: [{ commandName: 'command1' }] - } -]; -const commandsMap = getCommandsMap(groupDef); let loader: any; +const groupMap = new Map(); +const fooCommandMap = new Map(); +const defaultFooCommand = { + name: 'global', + group: 'foo', + path: 'path/to/command', + global: true, + installed: true, + description: 'a global command', + default: true, + register: () => {}, + run: () => Promise.resolve() +}; +const nonDefaultFooCommand = { + name: 'project', + group: 'foo', + path: 'path/to/command', + global: false, + installed: true, + description: 'a project command', + default: false, + register: () => {}, + run: () => Promise.resolve() +}; +fooCommandMap.set('global', defaultFooCommand); +fooCommandMap.set('project', nonDefaultFooCommand); +groupMap.set('foo', fooCommandMap); + function getCommandPath(prefixes: string[]): string[] { return prefixes.map((prefix) => { return `../tests/support/${prefix}-${testGroup}-${testName}`; @@ -156,27 +174,20 @@ registerSuite('command', { } } }, - getGroupDescription: { - before() { - loader = command.initCommandLoader(testSearchPrefixes); + getCommand: { + 'should return command'() { + const command = getCommand(groupMap, 'foo', 'project'); + assert.strictEqual(command, nonDefaultFooCommand); }, - - tests: { - 'Should return simple command description when only one command name passed'() { - const key = 'group1-command1'; - const description = command.getGroupDescription(new Set([key]), commandsMap); - assert.equal(commandsMap.get(key).description, description); - }, - 'Should return composite description of sub commands when multiple command names passed'() { - const key1 = 'group1-command1'; - const key2 = 'group2-command1'; - const description = command.getGroupDescription(new Set([key1, key2]), commandsMap); - const expected = `${commandsMap.get(key1).name} ${commandsMap.get(key1).description}\n${ - commandsMap.get(key1).name - } ${commandsMap.get(key2).description}`; - - assert.equal(expected, description); - } + 'should return default command for group'() { + const command = getCommand(groupMap, 'foo'); + assert.strictEqual(command, defaultFooCommand); + }, + 'should throw an error when the group does not exist'() { + assert.throws(() => getCommand(groupMap, 'fake'), /Unable to find command group: fake/); + }, + 'should throw an error when the command does not exist'() { + assert.throws(() => getCommand(groupMap, 'foo', 'fake'), /Unable to find command: fake for group: foo/); } } }); diff --git a/tests/unit/commands/eject.ts b/tests/unit/commands/eject.ts index e230edb0..2588b62e 100644 --- a/tests/unit/commands/eject.ts +++ b/tests/unit/commands/eject.ts @@ -5,7 +5,7 @@ import chalk from 'chalk'; import { join, resolve as pathResolve, sep } from 'path'; import * as sinon from 'sinon'; -import { CommandsMap, CommandWrapper } from '../../../src/interfaces'; +import { CommandMap, CommandWrapper } from '../../../src/interfaces'; import MockModule from '../../support/MockModule'; import { getCommandWrapperWithConfiguration } from '../../support/testHelper'; @@ -71,7 +71,7 @@ describe('eject command', () => { it(`should abort eject when 'N' selected`, () => { const abortOutput = 'Aborting eject'; - const commandMap: CommandsMap = new Map(); + const commandMap: CommandMap = new Map(); const helper = getHelper(); mockInquirer.prompt = sandbox.stub().resolves({ eject: false }); @@ -98,12 +98,13 @@ describe('eject command', () => { name: '' }); - const commandMap: CommandsMap = new Map([ + const commandMap: CommandMap = new Map([ ['command', installedCommandWrapper1], ['version', installedCommandWrapper2] ]); + const groupMap = new Map([['test', commandMap]]); const helper = getHelper(); - mockAllExternalCommands.loadExternalCommands = sandbox.stub().resolves({ commandsMap: commandMap }); + mockAllExternalCommands.loadExternalCommands = sandbox.stub().resolves(groupMap); return moduleUnderTest.run(helper, {}).then( () => { assert.equal(consoleLogStub.args[0][0], runOutput); @@ -116,11 +117,12 @@ describe('eject command', () => { describe('save ejected config', () => { it('should save config', () => { - const commandMap: CommandsMap = new Map([ + const commandMap: CommandMap = new Map([ ['apple', loadCommand('command-with-full-eject')] ]); + const groupMap = new Map([['test', commandMap]]); const helper = getHelper(); - mockAllExternalCommands.loadExternalCommands = sandbox.stub().resolves({ commandsMap: commandMap }); + mockAllExternalCommands.loadExternalCommands = sandbox.stub().resolves(groupMap); const configurationHelper = mockModule.getMock('../configurationHelper').default; @@ -143,11 +145,12 @@ describe('eject command', () => { describe('eject npm config', () => { it('should run npm install', () => { - const commandMap: CommandsMap = new Map([ + const commandMap: CommandMap = new Map([ ['apple', loadCommand('command-with-full-eject')] ]); + const groupMap = new Map([['test', commandMap]]); const helper = getHelper(); - mockAllExternalCommands.loadExternalCommands = sandbox.stub().resolves({ commandsMap: commandMap }); + mockAllExternalCommands.loadExternalCommands = sandbox.stub().resolves(groupMap); return moduleUnderTest.run(helper, {}).then( () => { assert.isTrue(mockNpmInstall.installDependencies.calledOnce); @@ -162,11 +165,12 @@ describe('eject command', () => { describe('eject copy config', () => { it('should run copy files', () => { - const commandMap: CommandsMap = new Map([ + const commandMap: CommandMap = new Map([ ['apple', loadCommand('command-with-full-eject')] ]); + const groupMap = new Map([['test', commandMap]]); const helper = getHelper(); - mockAllExternalCommands.loadExternalCommands = sandbox.stub().resolves({ commandsMap: commandMap }); + mockAllExternalCommands.loadExternalCommands = sandbox.stub().resolves(groupMap); return moduleUnderTest.run(helper, {}).then( () => { assert.isTrue( @@ -193,11 +197,12 @@ describe('eject command', () => { }); it('should not copy files if no files are specified', () => { - const commandMap: CommandsMap = new Map([ + const commandMap: CommandMap = new Map([ ['apple', loadCommand('command-with-nofile-eject')] ]); + const groupMap = new Map([['test', commandMap]]); const helper = getHelper(); - mockAllExternalCommands.loadExternalCommands = sandbox.stub().resolves({ commandsMap: commandMap }); + mockAllExternalCommands.loadExternalCommands = sandbox.stub().resolves(groupMap); return moduleUnderTest.run(helper, {}).then( () => { assert.isTrue(mockFsExtra.copySync.notCalled); @@ -211,11 +216,12 @@ describe('eject command', () => { describe('eject hints', () => { it('should show hints when supplied', () => { - const commandMap: CommandsMap = new Map([ + const commandMap: CommandMap = new Map([ ['apple', loadCommand('command-with-hints')] ]); + const groupMap = new Map([['test', commandMap]]); const helper = getHelper(); - mockAllExternalCommands.loadExternalCommands = sandbox.stub().resolves({ commandsMap: commandMap }); + mockAllExternalCommands.loadExternalCommands = sandbox.stub().resolves(groupMap); return moduleUnderTest.run(helper, {}).then( () => { const logCallCount = consoleLogStub.callCount; diff --git a/tests/unit/commands/init.ts b/tests/unit/commands/init.ts index a1a4611f..8fe323c2 100644 --- a/tests/unit/commands/init.ts +++ b/tests/unit/commands/init.ts @@ -23,7 +23,8 @@ describe('init command', () => { fs.existsSync = sandbox.stub().returns(true); fs.writeFileSync = sandbox.stub(); - const commandsMap = new Map(); + const buildCommandMap = new Map(); + const testCommandMap = new Map(); const build = { name: 'webpack', group: 'build' @@ -33,13 +34,12 @@ describe('init command', () => { group: 'test' }; - commandsMap.set('build', build); - commandsMap.set('build-webpack', build); - commandsMap.set('test', test); - commandsMap.set('test-intern', test); + buildCommandMap.set('webpack', build); + testCommandMap.set('test', test); + const groupMap = new Map([['build', buildCommandMap], ['test', testCommandMap]]); const allCommands = mockModule.getMock('../allCommands'); - allCommands.loadExternalCommands.returns({ commandsMap }); + allCommands.loadExternalCommands.returns(groupMap); }); afterEach(() => { diff --git a/tests/unit/commands/version.ts b/tests/unit/commands/version.ts index 7773bfd0..9d96aeab 100644 --- a/tests/unit/commands/version.ts +++ b/tests/unit/commands/version.ts @@ -7,7 +7,7 @@ import chalk from 'chalk'; import { join, resolve as pathResolve } from 'path'; -import { CommandsMap, CommandWrapper } from '../../../src/interfaces'; +import { CommandMap, CommandWrapper } from '../../../src/interfaces'; import { getCommandWrapperWithConfiguration } from '../../support/testHelper'; const validPackageInfo: any = require('../../support/valid-package/package.json'); const anotherValidPackageInfo: any = require('../../support/another-valid-package/package.json'); @@ -60,13 +60,12 @@ describe('version command', () => { it(`should run and return 'no registered commands' when there are no installed commands`, () => { const noCommandOutput = `${noCommandsPrefix}${outputSuffix}`; - const commandMap: CommandsMap = new Map(); + const groupMap = new Map(); const helper = { command: 'version' }; - mockAllCommands.default = sandbox.stub().resolves({ commandsMap: commandMap }); + mockAllCommands.default = sandbox.stub().resolves(groupMap); return moduleUnderTest.run(helper, { outdated: false }).then( () => { - // assert.isTrue(mockDavid.getUpdatedDependencies.notCalled); assert.equal(logStub.firstCall.args[0].trim(), noCommandOutput); }, () => { @@ -84,8 +83,9 @@ describe('version command', () => { path: join(pathResolve('.'), 'path/that/does/not/exist') }); - const commandMap: CommandsMap = new Map([['badCommand', badCommandWrapper]]); - mockAllCommands.default = sandbox.stub().resolves({ commandsMap: commandMap }); + const commandMap: CommandMap = new Map([['badCommand', badCommandWrapper]]); + const groupMap = new Map([['apple', commandMap]]); + mockAllCommands.default = sandbox.stub().resolves(groupMap); const helper = { command: 'version' }; return moduleUnderTest.run(helper, { outdated: false }).then( @@ -116,11 +116,12 @@ ${validPackageInfo.name}@${validPackageInfo.version} ${anotherValidPackageInfo.name}@${anotherValidPackageInfo.version} ${outputSuffix}`; - const commandMap: CommandsMap = new Map([ + const commandMap: CommandMap = new Map([ ['installedCommand1', installedCommandWrapper1], ['installedCommand2', installedCommandWrapper2] ]); - mockAllCommands.default = sandbox.stub().resolves({ commandsMap: commandMap }); + const groupMap = new Map([['test', commandMap]]); + mockAllCommands.default = sandbox.stub().resolves(groupMap); const helper = { command: 'version' }; return moduleUnderTest.run(helper, { outdated: false }).then( () => { @@ -149,13 +150,14 @@ ${outputSuffix}`; ${validPackageInfo.name}@${validPackageInfo.version} ${outputSuffix}`; - const commandMap: CommandsMap = new Map([ + const commandMap: CommandMap = new Map([ ['installedCommand1', installedCommandWrapper], ['builtInCommand1', builtInCommandWrapper] ]); + const groupMap = new Map([['test', commandMap]]); const helper = { command: 'version' }; - mockAllCommands.default = sandbox.stub().resolves({ commandsMap: commandMap }); + mockAllCommands.default = sandbox.stub().resolves(groupMap); return moduleUnderTest.run(helper, { outdated: false }).then( () => { assert.equal(logStub.firstCall.args[0].trim(), expectedOutput); @@ -184,12 +186,13 @@ ${outputSuffix}`; ${validPackageInfo.name}@${chalk.blue(validPackageInfo.version)} ${outputSuffix}`; - const commandMap: CommandsMap = new Map([ + const commandMap: CommandMap = new Map([ ['installedCommand1', installedCommandWrapper] ]); + const groupMap = new Map([['test', commandMap]]); const helper = { command: 'version' }; - mockAllCommands.default = sandbox.stub().resolves({ commandsMap: commandMap }); + mockAllCommands.default = sandbox.stub().resolves(groupMap); return moduleUnderTest.run(helper, { outdated: true }).then( () => { assert.isTrue(logStub.firstCall.calledWith('Fetching latest version information...')); @@ -218,12 +221,13 @@ ${outputSuffix}`; ${validPackageInfo.name}@${chalk.blue(validPackageInfo.version)} ${chalk.green('(latest is 1.2.3)')} ${outputSuffix}`; - const commandMap: CommandsMap = new Map([ + const commandMap: CommandMap = new Map([ ['installedCommand1', installedCommandWrapper] ]); + const groupMap = new Map([['test', commandMap]]); const helper = { command: 'version' }; - mockAllCommands.default = sandbox.stub().resolves({ commandsMap: commandMap }); + mockAllCommands.default = sandbox.stub().resolves(groupMap); return moduleUnderTest.run(helper, { outdated: true }).then( () => { assert.isTrue(logStub.firstCall.calledWith('Fetching latest version information...')); @@ -246,12 +250,13 @@ ${outputSuffix}`; const expectedOutput = 'Something went wrong trying to fetch command versions: Error'; - const commandMap: CommandsMap = new Map([ + const commandMap: CommandMap = new Map([ ['installedCommand1', installedCommandWrapper] ]); + const groupMap = new Map([['test', commandMap]]); const helper = { command: 'version' }; - mockAllCommands.default = sandbox.stub().resolves({ commandsMap: commandMap }); + mockAllCommands.default = sandbox.stub().resolves(groupMap); return moduleUnderTest.run(helper, { outdated: true }).then( () => { assert.isTrue(logStub.firstCall.calledWith('Fetching latest version information...')); diff --git a/tests/unit/help.ts b/tests/unit/help.ts new file mode 100644 index 00000000..f22e3ac5 --- /dev/null +++ b/tests/unit/help.ts @@ -0,0 +1,126 @@ +const { describe, it } = intern.getInterface('bdd'); +const { assert } = intern.getPlugin('chai'); + +import { formatHelp } from './../../src/help'; +import { CommandWrapper, OptionsHelper, Helper } from '../../src/interfaces'; + +const groupMap = new Map(); +const fooCommandMap = new Map(); +fooCommandMap.set('global', { + name: 'global', + group: 'foo', + path: 'path/to/command', + global: true, + installed: true, + description: 'a global command', + default: true, + register: (options: OptionsHelper, helper: Helper) => { + options('foo', { description: 'a basic option' }); + options('bar', { defaultDescription: 'a required option', require: true }); + }, + run: () => Promise.resolve() +}); +fooCommandMap.set('project', { + name: 'project', + group: 'foo', + path: 'path/to/command', + global: false, + installed: true, + description: 'a project command', + default: false, + register: (options: OptionsHelper, helper: Helper) => { + options('foo', { desc: 'a basic option', alias: ['f', 'foooo '] }); + options('b', { defaultDescription: 'a required option', demandOption: true, alias: 'bar' }); + }, + run: () => Promise.resolve() +}); +const barCommandMap = new Map(); +barCommandMap.set('default', { + name: 'default', + group: 'bar', + path: 'path/to/command', + global: false, + installed: true, + default: true, + description: 'default installed command for bar', + register: (options: OptionsHelper, helper: Helper) => { + options('foo', { describe: 'a basic option', alias: ['f', 'foooo '] }); + options('b', { defaultDescription: 'a required option', required: true, alias: 'bar' }); + }, + run: () => Promise.resolve() +}); +barCommandMap.set('other', { + name: 'other', + group: 'bar', + path: 'path/to/command', + global: true, + installed: true, + description: 'a another installed command for bar', + register: (options: OptionsHelper, helper: Helper) => { + options('foo', { defaultDescription: 'a basic option', alias: ['f', 'foooo '] }); + options('b', { defaultDescription: 'a required option', requiresArg: true, alias: 'bar' }); + }, + run: () => Promise.resolve() +}); +barCommandMap.set('non-default', { + name: 'non-default', + group: 'bar', + path: 'path/to/command', + global: false, + installed: true, + description: 'An installable command for bar', + register: (options: OptionsHelper, helper: Helper) => { + options('foo', { alias: ['f', 'foooo '] }); + options('b', { defaultDescription: 'a required option', demand: true, alias: 'bar' }); + options('c', { defaultDescription: 'a choices option', choices: ['one', 'two'], alias: 'choice' }); + }, + run: () => Promise.resolve() +}); +barCommandMap.set('installable', { + name: 'installable', + group: 'bar', + path: 'npm i @dojo/cli-bar-installable', + global: true, + installed: false, + description: 'An installable command for bar', + register: (options: OptionsHelper, helper: Helper) => {}, + run: () => Promise.resolve() +}); +groupMap.set('foo', fooCommandMap); +groupMap.set('bar', barCommandMap); + +const expectedMainHelp = `\n ..\n ';,,'..\n .colllc,. .''''''... .,,,. .. .,,,.\n ...',:loooc. ...''''''.. ;x' ;;' ':;,'.'',:;. :o. .:;,'..',;:.\n .'''. .... .',;'.. ..'. 'l' .c: :c. ,l' ,l. ,l' 'l;\n .,,,. ..;::,'.. ';. 'l' .l, ,l. :c. ,l. .l, 'l.\n .'..;;,. .,:cc;'... ,:' 'l' .l; :l. ,l. ,l. 'l. .l,\n ...;;;. .':clc;. .;:. 'l' ,l. 'l' c: ,l. .l; ;l.\n .:c:,..',:coooc,. .';;' 'l' .;c' 'c,. .c:. ,l. .c:. .;c.\n .:okOOkkxo:,.. ........',,,'. ,o:,,,,,,;'. ;;,,,,;; ,l. ';;,,,,;;'\n .''',;;,.. .... ........ ,l.\n ', .''. .c:\n .'.. .;c;. ..\n .. .\n\u001b[1mUsage:\u001b[22m\n\n $ \u001b[32mdojo\u001b[39m \u001b[32m\u001b[39m \u001b[2m\u001b[32m[]\u001b[39m\u001b[22m [] [--help]\n\n\u001b[1mGlobal Commands:\u001b[22m\n\n \u001b[32mfoo\u001b[39m \u001b[2m\u001b[32mglobal\u001b[39m\u001b[22m A global command\n \u001b[32mbar\u001b[39m \u001b[2m\u001b[32mother\u001b[39m\u001b[22m A another installed command for bar\n\n\u001b[1mProject Commands:\u001b[22m\n\n \u001b[32mfoo\u001b[39m \u001b[2m\u001b[32mproject\u001b[39m\u001b[22m A project command\n \u001b[32mbar\u001b[39m \u001b[2m\u001b[32mdefault\u001b[39m\u001b[22m Default installed command for bar\n \u001b[2m\u001b[32mnon-default\u001b[39m\u001b[22m An installable command for bar\n\n\u001b[1mInstallable Commands:\u001b[22m\n\n \u001b[32mbar\u001b[39m \u001b[2m\u001b[32minstallable\u001b[39m\u001b[22m An installable command for bar\n`; +const expectedFooGroupHelp = `\n ..\n ';,,'..\n .colllc,. .''''''... .,,,. .. .,,,.\n ...',:loooc. ...''''''.. ;x' ;;' ':;,'.'',:;. :o. .:;,'..',;:.\n .'''. .... .',;'.. ..'. 'l' .c: :c. ,l' ,l. ,l' 'l;\n .,,,. ..;::,'.. ';. 'l' .l, ,l. :c. ,l. .l, 'l.\n .'..;;,. .,:cc;'... ,:' 'l' .l; :l. ,l. ,l. 'l. .l,\n ...;;;. .':clc;. .;:. 'l' ,l. 'l' c: ,l. .l; ;l.\n .:c:,..',:coooc,. .';;' 'l' .;c' 'c,. .c:. ,l. .c:. .;c.\n .:okOOkkxo:,.. ........',,,'. ,o:,,,,,,;'. ;;,,,,;; ,l. ';;,,,,;;'\n .''',;;,.. .... ........ ,l.\n ', .''. .c:\n .'.. .;c;. ..\n .. .\n\u001b[1mUsage:\u001b[22m\n\n $ \u001b[32mdojo\u001b[39m \u001b[32mfoo\u001b[39m \u001b[2m\u001b[32m[]\u001b[39m\u001b[22m [] [--help]\n\n\u001b[1mCommands:\u001b[22m\n\n \u001b[32mfoo\u001b[39m \u001b[2m\u001b[32mglobal\u001b[39m\u001b[22m A global command (Default)\n \u001b[2m\u001b[32mproject\u001b[39m\u001b[22m A project command\n\n\u001b[1mDefault Command Options\u001b[22m\n\n --\u001b[32mfoo\u001b[39m A basic option\n --\u001b[32mbar\u001b[39m A required option [\u001b[33mrequired\u001b[39m]\n`; +const expectedBarGroupHelp = `\n ..\n ';,,'..\n .colllc,. .''''''... .,,,. .. .,,,.\n ...',:loooc. ...''''''.. ;x' ;;' ':;,'.'',:;. :o. .:;,'..',;:.\n .'''. .... .',;'.. ..'. 'l' .c: :c. ,l' ,l. ,l' 'l;\n .,,,. ..;::,'.. ';. 'l' .l, ,l. :c. ,l. .l, 'l.\n .'..;;,. .,:cc;'... ,:' 'l' .l; :l. ,l. ,l. 'l. .l,\n ...;;;. .':clc;. .;:. 'l' ,l. 'l' c: ,l. .l; ;l.\n .:c:,..',:coooc,. .';;' 'l' .;c' 'c,. .c:. ,l. .c:. .;c.\n .:okOOkkxo:,.. ........',,,'. ,o:,,,,,,;'. ;;,,,,;; ,l. ';;,,,,;;'\n .''',;;,.. .... ........ ,l.\n ', .''. .c:\n .'.. .;c;. ..\n .. .\n\u001b[1mUsage:\u001b[22m\n\n $ \u001b[32mdojo\u001b[39m \u001b[32mbar\u001b[39m \u001b[2m\u001b[32m[]\u001b[39m\u001b[22m [] [--help]\n\n\u001b[1mCommands:\u001b[22m\n\n \u001b[32mbar\u001b[39m \u001b[2m\u001b[32mdefault\u001b[39m\u001b[22m Default installed command for bar (Default)\n \u001b[2m\u001b[32mother\u001b[39m\u001b[22m A another installed command for bar\n \u001b[2m\u001b[32mnon-default\u001b[39m\u001b[22m An installable command for bar\n \u001b[2m\u001b[32minstallable\u001b[39m\u001b[22m An installable command for bar\n\n\u001b[1mDefault Command Options\u001b[22m\n\n -\u001b[32mf\u001b[39m, --\u001b[32mfoo\u001b[39m, --\u001b[32mfoooo \u001b[39m A basic option\n -\u001b[32mb\u001b[39m, --\u001b[32mbar\u001b[39m A required option [\u001b[33mrequired\u001b[39m]\n`; +const expectedFooGlobalHelp = `\n ..\n ';,,'..\n .colllc,. .''''''... .,,,. .. .,,,.\n ...',:loooc. ...''''''.. ;x' ;;' ':;,'.'',:;. :o. .:;,'..',;:.\n .'''. .... .',;'.. ..'. 'l' .c: :c. ,l' ,l. ,l' 'l;\n .,,,. ..;::,'.. ';. 'l' .l, ,l. :c. ,l. .l, 'l.\n .'..;;,. .,:cc;'... ,:' 'l' .l; :l. ,l. ,l. 'l. .l,\n ...;;;. .':clc;. .;:. 'l' ,l. 'l' c: ,l. .l; ;l.\n .:c:,..',:coooc,. .';;' 'l' .;c' 'c,. .c:. ,l. .c:. .;c.\n .:okOOkkxo:,.. ........',,,'. ,o:,,,,,,;'. ;;,,,,;; ,l. ';;,,,,;;'\n .''',;;,.. .... ........ ,l.\n ', .''. .c:\n .'.. .;c;. ..\n .. .\n\u001b[1mUsage:\u001b[22m\n\n $ \u001b[32mdojo\u001b[39m \u001b[32mfoo\u001b[39m \u001b[2m\u001b[32mglobal\u001b[39m\u001b[22m [] [--help]\n\n\u001b[1mDescription:\u001b[22m\n\n A global command\n\n\u001b[1mCommand Options:\u001b[22m\n\n --\u001b[32mfoo\u001b[39m A basic option\n --\u001b[32mbar\u001b[39m A required option [\u001b[33mrequired\u001b[39m]\n`; +const expectedFooProjectHelp = `\n ..\n ';,,'..\n .colllc,. .''''''... .,,,. .. .,,,.\n ...',:loooc. ...''''''.. ;x' ;;' ':;,'.'',:;. :o. .:;,'..',;:.\n .'''. .... .',;'.. ..'. 'l' .c: :c. ,l' ,l. ,l' 'l;\n .,,,. ..;::,'.. ';. 'l' .l, ,l. :c. ,l. .l, 'l.\n .'..;;,. .,:cc;'... ,:' 'l' .l; :l. ,l. ,l. 'l. .l,\n ...;;;. .':clc;. .;:. 'l' ,l. 'l' c: ,l. .l; ;l.\n .:c:,..',:coooc,. .';;' 'l' .;c' 'c,. .c:. ,l. .c:. .;c.\n .:okOOkkxo:,.. ........',,,'. ,o:,,,,,,;'. ;;,,,,;; ,l. ';;,,,,;;'\n .''',;;,.. .... ........ ,l.\n ', .''. .c:\n .'.. .;c;. ..\n .. .\n\u001b[1mUsage:\u001b[22m\n\n $ \u001b[32mdojo\u001b[39m \u001b[32mfoo\u001b[39m \u001b[2m\u001b[32mproject\u001b[39m\u001b[22m [] [--help]\n\n\u001b[1mDescription:\u001b[22m\n\n A project command\n\n\u001b[1mCommand Options:\u001b[22m\n\n -\u001b[32mf\u001b[39m, --\u001b[32mfoo\u001b[39m, --\u001b[32mfoooo \u001b[39m A basic option\n -\u001b[32mb\u001b[39m, --\u001b[32mbar\u001b[39m A required option [\u001b[33mrequired\u001b[39m]\n`; +const expectedBarInstallableHelp = `\n ..\n ';,,'..\n .colllc,. .''''''... .,,,. .. .,,,.\n ...',:loooc. ...''''''.. ;x' ;;' ':;,'.'',:;. :o. .:;,'..',;:.\n .'''. .... .',;'.. ..'. 'l' .c: :c. ,l' ,l. ,l' 'l;\n .,,,. ..;::,'.. ';. 'l' .l, ,l. :c. ,l. .l, 'l.\n .'..;;,. .,:cc;'... ,:' 'l' .l; :l. ,l. ,l. 'l. .l,\n ...;;;. .':clc;. .;:. 'l' ,l. 'l' c: ,l. .l; ;l.\n .:c:,..',:coooc,. .';;' 'l' .;c' 'c,. .c:. ,l. .c:. .;c.\n .:okOOkkxo:,.. ........',,,'. ,o:,,,,,,;'. ;;,,,,;; ,l. ';;,,,,;;'\n .''',;;,.. .... ........ ,l.\n ', .''. .c:\n .'.. .;c;. ..\n .. .\n\u001b[1mUsage:\u001b[22m\n\n $ \u001b[32mdojo\u001b[39m \u001b[32mbar\u001b[39m \u001b[2m\u001b[32minstallable\u001b[39m\u001b[22m [] [--help]\n\n\u001b[1mDescription:\u001b[22m\n\n An installable command for bar\n\n\u001b[1mCommand Options:\u001b[22m\n\n To install this command run \u001b[32mnpm i @dojo/cli-bar-installable\u001b[39m\n`; +const expectedBarNonDefaultHelp = `\n ..\n ';,,'..\n .colllc,. .''''''... .,,,. .. .,,,.\n ...',:loooc. ...''''''.. ;x' ;;' ':;,'.'',:;. :o. .:;,'..',;:.\n .'''. .... .',;'.. ..'. 'l' .c: :c. ,l' ,l. ,l' 'l;\n .,,,. ..;::,'.. ';. 'l' .l, ,l. :c. ,l. .l, 'l.\n .'..;;,. .,:cc;'... ,:' 'l' .l; :l. ,l. ,l. 'l. .l,\n ...;;;. .':clc;. .;:. 'l' ,l. 'l' c: ,l. .l; ;l.\n .:c:,..',:coooc,. .';;' 'l' .;c' 'c,. .c:. ,l. .c:. .;c.\n .:okOOkkxo:,.. ........',,,'. ,o:,,,,,,;'. ;;,,,,;; ,l. ';;,,,,;;'\n .''',;;,.. .... ........ ,l.\n ', .''. .c:\n .'.. .;c;. ..\n .. .\n\u001b[1mUsage:\u001b[22m\n\n $ \u001b[32mdojo\u001b[39m \u001b[32mbar\u001b[39m \u001b[2m\u001b[32mnon-default\u001b[39m\u001b[22m [] [--help]\n\n\u001b[1mDescription:\u001b[22m\n\n An installable command for bar\n\n\u001b[1mCommand Options:\u001b[22m\n\n -\u001b[32mf\u001b[39m, --\u001b[32mfoo\u001b[39m, --\u001b[32mfoooo \u001b[39m \n -\u001b[32mb\u001b[39m, --\u001b[32mbar\u001b[39m A required option [\u001b[33mrequired\u001b[39m]\n -\u001b[32mc\u001b[39m, --\u001b[32mchoice\u001b[39m A choices option [choices: \"\u001b[33mone\u001b[39m\", \"\u001b[33mtwo\u001b[39m\"]\n`; + +describe('help', () => { + it('should return formatted main help', () => { + const argv = { + _: [] + }; + const help = formatHelp(argv, groupMap); + assert.strictEqual(help, expectedMainHelp); + }); + + it('should return formatted group help', () => { + const fooHelp = formatHelp({ _: ['foo'] }, groupMap); + const barHelp = formatHelp({ _: ['bar'] }, groupMap); + assert.strictEqual(fooHelp, expectedFooGroupHelp); + assert.strictEqual(barHelp, expectedBarGroupHelp); + }); + + it('should return formatted command help', () => { + const fooGlobalHelp = formatHelp({ _: ['foo', 'global'] }, groupMap); + const fooProjectHelp = formatHelp({ _: ['foo', 'project'] }, groupMap); + const barInstallableHelp = formatHelp({ _: ['bar', 'installable'] }, groupMap); + const barNonDefaultHelp = formatHelp({ _: ['bar', 'non-default'] }, groupMap); + assert.strictEqual(fooGlobalHelp, expectedFooGlobalHelp); + assert.strictEqual(fooProjectHelp, expectedFooProjectHelp); + assert.strictEqual(barInstallableHelp, expectedBarInstallableHelp); + assert.strictEqual(barNonDefaultHelp, expectedBarNonDefaultHelp); + }); +}); diff --git a/tests/unit/installableCommands.ts b/tests/unit/installableCommands.ts index f3bbe76f..5f32096a 100644 --- a/tests/unit/installableCommands.ts +++ b/tests/unit/installableCommands.ts @@ -25,7 +25,7 @@ describe('installableCommands', () => { version: 'testVersion' }; const testCommandDetails2 = { - name: '@dojo/cli-test-foo', + name: '@dojo/cli-other-foo', description: 'testDescription2', version: 'testVersion2' }; @@ -98,42 +98,36 @@ describe('installableCommands', () => { }); it('creates installable command prompts', () => { - const { commandsMap, yargsCommandNames } = moduleUnderTest.createInstallableCommandPrompts([ + const groupMap = moduleUnderTest.mergeInstalledCommandsWithAvailableCommands(new Map(), [ testCommandDetails, testCommandDetails2 ]); - assert.equal(commandsMap.size, 3); - assert.equal(commandsMap.get('test').name, 'command'); - assert.equal(commandsMap.get('test').group, 'test'); - - assert.equal(yargsCommandNames.size, 1); - assert.isTrue(yargsCommandNames.has('test')); - assert.isTrue(yargsCommandNames.get('test').has('test-command')); - assert.isTrue(yargsCommandNames.get('test').has('test-foo')); + assert.equal(groupMap.size, 2); + + assert.equal(groupMap.get('test').get('command').name, 'command'); + assert.equal(groupMap.get('other').get('foo').name, 'foo'); }); - it('does not generate duplicate command promps', () => { - const { commandsMap, yargsCommandNames } = moduleUnderTest.createInstallableCommandPrompts([ + it('does not generate duplicate command prompts', () => { + const groupMap = moduleUnderTest.mergeInstalledCommandsWithAvailableCommands(new Map(), [ testCommandDetails, testCommandDetails ]); - assert.equal(commandsMap.size, 2); - assert.equal(yargsCommandNames.size, 1); + assert.equal(groupMap.size, 1); + assert.equal(groupMap.get('test').size, 1); }); it('does not create command prompts for ejected commands', () => { configHelperGetStub.returns({ ejected: true }); - const { commandsMap, yargsCommandNames } = moduleUnderTest.createInstallableCommandPrompts([ - testCommandDetails - ]); - assert.equal(commandsMap.size, 0); - assert.equal(yargsCommandNames.size, 0); + const groupMap = moduleUnderTest.mergeInstalledCommandsWithAvailableCommands(new Map(), [testCommandDetails]); + assert.equal(groupMap.size, 0); }); it('shows installation instructions for installable commands', () => { - const { commandsMap } = moduleUnderTest.createInstallableCommandPrompts([testCommandDetails]); - return commandsMap + const groupMap = moduleUnderTest.mergeInstalledCommandsWithAvailableCommands(new Map(), [testCommandDetails]); + return groupMap .get('test') + .get('command') .run() .then(() => { (console.log as sinon.SinonStub).calledWith( @@ -143,24 +137,15 @@ describe('installableCommands', () => { }); it('can merge installed commands with available commands', () => { - const commandsMap = new Map(); - const yargsCommandNames = new Map(); - commandsMap.set('installed', { name: 'installed-command' }); - yargsCommandNames.set('installed', new Set()); - const installedCommands = { - commandsMap, - yargsCommandNames - }; - const mergedCommands = moduleUnderTest.mergeInstalledCommandsWithAvailableCommands(installedCommands, [ - testCommandDetails - ]); - - assert.equal(mergedCommands.commandsMap.size, 3); - assert.isTrue(mergedCommands.commandsMap.has('test')); - assert.isTrue(mergedCommands.commandsMap.has('test-command')); - assert.isTrue(mergedCommands.commandsMap.has('installed')); - assert.equal(mergedCommands.yargsCommandNames.size, 2); - assert.isTrue(mergedCommands.yargsCommandNames.has('test')); - assert.isTrue(mergedCommands.yargsCommandNames.has('installed')); + const groupMap = new Map(); + const commandMap = new Map(); + groupMap.set('test', commandMap); + commandMap.set('installed', {}); + moduleUnderTest.mergeInstalledCommandsWithAvailableCommands(groupMap, [testCommandDetails]); + + assert.equal(groupMap.size, 1); + assert.equal(groupMap.get('test').size, 2); + assert.isTrue(groupMap.get('test').has('command')); + assert.isTrue(groupMap.get('test').has('installed')); }); }); diff --git a/tests/unit/loadCommands.ts b/tests/unit/loadCommands.ts index f4112acb..a5c68c12 100644 --- a/tests/unit/loadCommands.ts +++ b/tests/unit/loadCommands.ts @@ -91,12 +91,13 @@ registerSuite('loadCommands', { tests: { async 'Should set first loaded command of each group to be the default'() { const installedPaths = await enumInstalledCommands(goodConfig); - const { commandsMap } = await loadCommands(installedPaths, loadStub); + const groupMap = await loadCommands(installedPaths, loadStub); assert.isTrue(loadStub.calledTwice); - assert.equal(3, commandsMap.size); - assert.equal(commandWrapper1, commandsMap.get(commandWrapper1.group)); - assert.equal(commandWrapper1, commandsMap.get(`${commandWrapper1.group}-${commandWrapper1.name}`)); + assert.equal(groupMap.size, 1); + assert.equal(groupMap.get(commandWrapper1.group)!.size, 2); + const command = groupMap.get(commandWrapper1.group)!.get(commandWrapper1.name)!; + assert.isTrue(command.default); }, async 'should apply loading precedence to duplicate commands'() { const duplicateCommandName = 'command1'; @@ -106,13 +107,13 @@ registerSuite('loadCommands', { loadStub.onSecondCall().returns(commandWrapperDuplicate); const installedPaths = await enumInstalledCommands(goodConfig); - const { yargsCommandNames } = await loadCommands(installedPaths, loadStub); + const groupMap = await loadCommands(installedPaths, loadStub); assert.isTrue(loadStub.calledTwice); - const groupCommandSet = yargsCommandNames.get(duplicateGroupName); + const groupCommandSet = groupMap.get(duplicateGroupName); assert.equal(1, groupCommandSet!.size); - assert.isTrue(groupCommandSet!.has(`${duplicateGroupName}-${duplicateCommandName}`)); + assert.isTrue(groupCommandSet!.has(duplicateCommandName)); } } }, @@ -156,9 +157,9 @@ registerSuite('loadCommands', { goodConfig = config(); const installedPaths = await enumInstalledCommands(goodConfig); - const { commandsMap } = await mockedLoadCommands(installedPaths, loadStub); + const groupMap = await mockedLoadCommands(installedPaths, loadStub); - assert.equal(commandsMap.size, 0); + assert.equal(groupMap.size, 0); } } } diff --git a/tests/unit/registerCommands.ts b/tests/unit/registerCommands.ts index bdd78adb..34dce3b3 100644 --- a/tests/unit/registerCommands.ts +++ b/tests/unit/registerCommands.ts @@ -2,11 +2,8 @@ const { registerSuite } = intern.getInterface('object'); const { assert } = intern.getPlugin('chai'); import { stub, SinonStub } from 'sinon'; -import { getCommandsMap, getYargsStub, GroupDef } from '../support/testHelper'; +import { getYargsStub, GroupDef, getGroupMap } from '../support/testHelper'; import MockModule from '../support/MockModule'; - -import * as defaultCommandWrapper from '../support/test-prefix-foo-bar'; - const groupDef: GroupDef = [ { groupName: 'group1', @@ -19,30 +16,24 @@ const groupDef: GroupDef = [ ]; let mockModule: MockModule; -let commandsMap: any; -let yargsStub: any; -let defaultRegisterStub: SinonStub; -let defaultRunStub: SinonStub; +let groupMap: any; +let yargsStub: { + [index: string]: SinonStub; +}; let consoleErrorStub: SinonStub; +let consoleLogStub: SinonStub; let processExitStub: SinonStub; const errorMessage = 'test error message'; let registerCommands: any; -function createYargsCommandNames(obj: any): Map> { - const map = new Map(); - for (let key in obj) { - map.set(key, obj[key]); - } - return map; -} - registerSuite('registerCommands', { beforeEach() { mockModule = new MockModule('../../src/registerCommands', require); mockModule.dependencies(['./configurationHelper']); + mockModule.dependencies(['./help']); registerCommands = mockModule.getModuleUnderTest().default; yargsStub = getYargsStub(); - commandsMap = getCommandsMap(groupDef); + groupMap = getGroupMap(groupDef); processExitStub = stub(process, 'exit'); }, @@ -54,55 +45,74 @@ registerSuite('registerCommands', { tests: { 'Should setup correct yargs arguments'() { - const yargsArgs = ['demand', 'usage', 'epilog', 'help', 'strict']; - registerCommands(yargsStub, commandsMap, new Map()); + const yargsArgs = ['demand', 'help', 'strict', 'check', 'command']; + registerCommands(yargsStub, new Map()); yargsArgs.forEach((arg) => { assert.isTrue(yargsStub[arg].calledOnce); }); - assert.isTrue(yargsStub.alias.calledOnce, 'Should be called for help aliases'); }, 'Should call strict for all commands'() { - registerCommands( - yargsStub, - commandsMap, - createYargsCommandNames({ - group1: new Set(['group1-command1']), - group2: new Set(['group2-command1', 'group2-command2']) - }) - ); - assert.equal(yargsStub.strict.callCount, 4); + registerCommands(yargsStub, groupMap); + assert.equal(yargsStub.strict.callCount, 6); }, 'Should call yargs.command once for each yargsCommandName passed and once for the default command'() { - const key = 'group1-command1'; - const { group, description } = commandsMap.get(key); - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); - assert.isTrue(yargsStub.command.calledTwice); - assert.isTrue(yargsStub.command.firstCall.calledWith(group, description), 'First call is for parent'); - assert.isTrue(yargsStub.command.secondCall.calledWith('command1', key), 'Second call is sub-command'); + const { group } = groupMap.get('group1').get('command1'); + registerCommands(yargsStub, groupMap); + assert.strictEqual(yargsStub.command.callCount, 6); + assert.isTrue(yargsStub.command.getCall(0).calledWith(group, false), 'First call is for parent'); + assert.isTrue(yargsStub.command.getCall(1).calledWith('command1', false), 'Second call is sub-command'); }, 'Should run the passed command when yargs called with group name and command'() { - const key = 'group1-command1'; - const { run } = commandsMap.get(key); - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); - yargsStub.command.secondCall.args[3](); + const { run } = groupMap.get('group1').get('command1'); + registerCommands(yargsStub, groupMap); + yargsStub.command.secondCall.args[3]({}); assert.isTrue(run.calledOnce); }, 'Should call into register method'() { - const key = 'group1-command1'; - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); + registerCommands(yargsStub, groupMap); assert.isTrue(yargsStub.option.called); }, + help: { + beforeEach() { + registerCommands(yargsStub, groupMap); + consoleLogStub = stub(console, 'log'); + }, + + afterEach() { + consoleLogStub.restore(); + }, + tests: { + 'main help called'() { + const help = mockModule.getMock('./help').formatHelp; + help.reset(); + yargsStub.command.lastCall.args[3]({ _: [], h: true }); + assert.isTrue(help.calledOnce); + }, + 'group help called'() { + const help = mockModule.getMock('./help').formatHelp; + help.reset(); + yargsStub.command.firstCall.args[3]({ _: ['group'], h: true }); + assert.isTrue(help.calledOnce); + }, + 'command help called'() { + const help = mockModule.getMock('./help').formatHelp; + help.reset(); + yargsStub.command.secondCall.args[3]({ _: ['group', 'command'], h: true }); + assert.isTrue(help.calledOnce); + } + } + }, + 'command arguments': { 'pass dojo rc config as run arguments and expand to all aliases'() { - const key = 'group1-command1'; - commandsMap = getCommandsMap(groupDef, (compositeKey: string) => { + groupMap = getGroupMap(groupDef, (compositeKey: string) => { return (func: Function) => { func('foo', { alias: ['f', 'fo'] }); return compositeKey; }; }); - const { run } = commandsMap.get(key); + const { run } = groupMap.get('group1').get('command1'); const registerCommands = mockModule.getModuleUnderTest().default; const configurationHelper = mockModule.getMock('./configurationHelper'); configurationHelper.default = { @@ -115,16 +125,15 @@ registerSuite('registerCommands', { } }; - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); + registerCommands(yargsStub, groupMap); yargsStub.command.secondCall.args[3]({ f: undefined }); assert.isTrue(run.calledOnce); assert.deepEqual(run.firstCall.args[1], { foo: 'bar', f: 'bar', fo: 'bar' }); }, 'command line args should override dojo rc config'() { - const key = 'group1-command1'; process.argv = ['-foo']; - const { run } = commandsMap.get(key); + const { run } = groupMap.get('group1').get('command1'); const registerCommands = mockModule.getModuleUnderTest().default; const configurationHelper = mockModule.getMock('./configurationHelper'); configurationHelper.default = { @@ -137,21 +146,20 @@ registerSuite('registerCommands', { } }; - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); + registerCommands(yargsStub, groupMap); yargsStub.command.secondCall.args[3]({ foo: 'foo' }); assert.isTrue(run.calledOnce); assert.deepEqual(run.firstCall.args[1], { foo: 'foo' }); }, 'default command line args should not override dojo rc config'() { - const key = 'group1-command1'; - commandsMap = getCommandsMap(groupDef, (compositeKey: string) => { + groupMap = getGroupMap(groupDef, (compositeKey: string) => { return (func: Function) => { func('foo', { alias: ['f', 'fo'] }); return compositeKey; }; }); - const { run } = commandsMap.get(key); + const { run } = groupMap.get('group1').get('command1'); const registerCommands = mockModule.getModuleUnderTest().default; const configurationHelper = mockModule.getMock('./configurationHelper'); configurationHelper.default = { @@ -164,23 +172,22 @@ registerSuite('registerCommands', { } }; - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); + registerCommands(yargsStub, groupMap); yargsStub.command.secondCall.args[3]({ foo: 'foo', fo: 'foo', f: 'foo' }); assert.isTrue(run.calledOnce); assert.deepEqual(run.firstCall.args[1], { foo: 'bar', fo: 'bar', f: 'bar' }); }, 'command line options aliases should override dojo rc config'() { - const key = 'group1-command1'; process.argv = ['-f']; yargsStub = getYargsStub(); - commandsMap = getCommandsMap(groupDef, (compositeKey: string) => { + groupMap = getGroupMap(groupDef, (compositeKey: string) => { return (func: Function) => { func('foo', { alias: ['f'] }); return compositeKey; }; }); - const { run } = commandsMap.get(key); + const { run } = groupMap.get('group1').get('command1'); const registerCommands = mockModule.getModuleUnderTest().default; const configurationHelper = mockModule.getMock('./configurationHelper'); configurationHelper.default = { @@ -193,22 +200,21 @@ registerSuite('registerCommands', { } }; - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); + registerCommands(yargsStub, groupMap); yargsStub.command.secondCall.args[3]({ f: 'foo', foo: 'foo' }); assert.isTrue(run.calledOnce); assert.deepEqual(run.firstCall.args[1], { foo: 'foo', f: 'foo' }); }, 'should use rc config value for option aliases'() { - const key = 'group1-command1'; yargsStub = getYargsStub({ foo: ['f'], f: ['foo'] }); - commandsMap = getCommandsMap(groupDef, (compositeKey: string) => { + groupMap = getGroupMap(groupDef, (compositeKey: string) => { return (func: Function) => { func('foo', { alias: 'f' }); return compositeKey; }; }); - const { run } = commandsMap.get(key); + const { run } = groupMap.get('group1').get('command1'); const registerCommands = mockModule.getModuleUnderTest().default; const configurationHelper = mockModule.getMock('./configurationHelper'); configurationHelper.default = { @@ -221,16 +227,15 @@ registerSuite('registerCommands', { } }; - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); + registerCommands(yargsStub, groupMap); yargsStub.command.secondCall.args[3]({ f: 'foo', foo: 'foo' }); assert.isTrue(run.calledOnce); assert.deepEqual(run.firstCall.args[1], { foo: 'bar', f: 'bar' }); }, 'should use default command line arguments when not provided in config'() { - const key = 'group1-command1'; yargsStub = getYargsStub({ foo: ['f'], f: ['foo'] }); - const { run } = commandsMap.get(key); + const { run } = groupMap.get('group1').get('command1'); const registerCommands = mockModule.getModuleUnderTest().default; const configurationHelper = mockModule.getMock('./configurationHelper'); configurationHelper.default = { @@ -243,165 +248,68 @@ registerSuite('registerCommands', { } }; - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); + registerCommands(yargsStub, groupMap); yargsStub.command.secondCall.args[3]({ f: 'foo', foo: 'foo' }); assert.isTrue(run.calledOnce); assert.deepEqual(run.firstCall.args[1], { foo: 'foo', f: 'foo' }); } }, - alias: { - beforeEach() { - const command = commandsMap.get('group1-command1'); - command.alias = { - name: 'alias', - description: 'some description', - options: [ - { - option: 'w', - value: 10 - } - ] - }; - }, - - tests: { - 'should register add itself as a command'() { - registerCommands( - yargsStub, - commandsMap, - createYargsCommandNames({ group1: new Set(['group1-command1']) }) - ); - assert.equal(yargsStub.command.thirdCall.args[0], 'alias'); - assert.equal(yargsStub.command.thirdCall.args[1], 'some description'); - }, - 'should register options'() { - registerCommands( - yargsStub, - commandsMap, - createYargsCommandNames({ group1: new Set(['group1-command1']) }) - ); - assert.isTrue(yargsStub.option.calledTwice); - }, - 'should not register provided options'() { - const key = 'group1-command1'; - const command = commandsMap.get(key); - (command.register = stub() - .callsArgWith(0, 'w', {}) - .returns(key)), - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); - assert.isTrue(yargsStub.option.calledOnce); - }, - 'should register when alias is an array'() { - const key = 'group1-command1'; - const command = commandsMap.get(key); - command.alias = [ - { - name: 'alias', - options: [ - { - option: 'w', - value: 10 - } - ] - } - ]; - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); - assert.isTrue(yargsStub.option.calledTwice); - }, - 'should augment argv when run'() { - const key = 'group1-command1'; - const command = commandsMap.get(key); - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); - yargsStub.command.thirdCall.args[3]({ _: ['group', 'command'] }); - assert.equal(command.run.firstCall.args[1].w, 10); - }, - 'should run without options'() { - const key = 'group1-command1'; - const command = commandsMap.get(key); - command.alias = [ - { - name: 'alias' - } - ]; - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); - yargsStub.command.thirdCall.args[3]({ _: ['group', 'command'] }); - const properties = Object.keys(command.run.firstCall.args[1]); - assert.equal(properties.length, 1); - ['group', 'command'].forEach((key) => { - assert.notEqual(command.run.firstCall.args[1]._.indexOf(key), -1); - }); - } - } - }, 'default command': { beforeEach() { - const key = 'group1-command1'; - defaultRegisterStub = stub(defaultCommandWrapper, 'register') - .callsArgWith(0, 'key', {}) - .returns(key); - defaultRunStub = stub(defaultCommandWrapper, 'run').returns(Promise.resolve()); - commandsMap.set('group1', defaultCommandWrapper); - registerCommands(yargsStub, commandsMap, createYargsCommandNames({ group1: new Set([key]) })); + groupMap = getGroupMap(groupDef); + registerCommands(yargsStub, groupMap); + consoleErrorStub = stub(console, 'error'); }, + afterEach() { - defaultRegisterStub.restore(); - defaultRunStub.restore(); + consoleErrorStub.restore(); }, tests: { 'Should register the default command'() { - assert.isTrue(defaultRegisterStub.calledOnce); + const { register } = groupMap.get('group1').get('command1'); + assert.isTrue(register.calledTwice); }, 'Should run default command when yargs called with only group name'() { + const { run } = groupMap.get('group1').get('command1'); yargsStub.command.firstCall.args[3]({ _: ['group'] }); - assert.isTrue(defaultRunStub.calledOnce); + assert.isTrue(run.calledOnce); }, 'Should not run default command when yargs called with group name and command'() { + const { run } = groupMap.get('group1').get('command1'); yargsStub.command.firstCall.args[3]({ _: ['group', 'command'] }); - assert.isFalse(defaultRunStub.called); - }, - 'error message': { - beforeEach() { - consoleErrorStub = stub(console, 'error'); - defaultRunStub.returns(Promise.reject(new Error(errorMessage))); - }, - afterEach() { - consoleErrorStub.restore(); - }, - - tests: { - async 'Should show error message if the run command rejects'() { - await yargsStub.command.firstCall.args[3]({ _: ['group'] }); - assert.isTrue(consoleErrorStub.calledOnce); - assert.isTrue(consoleErrorStub.firstCall.calledWithMatch(errorMessage)); - assert.isTrue(processExitStub.called); - } + assert.isFalse(run.calledOnce); + } + } + }, + 'handling errors': { + beforeEach() { + groupMap = getGroupMap([ + { + groupName: 'group1', + commands: [{ commandName: 'command1', fails: true }] } - }, - 'status codes call process exit': (function() { - return { - tests: { - async 'Should exit process with exitCode of 1 when no exitCode is returned'() { - defaultRunStub.returns(Promise.reject(new Error(errorMessage))); + ]); + registerCommands(yargsStub, groupMap); + consoleErrorStub = stub(console, 'error'); + }, - await yargsStub.command.firstCall.args[3]({ _: ['group'] }); - assert.isTrue(processExitStub.calledOnce); - assert.isTrue(processExitStub.calledWith(1)); - }, - async 'Should exit process if status code is returned'() { - defaultRunStub.returns( - Promise.reject({ - message: errorMessage, - exitCode: 1 - }) - ); + afterEach() { + consoleErrorStub.restore(); + }, - await yargsStub.command.firstCall.args[3]({ _: ['group'] }); - assert.isTrue(processExitStub.called); - } - } - }; - })() + tests: { + async 'Should show error message if the run command rejects'() { + await yargsStub.command.firstCall.args[3]({ _: ['group'] }); + assert.isTrue(consoleErrorStub.calledOnce); + assert.isTrue(consoleErrorStub.firstCall.calledWithMatch(errorMessage)); + assert.isTrue(processExitStub.called); + }, + async 'Should exit process with exitCode of 1 when no exitCode is returned'() { + await yargsStub.command.firstCall.args[3]({ _: ['group'] }); + assert.isTrue(processExitStub.calledOnce); + assert.isTrue(processExitStub.calledWith(1)); + } } } } diff --git a/tests/unit/text.ts b/tests/unit/text.ts deleted file mode 100644 index 8181d1bb..00000000 --- a/tests/unit/text.ts +++ /dev/null @@ -1,19 +0,0 @@ -const { registerSuite } = intern.getInterface('object'); -const { assert } = intern.getPlugin('chai'); - -import * as text from '../../src/text'; - -registerSuite('text', { - exports: (function(exportedStrings) { - const tests: { [key: string]: () => void } = {}; - - exportedStrings.forEach(function(exportedString) { - tests[exportedString] = () => { - assert.isNotNull((text)[exportedString]); - assert.isString((text)[exportedString]); - }; - }); - - return tests; - })(['helpUsage', 'helpEpilog']) -}); diff --git a/tests/unit/validation.ts b/tests/unit/validation.ts new file mode 100644 index 00000000..c0564791 --- /dev/null +++ b/tests/unit/validation.ts @@ -0,0 +1,111 @@ +const { describe, it } = intern.getInterface('bdd'); +const { assert } = intern.getPlugin('chai'); + +import { createOptionValidator, isRequiredOption } from './../../src/validation'; +import { CommandWrapper, OptionsHelper, Helper } from '../../src/interfaces'; + +const groupMap = new Map(); +const fooCommandMap = new Map(); +const barCommandMap = new Map(); + +fooCommandMap.set('global', { + name: 'global', + group: 'foo', + path: 'path/to/command', + global: true, + installed: true, + description: 'a global command', + default: true, + register: (options: OptionsHelper, helper: Helper) => { + options('foo', { description: 'a basic option' }); + options('bar', { defaultDescription: 'a required option', require: true }); + }, + run: () => Promise.resolve() +}); +fooCommandMap.set('project', { + name: 'project', + group: 'foo', + path: 'path/to/command', + global: false, + installed: true, + description: 'a project command', + default: false, + register: (options: OptionsHelper, helper: Helper) => { + options('foo', { description: 'a basic option', require: true }); + options('bar', { defaultDescription: 'a required option', require: true }); + }, + run: () => Promise.resolve() +}); +barCommandMap.set('default', { + name: 'default', + group: 'bar', + path: 'path/to/command', + global: false, + installed: true, + default: true, + description: 'default installed command for bar', + register: (options: OptionsHelper, helper: Helper) => { + options('foo', { describe: 'a basic option', alias: ['f', 'foooo '] }); + options('b', { defaultDescription: 'a required option', alias: 'bar' }); + }, + run: () => Promise.resolve() +}); + +groupMap.set('foo', fooCommandMap); +groupMap.set('bar', barCommandMap); + +const optionValidator = createOptionValidator(groupMap); + +describe('validation', () => { + describe('optionValidator', () => { + it('Should return validation error for required option', () => { + try { + optionValidator({ _: ['foo', 'global'] }); + assert.fail('Should throw a validation error'); + } catch (err) { + assert.strictEqual( + err.message, + "\n\u001b[1m\u001b[31mError(s):\u001b[39m\u001b[22m\n Required option '\u001b[91mbar\u001b[39m' not provided" + ); + } + }); + + it('Should return validation error for multiple options', () => { + try { + optionValidator({ _: ['foo', 'project'] }); + assert.fail('Should throw a validation error'); + } catch (err) { + assert.strictEqual( + err.message, + "\n\u001b[1m\u001b[31mError(s):\u001b[39m\u001b[22m\n Required option '\u001b[91mfoo\u001b[39m' not provided\n Required option '\u001b[91mbar\u001b[39m' not provided" + ); + } + }); + + it('Should not return validation error for when option provided', () => { + assert.isTrue(optionValidator({ _: ['foo', 'global'], foo: 'bar', bar: 'foo' })); + }); + + it('Should not return validation error for no required options', () => { + assert.isTrue(optionValidator({ _: ['bar', 'default'] })); + }); + + it('Should not return validation error when help option provided', () => { + assert.isTrue(optionValidator({ _: ['foo', 'global'], h: true })); + assert.isTrue(optionValidator({ _: ['foo', 'global'], help: true })); + }); + }); + describe('isRequiredOption', () => { + it('for required option', () => { + assert.isTrue(isRequiredOption({ require: true })); + assert.isTrue(isRequiredOption({ required: true })); + assert.isTrue(isRequiredOption({ requiresArg: true })); + assert.isTrue(isRequiredOption({ demand: true })); + assert.isTrue(isRequiredOption({ demandOption: true })); + }); + + it('for not required option', () => { + assert.isFalse(isRequiredOption({})); + }); + }); +});