diff --git a/package-lock.json b/package-lock.json index f1cfafef140..e115d96b136 100644 --- a/package-lock.json +++ b/package-lock.json @@ -468,6 +468,27 @@ "netlify-cli-logo": "^1.0.0", "netlify-redirect-parser": "^1.0.3", "netlify-redirector": "^0.0.4" + }, + "dependencies": { + "chokidar": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", + "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + } } }, "@netlify/zip-it-and-ship-it": { @@ -487,13 +508,23 @@ "read-pkg-up": "^4.0.0", "require-package-name": "^2.0.1", "resolve": "^1.10.0" + }, + "dependencies": { + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + } } }, "@nodelib/fs.scandir": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.1.tgz", "integrity": "sha512-NT/skIZjgotDSiXs0WqYhgcuBKhUMgfekCmCGtkUAiLqZdOnrdjmZr9wRl3ll64J9NF79uZ4fk16Dx0yMc/Xbg==", - "dev": true, "requires": { "@nodelib/fs.stat": "2.0.1", "run-parallel": "^1.1.9" @@ -502,14 +533,12 @@ "@nodelib/fs.stat": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.1.tgz", - "integrity": "sha512-+RqhBlLn6YRBGOIoVYthsG0J9dfpO79eJyN7BYBkZJtfqrBwf2KK+rD/M/yjZR6WBmIhAgOV7S60eCgaSWtbFw==", - "dev": true + "integrity": "sha512-+RqhBlLn6YRBGOIoVYthsG0J9dfpO79eJyN7BYBkZJtfqrBwf2KK+rD/M/yjZR6WBmIhAgOV7S60eCgaSWtbFw==" }, "@nodelib/fs.walk": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.2.tgz", "integrity": "sha512-J/DR3+W12uCzAJkw7niXDcqcKBg6+5G5Q/ZpThpGNzAUz70eOR6RV4XnnSN01qHZiVl0eavoxJsBypQoKsV2QQ==", - "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.1", "fastq": "^1.6.0" @@ -566,6 +595,19 @@ "normalize-package-data": "^2.5.0", "qqjs": "^0.3.10", "tslib": "^1.9.3" + }, + "dependencies": { + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } } }, "@oclif/errors": { @@ -578,6 +620,43 @@ "indent-string": "^3.2.0", "strip-ansi": "^5.0.0", "wrap-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "wrap-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-4.0.0.tgz", + "integrity": "sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + } } }, "@oclif/linewrap": { @@ -586,9 +665,9 @@ "integrity": "sha512-Ups2dShK52xXa8w6iBWLgcjPJWjais6KPJQq3gQ/88AY6BXoTX+MIGFPrWQO1KLMiQfoTpcLnUwloN4brrVUHw==" }, "@oclif/parser": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/@oclif/parser/-/parser-3.8.3.tgz", - "integrity": "sha512-zN+3oGuv9Lg8NjFvxZTDKFEmhAMfAvd/JWeQp3Ri8pDezoyJQi4OSHHLM8sdHjBh8sePewfWI7+fDUXdrVbrqg==", + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@oclif/parser/-/parser-3.8.4.tgz", + "integrity": "sha512-cyP1at3l42kQHZtqDS3KfTeyMvxITGwXwH1qk9ktBYvqgMp5h4vHT+cOD74ld3RqJUOZY/+Zi9lb4Tbza3BtuA==", "requires": { "@oclif/linewrap": "^1.0.0", "chalk": "^2.4.2", @@ -610,6 +689,11 @@ "wrap-ansi": "^4.0.0" }, "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -619,6 +703,35 @@ "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } + }, + "wrap-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-4.0.0.tgz", + "integrity": "sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } } } }, @@ -665,6 +778,18 @@ "supports-hyperlinks": "^1.0.1", "treeify": "^1.1.0", "tslib": "^1.9.3" + }, + "dependencies": { + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } } } } @@ -688,6 +813,16 @@ "yarn": "^1.15.0" }, "dependencies": { + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "load-json-file": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", @@ -712,6 +847,14 @@ "resolved": "https://registry.npmjs.org/@oclif/screen/-/screen-1.0.4.tgz", "integrity": "sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw==" }, + "@oclif/test": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@oclif/test/-/test-1.2.5.tgz", + "integrity": "sha512-8Y+Ix4A3Zhm87aL0ldVonDK7vFWyLfnFHzP3goYaLyIeh/60KL37lMxfmbp/kBN6/Y0Ru17iR1pdDi/hTDClLQ==", + "requires": { + "fancy-test": "^1.4.3" + } + }, "@octokit/endpoint": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.3.2.tgz", @@ -766,11 +909,24 @@ "url-template": "^2.0.8" } }, + "@samverschueren/stream-to-observable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", + "integrity": "sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==", + "requires": { + "any-observable": "^0.3.0" + } + }, "@sindresorhus/is": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==" }, + "@types/chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-zw8UvoBEImn392tLjxoavuonblX/4Yb9ha4KBU10FirCfwgzhKO0dvyJSF9ByxV1xK1r2AgnAi/tvQaLgxQqxA==" + }, "@types/decompress": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.3.tgz", @@ -792,14 +948,12 @@ "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" }, "@types/glob": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "dev": true, "requires": { "@types/events": "*", "@types/minimatch": "*", @@ -820,11 +974,15 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/lodash": { + "version": "4.14.136", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.136.tgz", + "integrity": "sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA==" + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" }, "@types/mkdirp": { "version": "0.5.2", @@ -834,6 +992,19 @@ "@types/node": "*" } }, + "@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==" + }, + "@types/nock": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.3.tgz", + "integrity": "sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow==", + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "12.6.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.8.tgz", @@ -857,6 +1028,11 @@ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==" }, + "@types/sinon": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.13.tgz", + "integrity": "sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung==" + }, "@typescript-eslint/typescript-estree": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz", @@ -959,6 +1135,11 @@ } } }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==" + }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -997,6 +1178,11 @@ "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=" }, + "any-observable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", + "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==" + }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -1104,7 +1290,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -1172,8 +1357,7 @@ "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" }, "array-uniq": { "version": "2.1.0", @@ -1218,6 +1402,11 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -1402,6 +1591,26 @@ } } }, + "chokidar": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", + "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, "clean-stack": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.1.0.tgz", @@ -1729,28 +1938,56 @@ } }, "boxen": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-3.2.0.tgz", - "integrity": "sha512-cU4J/+NodM3IHdSL2yN8bqYqnmlBTidDR4RC7nJs61ZmtGz8VZzM3HLQX0zY5mrSmPtR3xWwsq2jOUQqFZN8+A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.1.0.tgz", + "integrity": "sha512-Iwq1qOkmEsl0EVABa864Bbj3HCL4186DRZgFW/NrFs5y5GMM3ljsxzMLgOHdWISDRvcM8beh8q4tTNzXz+mSKg==", "requires": { "ansi-align": "^3.0.0", "camelcase": "^5.3.1", "chalk": "^2.4.2", "cli-boxes": "^2.2.0", - "string-width": "^3.0.0", - "term-size": "^1.2.0", - "type-fest": "^0.3.0", - "widest-line": "^2.0.0" + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.5.2", + "widest-line": "^3.1.0" }, "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, "string-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz", + "integrity": "sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^5.2.0" + } + }, + "term-size": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.1.0.tgz", + "integrity": "sha512-I42EWhJ+2aeNQawGx1VtpO0DFI9YcfuvAMNIdKyf/6sRbHJ4P+ZQ/zIT87tE+ln1ymAGcCJds4dolfSAS0AcNg==" + }, + "type-fest": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", + "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==" + }, + "widest-line": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "string-width": "^4.0.0" } } } @@ -1796,6 +2033,11 @@ } } }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, "btoa-lite": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", @@ -1943,6 +2185,29 @@ "integrity": "sha1-qEq8glpV70yysCi9dOIFpluaSZY=", "dev": true }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "requires": { + "callsites": "^2.0.0" + }, + "dependencies": { + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" + } + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "requires": { + "caller-callsite": "^2.0.0" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2004,6 +2269,19 @@ "url-to-options": "^1.0.1" } }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -2029,23 +2307,99 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" + }, "chokidar": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", - "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.0.2.tgz", + "integrity": "sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA==", + "requires": { + "anymatch": "^3.0.1", + "braces": "^3.0.2", + "fsevents": "^2.0.6", + "glob-parent": "^5.0.0", + "is-binary-path": "^2.1.0", + "is-glob": "^4.0.1", "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" + "readdirp": "^3.1.1" + }, + "dependencies": { + "anymatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.0.3.tgz", + "integrity": "sha512-c6IvoeBECQlMVuYUjSwimnhmztImpErfxJzWZhIQinIvQWoGOnB0dLIgifbPHQt5heS6mNlaZG16f06H3C8t1g==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.0.7.tgz", + "integrity": "sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ==", + "optional": true + }, + "glob-parent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", + "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "readdirp": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.1.1.tgz", + "integrity": "sha512-XXdSXZrQuvqoETj50+JAitxz1UPdt5dupjT6T5nVB+WvjMv2XKYj+s7hPeAVCXvmJrL36O4YYyWlIC3an2ePiQ==", + "requires": { + "picomatch": "^2.0.4" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + } } }, "chownr": { @@ -2107,6 +2461,14 @@ "lodash.transform": "^4.6.0" } }, + "clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha1-jffHquUf02h06PjQW5GAvBGj/tc=", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, "clean-stack": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-1.3.0.tgz", @@ -2182,6 +2544,16 @@ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.1.0.tgz", "integrity": "sha512-uQWrpRm+iZZUCAp7ZZJQbd4Za9I3AjR/3YTjmcnAtkauaIm/T5CT6U8zVI6e60T6OANqBFAzuR9/HB3NzuZCRA==" }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -2204,18 +2576,85 @@ "resolved": "https://registry.npmjs.org/cliclopts/-/cliclopts-1.1.1.tgz", "integrity": "sha1-aUMcfLWvcjd0sNORG0w3USQxkQ8=" }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", "requires": { - "mimic-response": "^1.0.0" - } + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + } + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } }, "code-excerpt": { "version": "2.1.1", @@ -2229,8 +2668,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "coffee-script": { "version": "1.12.7", @@ -2489,9 +2927,9 @@ } }, "core-js": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.1.4.tgz", - "integrity": "sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz", + "integrity": "sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==", "dev": true }, "core-util-is": { @@ -2499,6 +2937,28 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "dependencies": { + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + } + } + }, "coveralls": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.5.tgz", @@ -2594,6 +3054,11 @@ "assert-plus": "^1.0.0" } }, + "date-fns": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", + "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==" + }, "date-time": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/date-time/-/date-time-2.1.0.tgz", @@ -2613,8 +3078,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decamelize-keys": { "version": "1.1.0", @@ -2791,11 +3255,23 @@ } } }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=" + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" }, "deep-extend": { "version": "0.6.0", @@ -3074,6 +3550,65 @@ "wrap-ansi": "5.1.0" }, "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "inquirer": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.0.tgz", + "integrity": "sha512-scfHejeG/lVZSpvCXpsB4j/wQNPM5JC8kiElOI0OUTwmc1RTpXr4H32/HOlQHcZiYl2z2VElwuCVDRG8vFmbnA==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + } + } + }, + "resolve": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", + "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, "semver": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.2.0.tgz", @@ -3221,11 +3756,15 @@ "integrity": "sha1-bfwP+dAQAKLt8oZTccrDFulJd68=", "dev": true }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "requires": { "path-type": "^4.0.0" }, @@ -3233,8 +3772,7 @@ "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" } } }, @@ -3298,7 +3836,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { @@ -3310,7 +3848,7 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true } @@ -3336,6 +3874,11 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "elegant-spinner": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", + "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=" + }, "elf-tools": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/elf-tools/-/elf-tools-1.1.1.tgz", @@ -3557,6 +4100,46 @@ } } }, + "eslint-ast-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-ast-utils/-/eslint-ast-utils-1.1.0.tgz", + "integrity": "sha512-otzzTim2/1+lVrlH19EfQQJEhVJSu0zOb9ygb3iapN6UlyaDtyRq4b5U1FuW0v1lRa9Fp/GJyHkSwm6NqABgCA==", + "requires": { + "lodash.get": "^4.4.2", + "lodash.zip": "^4.2.0" + } + }, + "eslint-config-oclif": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-oclif/-/eslint-config-oclif-3.1.0.tgz", + "integrity": "sha512-Tqgy43cNXsSdhTLWW4RuDYGFhV240sC4ISSv/ZiUEg/zFxExSEUpRE6J+AGnkKY9dYwIW4C9b2YSUVv8z/miMA==", + "requires": { + "eslint-config-xo-space": "^0.20.0", + "eslint-plugin-mocha": "^5.2.0", + "eslint-plugin-node": "^7.0.1", + "eslint-plugin-unicorn": "^6.0.1" + }, + "dependencies": { + "eslint-plugin-node": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-7.0.1.tgz", + "integrity": "sha512-lfVw3TEqThwq0j2Ba/Ckn2ABdwmL5dkOgAux1rvOk6CO7A6yGyPI2+zIxN6FyNkp1X1X/BSvKOceD6mBWSj4Yw==", + "requires": { + "eslint-plugin-es": "^1.3.1", + "eslint-utils": "^1.3.1", + "ignore": "^4.0.2", + "minimatch": "^3.0.4", + "resolve": "^1.8.1", + "semver": "^5.5.0" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" + } + } + }, "eslint-config-prettier": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-4.3.0.tgz", @@ -3566,6 +4149,19 @@ "get-stdin": "^6.0.0" } }, + "eslint-config-xo": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/eslint-config-xo/-/eslint-config-xo-0.24.2.tgz", + "integrity": "sha512-ivQ7qISScW6gfBp+p31nQntz1rg34UCybd3uvlngcxt5Utsf4PMMi9QoAluLFcPUM5Tvqk4JGraR9qu3msKPKQ==" + }, + "eslint-config-xo-space": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/eslint-config-xo-space/-/eslint-config-xo-space-0.20.0.tgz", + "integrity": "sha512-bOsoZA8M6v1HviDUIGVq1fLVnSu3mMZzn85m2tqKb73tSzu4GKD4Jd2Py4ZKjCgvCbRRByEB5HPC3fTMnnJ1uw==", + "requires": { + "eslint-config-xo": "^0.24.0" + } + }, "eslint-import-resolver-node": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", @@ -3676,7 +4272,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz", "integrity": "sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw==", - "dev": true, "requires": { "eslint-utils": "^1.3.0", "regexpp": "^2.0.1" @@ -3828,6 +4423,14 @@ } } }, + "eslint-plugin-mocha": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-5.3.0.tgz", + "integrity": "sha512-3uwlJVLijjEmBeNyH60nzqgA1gacUWLUmcKV8PIGNvj1kwP/CTgAWQHn2ayyJVwziX+KETkr9opNwT1qD/RZ5A==", + "requires": { + "ramda": "^0.26.1" + } + }, "eslint-plugin-node": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-8.0.1.tgz", @@ -3851,6 +4454,21 @@ "prettier-linter-helpers": "^1.0.0" } }, + "eslint-plugin-unicorn": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-6.0.1.tgz", + "integrity": "sha512-hjy9LhTdtL7pz8WTrzS0CGXRkWK3VAPLDjihofj8JC+uxQLfXm0WwZPPPB7xKmcjRyoH+jruPHOCrHNEINpG/Q==", + "requires": { + "clean-regexp": "^1.0.0", + "eslint-ast-utils": "^1.0.0", + "import-modules": "^1.1.0", + "lodash.camelcase": "^4.1.1", + "lodash.kebabcase": "^4.0.1", + "lodash.snakecase": "^4.0.1", + "lodash.upperfirst": "^4.2.0", + "safe-regex": "^1.1.0" + } + }, "eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", @@ -3865,7 +4483,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.0.tgz", "integrity": "sha512-7ehnzPaP5IIEh1r1tkjuIrxqhNkzUJa9z3R92tLJdZIVdWaczEhr3EbhGtsMrVxi1KeR8qA7Off6SWc5WNQqyQ==", - "dev": true, "requires": { "eslint-visitor-keys": "^1.0.0" } @@ -3873,8 +4490,7 @@ "eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", - "dev": true + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==" }, "esm": { "version": "3.2.25", @@ -3974,26 +4590,43 @@ "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-2.0.3.tgz", + "integrity": "sha512-iM124nlyGSrXmuyZF1EMe83ESY2chIYVyDRZKgmcDynid2Q2v/+GuE7gNMl6Sy9Niwf4MC0DDxagOxeMPjuLsw==", "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" + "cross-spawn": "^6.0.5", + "get-stream": "^5.0.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^3.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" }, "dependencies": { - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", "requires": { - "path-key": "^2.0.0" + "mimic-fn": "^2.1.0" } + }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==" } } }, @@ -4293,6 +4926,22 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", "dev": true }, + "fancy-test": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/fancy-test/-/fancy-test-1.4.4.tgz", + "integrity": "sha512-F2JYBLJTsfvqjziAl/niwxnWYJy+JCIyDMbbBJqT7XzF8JwEIOL3/TC99v3Ig5LFXkvuwKrKpetSymd6CjH8ew==", + "requires": { + "@types/chai": "*", + "@types/lodash": "*", + "@types/mocha": "*", + "@types/nock": "*", + "@types/node": "*", + "@types/sinon": "*", + "lodash": "^4.17.13", + "mock-stdin": "^0.3.1", + "stdout-stderr": "^0.1.9" + } + }, "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", @@ -4308,7 +4957,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.0.4.tgz", "integrity": "sha512-wkIbV6qg37xTJwqSsdnIphL1e+LaGz4AIQqr00mIubMaEhv1/HEmJ0uuCGZRNRUkZZmOB5mJKO0ZUTVq+SxMQg==", - "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.1", "@nodelib/fs.walk": "^1.2.1", @@ -4322,7 +4970,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -4331,7 +4978,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -4340,7 +4986,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -4348,14 +4993,12 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, "requires": { "braces": "^3.0.1", "picomatch": "^2.0.5" @@ -4365,7 +5008,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "requires": { "is-number": "^7.0.0" } @@ -4387,7 +5029,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz", "integrity": "sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==", - "dev": true, "requires": { "reusify": "^1.0.0" } @@ -4500,6 +5141,21 @@ "locate-path": "^3.0.0" } }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "requires": { + "is-buffer": "~2.0.3" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + } + } + }, "flat-cache": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", @@ -4645,11 +5301,11 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "requires": { - "graceful-fs": "^4.1.2", + "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } @@ -5181,7 +5837,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -5199,6 +5855,21 @@ "node-source-walk": "^4.0.0" } }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" + }, + "get-own-enumerable-property-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz", + "integrity": "sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg==" + }, "get-port": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.0.0.tgz", @@ -5222,9 +5893,9 @@ "dev": true }, "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", "requires": { "pump": "^3.0.0" } @@ -5277,7 +5948,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -5299,7 +5970,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -5329,7 +6000,7 @@ "dependencies": { "async": { "version": "0.9.2", - "resolved": "http://registry.npmjs.org/async/-/async-0.9.2.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", "dev": true } @@ -5366,7 +6037,7 @@ "dependencies": { "bl": { "version": "1.1.2", - "resolved": "http://registry.npmjs.org/bl/-/bl-1.1.2.tgz", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", "integrity": "sha1-/cqHGplxOqANGeO7ukHER4emU5g=", "dev": true, "requires": { @@ -5381,7 +6052,7 @@ }, "readable-stream": { "version": "2.0.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", "dev": true, "requires": { @@ -5395,7 +6066,7 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true } @@ -5512,7 +6183,6 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", - "dev": true, "requires": { "@types/glob": "^7.1.1", "array-union": "^2.1.0", @@ -5607,8 +6277,7 @@ "growl": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" }, "gulp-header": { "version": "1.8.12", @@ -5745,6 +6414,11 @@ "is-stream": "^1.0.1" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, "hosted-git-info": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", @@ -5851,6 +6525,96 @@ "sshpk": "^1.7.0" } }, + "husky": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.3.tgz", + "integrity": "sha512-DBBMPSiBYEMx7EVUTRE/ymXJa/lOL+WplcsV/lZu+/HHGt0gzD+5BIz9EJnCrWyUa7hkMuBh7/9OZ04qDkM+Nw==", + "requires": { + "chalk": "^2.4.2", + "cosmiconfig": "^5.2.1", + "execa": "^1.0.0", + "get-stdin": "^7.0.0", + "is-ci": "^2.0.0", + "opencollective-postinstall": "^2.0.2", + "pkg-dir": "^4.2.0", + "please-upgrade-node": "^3.2.0", + "read-pkg": "^5.1.1", + "run-node": "^1.0.0", + "slash": "^3.0.0" + }, + "dependencies": { + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stdin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", + "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==" + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "requires": { + "ci-info": "^2.0.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "parse-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", + "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + } + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + } + } + }, "hyperlinker": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz", @@ -5874,7 +6638,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -5886,13 +6650,13 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true }, "through2": { "version": "0.6.5", - "resolved": "http://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", "dev": true, "requires": { @@ -5918,8 +6682,7 @@ "ignore": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.2.tgz", - "integrity": "sha512-vdqWBp7MyzdmHkkRWV5nY+PfGRbYbahfuvsBCh277tq+w9zyNi7h5CYJCK0kmzti9kU+O/cB7sE8HvKv6aXAKQ==", - "dev": true + "integrity": "sha512-vdqWBp7MyzdmHkkRWV5nY+PfGRbYbahfuvsBCh277tq+w9zyNi7h5CYJCK0kmzti9kU+O/cB7sE8HvKv6aXAKQ==" }, "ignore-by-default": { "version": "1.0.1", @@ -5979,6 +6742,11 @@ } } }, + "import-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/import-modules/-/import-modules-1.1.0.tgz", + "integrity": "sha1-dI23nFzEK7lwHvq0JPiU5yYA6dw=" + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6014,23 +6782,101 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "inquirer": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.0.tgz", - "integrity": "sha512-scfHejeG/lVZSpvCXpsB4j/wQNPM5JC8kiElOI0OUTwmc1RTpXr4H32/HOlQHcZiYl2z2VElwuCVDRG8vFmbnA==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.1.tgz", + "integrity": "sha512-uxNHBeQhRXIoHWTSNYUFhQVrHYFThIt6IVo2fFmSe8aBwdR3/w6b58hJpiL/fMukFkvGzjg+hSxFtwvVmKZmXw==", "requires": { - "ansi-escapes": "^3.2.0", + "ansi-escapes": "^4.2.1", "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", + "cli-cursor": "^3.1.0", "cli-width": "^2.0.0", "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", "run-async": "^2.2.0", "rxjs": "^6.4.0", - "string-width": "^2.1.0", + "string-width": "^4.1.0", "strip-ansi": "^5.1.0", "through": "^2.3.6" + }, + "dependencies": { + "ansi-escapes": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz", + "integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==", + "requires": { + "type-fest": "^0.5.2" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "figures": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.0.0.tgz", + "integrity": "sha512-HKri+WoWoUgr83pehn/SIgLOMZ9nAWC6dcGj26RY2R4F50u4+RTUz0RCrUlOV3nKRAICW1UGzyb+kcX2qK1S/g==", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "string-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz", + "integrity": "sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^5.2.0" + } + }, + "type-fest": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", + "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==" + } } }, "inquirer-autocomplete-prompt": { @@ -6053,6 +6899,11 @@ "p-is-promise": "^1.1.0" } }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" + }, "ipaddr.js": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", @@ -6160,6 +7011,11 @@ } } }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + }, "is-docker": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-1.1.0.tgz", @@ -6251,7 +7107,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", - "dev": true, "requires": { "symbol-observable": "^1.1.0" } @@ -6259,14 +7114,12 @@ "is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==" }, "is-path-in-cwd": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", - "dev": true, "requires": { "is-path-inside": "^2.1.0" }, @@ -6275,7 +7128,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dev": true, "requires": { "path-is-inside": "^1.0.2" } @@ -6321,6 +7173,11 @@ "has": "^1.0.1" } }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=" + }, "is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -6457,7 +7314,6 @@ "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -6506,8 +7362,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json5": { "version": "2.1.0", @@ -6612,6 +7467,14 @@ } } }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "requires": { + "invert-kv": "^2.0.0" + } + }, "lcov-parse": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", @@ -6638,6 +7501,86 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" }, + "lint-staged": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-9.2.1.tgz", + "integrity": "sha512-3lGgJfBddCy/WndKdNko+uJbwyYjBD1k+V+SA+phBYWzH265S95KQya/Wln/UL+hOjc7NcjtFYVCUWuAcqYHhg==", + "requires": { + "chalk": "^2.4.2", + "commander": "^2.20.0", + "cosmiconfig": "^5.2.1", + "debug": "^4.1.1", + "dedent": "^0.7.0", + "del": "^5.0.0", + "execa": "^2.0.3", + "listr": "^0.14.3", + "log-symbols": "^3.0.0", + "micromatch": "^4.0.2", + "please-upgrade-node": "^3.1.1", + "string-argv": "^0.3.0", + "stringify-object": "^3.3.0" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "del": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-5.0.0.tgz", + "integrity": "sha512-TfU3nUY0WDIhN18eq+pgpbLY9AfL5RfiE9czKaTSolc6aK7qASXfDErvYgjV1UqCR4sNXDoxO0/idPmhDUt2Sg==", + "requires": { + "globby": "^10.0.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "rimraf": "^2.6.3" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "requires": { + "chalk": "^2.4.2" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + } + } + }, "list-item": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/list-item/-/list-item-1.1.1.tgz", @@ -6656,29 +7599,170 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "^0.1.0" + "is-extendable": "^0.1.0" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "listr": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", + "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", + "requires": { + "@samverschueren/stream-to-observable": "^0.3.0", + "is-observable": "^1.1.0", + "is-promise": "^2.1.0", + "is-stream": "^1.1.0", + "listr-silent-renderer": "^1.1.1", + "listr-update-renderer": "^0.5.0", + "listr-verbose-renderer": "^0.5.0", + "p-map": "^2.0.0", + "rxjs": "^6.3.3" + } + }, + "listr-silent-renderer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", + "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=" + }, + "listr-update-renderer": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", + "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", + "requires": { + "chalk": "^1.1.3", + "cli-truncate": "^0.2.1", + "elegant-spinner": "^1.0.1", + "figures": "^1.7.0", + "indent-string": "^3.0.0", + "log-symbols": "^1.0.2", + "log-update": "^2.3.0", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cli-truncate": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", + "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", + "requires": { + "slice-ansi": "0.0.4", + "string-width": "^1.0.1" } }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", "requires": { - "kind-of": "^3.0.2" + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" } }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "requires": { - "is-buffer": "^1.1.5" + "ansi-regex": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "requires": { + "chalk": "^1.0.0" + } + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" } } }, + "listr-verbose-renderer": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", + "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", + "requires": { + "chalk": "^2.4.1", + "cli-cursor": "^2.1.0", + "date-fns": "^1.27.2", + "figures": "^2.0.0" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -6782,6 +7866,11 @@ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" }, + "lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6845,6 +7934,16 @@ "integrity": "sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=", "dev": true }, + "lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984=" + }, + "lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=" + }, "log-driver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", @@ -6859,6 +7958,40 @@ "chalk": "^2.0.1" } }, + "log-update": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", + "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=", + "requires": { + "ansi-escapes": "^3.0.0", + "cli-cursor": "^2.0.0", + "wrap-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", + "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0" + } + } + } + }, "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -6896,6 +8029,14 @@ "pify": "^3.0.0" } }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "requires": { + "p-defer": "^1.0.0" + } + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -7122,6 +8263,28 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" + } + } + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -7232,11 +8395,15 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, "merge2": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz", - "integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==", - "dev": true + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.4.tgz", + "integrity": "sha512-FYE8xI+6pjFOhokZu0We3S5NKCirLbCzSh2Usf3qEyr4X8U+0jNg9P8RZ4qz+V2UoECLVwSyzU3LxXBaLGtD3A==" }, "methods": { "version": "1.1.2", @@ -7306,7 +8473,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "minimist-options": { @@ -7361,11 +8528,119 @@ "dependencies": { "minimist": { "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, + "mocha": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.0.tgz", + "integrity": "sha512-qwfFgY+7EKAAUAdv7VYMZQknI7YJSGesxHyhn6qD52DV8UcSZs5XwCifcZGMVIE4a5fbmhvbotxC0DLQ0oKohQ==", + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.2.2", + "yargs-parser": "13.0.0", + "yargs-unparser": "1.5.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "yargs": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", + "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "requires": { + "cliui": "^4.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.0.0" + } + }, + "yargs-parser": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", + "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "mock-stdin": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/mock-stdin/-/mock-stdin-0.3.1.tgz", + "integrity": "sha1-xlfZZC2QeGQ1xkyl6Zu9TQm9fdM=" + }, "module-definition": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-3.2.0.tgz", @@ -7412,7 +8687,8 @@ "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true }, "nan": { "version": "2.14.0", @@ -7544,11 +8820,84 @@ "wrap-ansi": "^5.1.0" }, "dependencies": { + "boxen": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-3.2.0.tgz", + "integrity": "sha512-cU4J/+NodM3IHdSL2yN8bqYqnmlBTidDR4RC7nJs61ZmtGz8VZzM3HLQX0zY5mrSmPtR3xWwsq2jOUQqFZN8+A==", + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^2.4.2", + "cli-boxes": "^2.2.0", + "string-width": "^3.0.0", + "term-size": "^1.2.0", + "type-fest": "^0.3.0", + "widest-line": "^2.0.0" + }, + "dependencies": { + "type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==" + } + } + }, + "chokidar": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", + "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "get-port": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==" }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, "netlify": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/netlify/-/netlify-2.4.1.tgz", @@ -7616,6 +8965,14 @@ } } }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, "parse-json": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", @@ -7694,6 +9051,31 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "nock": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", + "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "deep-equal": "^1.0.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.5", + "mkdirp": "^0.5.0", + "propagate": "^1.0.0", + "qs": "^6.5.1", + "semver": "^5.5.0" + } + }, + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + } + }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", @@ -7806,8 +9188,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "nyc": { "version": "13.3.0", @@ -8906,7 +10287,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -9021,6 +10401,11 @@ "is-wsl": "^1.1.0" } }, + "opencollective-postinstall": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz", + "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==" + }, "opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -9041,7 +10426,7 @@ "dependencies": { "minimist": { "version": "0.0.10", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true }, @@ -9086,6 +10471,48 @@ } } }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + }, + "dependencies": { + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + } + } + }, "os-name": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", @@ -9113,6 +10540,11 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==" }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" + }, "p-event": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", @@ -9356,6 +10788,11 @@ "pify": "^3.0.0" } }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -9370,8 +10807,7 @@ "picomatch": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", - "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", - "dev": true + "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==" }, "pidtree": { "version": "0.3.0", @@ -9432,7 +10868,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, "requires": { "find-up": "^4.0.0" }, @@ -9441,7 +10876,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "requires": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -9451,7 +10885,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "requires": { "p-locate": "^4.1.0" } @@ -9460,7 +10893,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "requires": { "p-limit": "^2.2.0" } @@ -9468,11 +10900,18 @@ "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" } } }, + "please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "requires": { + "semver-compare": "^1.0.0" + } + }, "plur": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/plur/-/plur-3.1.1.tgz", @@ -9624,6 +11063,11 @@ "asap": "~2.0.3" } }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=" + }, "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -9725,15 +11169,6 @@ "universalify": "^0.1.0" } }, - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "load-json-file": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz", @@ -9811,6 +11246,11 @@ "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", "dev": true }, + "ramda": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", + "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==" + }, "random-item": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-item/-/random-item-1.0.0.tgz", @@ -9903,12 +11343,79 @@ } }, "read-pkg-up": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", - "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-6.0.0.tgz", + "integrity": "sha512-odtTvLl+EXo1eTsMnoUHRmg/XmXdTkwXVxy4VFE9Kp6cCq7b3l7QMdBndND3eAFzrbSAXC/WCUOQQ9rLjifKZw==", "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" + "find-up": "^4.0.0", + "read-pkg": "^5.1.1", + "type-fest": "^0.5.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "parse-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", + "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + } + } + }, + "type-fest": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", + "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==" + } } }, "readable-stream": { @@ -10004,8 +11511,7 @@ "regexpp": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==" }, "regexpu-core": { "version": "4.5.4", @@ -10055,7 +11561,7 @@ "dependencies": { "jsesc": { "version": "0.5.0", - "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true } @@ -10137,6 +11643,16 @@ } } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, "require-package-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", @@ -10154,9 +11670,9 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, "resolve": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", - "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", + "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", "requires": { "path-parse": "^1.0.6" } @@ -10173,8 +11689,7 @@ "resolve-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" }, "resolve-url": { "version": "0.2.1", @@ -10206,8 +11721,7 @@ "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, "rimraf": { "version": "2.6.3", @@ -10225,6 +11739,11 @@ "is-promise": "^2.1.0" } }, + "run-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", + "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==" + }, "run-parallel": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", @@ -10290,6 +11809,11 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=" + }, "semver-diff": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", @@ -10374,6 +11898,11 @@ "send": "0.17.1" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, "set-getter": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz", @@ -10449,7 +11978,7 @@ }, "shelljs": { "version": "0.3.0", - "resolved": "http://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", "dev": true }, @@ -10461,8 +11990,7 @@ "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" }, "slice-ansi": { "version": "1.0.0", @@ -10700,8 +12228,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { "version": "1.16.1", @@ -10799,11 +12326,48 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "stdout-stderr": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/stdout-stderr/-/stdout-stderr-0.1.9.tgz", + "integrity": "sha1-m0juBO/5Ve4Hd24nEl1VJNnQL1c=", + "requires": { + "debug": "^3.1.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" }, + "string-argv": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.0.tgz", + "integrity": "sha512-NGZHq3nkSXVtGZXTBjFru3MNfoZyIzN25T7BmvdgnSC0LCJczAGLLMQLyjywSIaAoqSemgLzBRHOsnrHbt60+Q==" + }, "string-template": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", @@ -10853,6 +12417,16 @@ "safe-buffer": "~5.1.0" } }, + "stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "requires": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + } + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -10894,6 +12468,11 @@ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, "strip-indent": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", @@ -10970,8 +12549,7 @@ "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, "sync-request": { "version": "3.0.1", @@ -10985,9 +12563,9 @@ } }, "table": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.4.tgz", - "integrity": "sha512-IIfEAUx5QlODLblLrGTTLJA7Tk0iLSGBvgY8essPRVNGHAzThujww1YqHLs6h3HfTg55h++RzLHH5Xw/rfv+mg==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.5.tgz", + "integrity": "sha512-oGa2Hl7CQjfoaogtrOHEJroOcYILTx7BZWLGsJIlzoWmB2zmguhNfPJZsWPKYek/MgCxfco54gEi31d1uN2hFA==", "dev": true, "requires": { "ajv": "^6.10.2", @@ -11394,6 +12972,11 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, "type-fest": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", @@ -11837,11 +13420,15 @@ "isexe": "^2.0.0" } }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, "requires": { "string-width": "^1.0.2 || 2" } @@ -11860,6 +13447,38 @@ "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==", "requires": { "execa": "^1.0.0" + }, + "dependencies": { + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + } } }, "wordwrap": { @@ -11868,26 +13487,54 @@ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" }, "wrap-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-4.0.0.tgz", - "integrity": "sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.0.0.tgz", + "integrity": "sha512-8YwLklVkHe4QNpGFrK6Mxm+BaMY7da6C9GlDED3xs3XwThyJHSbVwg9qC4s1N8tBFcnM1S0s8I390RC6SgGe+g==", "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^5.0.0" }, "dependencies": { - "ansi-regex": { + "ansi-styles": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.0.0.tgz", + "integrity": "sha512-8zjUtFJ3db/QoPXuuEMloS2AUf79/yeyttJ7Abr3hteopJu9HK8vsgGviGUMq+zyA6cZZO6gAyZoMTF6TgaEjA==", + "requires": { + "color-convert": "^2.0.0" + } + }, + "color-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.0.tgz", + "integrity": "sha512-hzTicsCJIHdxih9+2aLR1tNGZX5qSJGRHDPVwSY26tVrEf55XNajLOBWz2UuWSIergszA09/bqnOiHyqx9fxQg==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "string-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz", + "integrity": "sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==", "requires": { - "ansi-regex": "^3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^5.2.0" } } } @@ -11984,6 +13631,11 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", @@ -12023,6 +13675,56 @@ } } }, + "yargs-unparser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", + "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.11", + "yargs": "^12.0.5" + }, + "dependencies": { + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "yarn": { "version": "1.17.3", "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.17.3.tgz", diff --git a/package.json b/package.json index 081ab6706fb..e3f0c12e7da 100644 --- a/package.json +++ b/package.json @@ -60,51 +60,86 @@ }, "dependencies": { "@netlify/cli-utils": "^1.0.7", - "@oclif/command": "^1.5.14", + "@netlify/rules-proxy": "^0.1.4", + "@netlify/zip-it-and-ship-it": "^0.3.1", + "@oclif/command": "^1.5.18", + "@oclif/config": "^1.13.2", "@oclif/errors": "^1.1.2", "@oclif/plugin-help": "^2.2.0", "@oclif/plugin-not-found": "^1.1.4", "@oclif/plugin-plugins": "^1.7.8", + "@oclif/test": "^1.2.5", "@octokit/rest": "^16.28.1", "ansi-styles": "^3.2.1", "ascii-table": "0.0.9", + "body-parser": "^1.19.0", + "boxen": "^4.1.0", + "chai": "^4.2.0", "chalk": "^2.4.2", + "chokidar": "^3.0.2", "ci-info": "^2.0.0", "clean-deep": "^3.0.2", "cli-spinners": "^1.3.1", "cli-ux": "^5.2.1", "concordance": "^4.0.0", + "copy-template-dir": "^1.4.0", + "debug": "^4.1.1", "envinfo": "^7.3.1", + "eslint-config-oclif": "^3.1.0", + "execa": "^2.0.3", + "express": "^4.17.1", + "express-logging": "^1.1.1", "find-up": "^3.0.0", + "fs-extra": "^8.1.0", + "fuzzy": "^0.1.3", "get-port": "^5.0.0", + "gh-release-fetch": "^1.0.3", "git-remote-origin-url": "^2.0.0", "git-repo-info": "^2.1.0", - "inquirer": "^6.3.1", + "globby": "^10.0.1", + "http-proxy": "^1.17.0", + "husky": "^3.0.3", + "inquirer": "^6.5.1", + "inquirer-autocomplete-prompt": "^1.0.1", "is-docker": "^1.1.0", + "jwt-decode": "^2.2.0", + "lint-staged": "^9.2.1", "lodash.get": "^4.4.2", "lodash.isempty": "^4.4.0", "lodash.isequal": "^4.5.0", "lodash.sample": "^4.2.1", "log-symbols": "^2.2.0", + "mocha": "^6.2.0", "netlify": "^2.4.8", + "netlify-cli-logo": "^1.0.0", "netlify-dev-plugin": "^1.0.28", + "nock": "^10.0.6", "node-fetch": "^2.6.0", + "npm-packlist": "^1.4.4", "open": "^6.4.0", "ora": "^3.4.0", "p-wait-for": "^2.0.0", "parse-github-url": "^1.0.2", "parse-gitignore": "^1.0.1", + "precinct": "^6.1.2", "prettyjson": "^1.2.1", "random-item": "^1.0.0", - "update-notifier": "^2.5.0" + "read-pkg-up": "^6.0.0", + "require-package-name": "^2.0.1", + "resolve": "^1.12.0", + "safe-join": "^0.1.3", + "static-server": "^2.2.1", + "update-notifier": "^2.5.0", + "wait-port": "^0.2.2", + "wrap-ansi": "^6.0.0" }, "devDependencies": { - "@oclif/dev-cli": "^1.22.0", - "auto-changelog": "^1.13.0", + "@oclif/dev-cli": "^1.22.2", + "auto-changelog": "^1.14.1", "ava": "^1.4.1", "body": "^5.1.0", "coveralls": "^3.0.4", - "dependency-check": "^3.3.0", + "dependency-check": "^3.4.1", "dependency-cruiser": "^4.22.0", "eslint": "^5.16.0", "eslint-config-prettier": "^4.3.0", @@ -115,9 +150,9 @@ "gh-release": "^3.5.0", "markdown-magic": "^0.1.25", "mkdirp": "^0.5.1", - "npm-run-all": "^4.1.3", + "npm-run-all": "^4.1.5", "nyc": "^13.3.0", - "prettier": "^1.16.4", + "prettier": "^1.18.2", "strip-ansi": "^5.2.0" }, "ava": { diff --git a/src/commands/dev/exec.js b/src/commands/dev/exec.js new file mode 100644 index 00000000000..6b64bc75e25 --- /dev/null +++ b/src/commands/dev/exec.js @@ -0,0 +1,46 @@ +const execa = require("execa"); +const Command = require("@netlify/cli-utils"); +const { track } = require("@netlify/cli-utils/src/utils/telemetry"); +const { + // NETLIFYDEV, + NETLIFYDEVLOG, + // NETLIFYDEVWARN, + NETLIFYDEVERR +} = require("netlify-cli-logo"); + +class ExecCommand extends Command { + async run() { + const { site, api } = this.netlify; + if (site.id) { + this.log( + `${NETLIFYDEVLOG} Checking your site's environment variables...` + ); // just to show some visual response first + const accessToken = api.accessToken; + const { addEnvVariables } = require("../../utils/dev"); + await addEnvVariables(api, site, accessToken); + } else { + this.log( + `${NETLIFYDEVERR} No Site ID detected. You probably forgot to run \`netlify link\` or \`netlify init\`. ` + ); + } + execa(this.argv[0], this.argv.slice(1), { + env: process.env, + stdio: "inherit" + }); + // Todo hoist this telemetry `command` to CLI hook + track("command", { + command: "dev:exec" + }); + } +} + +ExecCommand.description = `Exec command +Runs a command within the netlify dev environment, e.g. with env variables from any installed addons +`; + +ExecCommand.examples = ["$ netlify exec npm run bootstrap"]; + +ExecCommand.strict = false; +ExecCommand.parse = false; + +module.exports = ExecCommand; diff --git a/src/commands/dev/index.js b/src/commands/dev/index.js new file mode 100644 index 00000000000..eb264a18323 --- /dev/null +++ b/src/commands/dev/index.js @@ -0,0 +1,348 @@ +const { flags } = require("@oclif/command"); +const execa = require("execa"); +const http = require("http"); +const httpProxy = require("http-proxy"); +const waitPort = require("wait-port"); +const getPort = require("get-port"); +const chokidar = require("chokidar"); +const { serveFunctions } = require("../../utils/serve-functions"); +const { serverSettings } = require("../../utils/detect-server"); +const { detectFunctionsBuilder } = require("../../utils/detect-functions-builder"); +const Command = require("@netlify/cli-utils"); +const { track } = require("@netlify/cli-utils/src/utils/telemetry"); +const chalk = require("chalk"); +const { + NETLIFYDEV, + NETLIFYDEVLOG, + NETLIFYDEVWARN + // NETLIFYDEVERR +} = require("netlify-cli-logo"); +const boxen = require("boxen"); +const { createTunnel, connectTunnel } = require("../../utils/live-tunnel"); + +function isFunction(settings, req) { + return settings.functionsPort && req.url.match(/^\/.netlify\/functions\/.+/); +} + +function addonUrl(addonUrls, req) { + const m = req.url.match(/^\/.netlify\/([^\/]+)(\/.*)/); // eslint-disable-line no-useless-escape + const addonUrl = m && addonUrls[m[1]]; + return addonUrl ? `${addonUrl}${m[2]}` : null; +} + +// Used as an optimization to avoid dual lookups for missing assets +const assetExtensionRegExp = /\.(html?|png|jpg|js|css|svg|gif|ico|woff|woff2)$/; + +function alternativePathsFor(url) { + const paths = []; + if (url[url.length - 1] === "/") { + const end = url.length - 1; + if (url !== "/") { + paths.push(url.slice(0, end) + ".html"); + paths.push(url.slice(0, end) + ".htm"); + } + paths.push(url + "index.html"); + paths.push(url + "index.htm"); + } else if (!url.match(assetExtensionRegExp)) { + paths.push(url + ".html"); + paths.push(url + ".htm"); + paths.push(url + "/index.html"); + paths.push(url + "/index.htm"); + } + + return paths; +} + +function initializeProxy(port) { + const proxy = httpProxy.createProxyServer({ + selfHandleResponse: true, + target: { + host: "localhost", + port: port + } + }); + + proxy.on("proxyRes", (proxyRes, req, res) => { + if ( + proxyRes.statusCode === 404 && + req.alternativePaths && + req.alternativePaths.length > 0 + ) { + req.url = req.alternativePaths.shift(); + return proxy.web(req, res, req.proxyOptions); + } + res.writeHead(proxyRes.statusCode, proxyRes.headers); + proxyRes.on("data", function(data) { + res.write(data); + }); + proxyRes.on("end", function() { + res.end(); + }); + }); + + return { + web: (req, res, options) => { + req.proxyOptions = options; + req.alternativePaths = alternativePathsFor(req.url); + req.headers['x-forwarded-for'] = req.connection.remoteAddress; + return proxy.web(req, res, options); + }, + ws: (req, socket, head) => proxy.ws(req, socket, head) + }; +} + +async function startProxy(settings, addonUrls) { + const rulesProxy = require("@netlify/rules-proxy"); + + await waitPort({ port: settings.proxyPort }); + if (settings.functionsPort) { + await waitPort({ port: settings.functionsPort }); + } + const port = await getPort({ port: settings.port || 8888 }); + const functionsServer = settings.functionsPort + ? `http://localhost:${settings.functionsPort}` + : null; + + const proxy = initializeProxy(settings.proxyPort); + + const rewriter = rulesProxy({ publicFolder: settings.dist }); + + const server = http.createServer(function(req, res) { + if (isFunction(settings, req)) { + return proxy.web(req, res, { target: functionsServer }); + } + let url = addonUrl(addonUrls, req); + if (url) { + return proxy.web(req, res, { target: url }); + } + + rewriter(req, res, () => { + if (isFunction(settings, req)) { + return proxy.web(req, res, { target: functionsServer }); + } + url = addonUrl(addonUrls, req); + if (url) { + return proxy.web(req, res, { target: url }); + } + + proxy.web(req, res, { target: `http://localhost:${settings.proxyPort}` }); + }); + }); + + server.on("upgrade", function(req, socket, head) { + proxy.ws(req, socket, head); + }); + + server.listen(port); + return { url: `http://localhost:${port}`, port }; +} + +function startDevServer(settings, log) { + if (settings.noCmd) { + const StaticServer = require("static-server"); + + const server = new StaticServer({ + rootPath: settings.dist, + name: "netlify-dev", + port: settings.proxyPort, + templates: { + notFound: "404.html" + } + }); + + server.start(function() { + log(`\n${NETLIFYDEVLOG} Server listening to`, settings.proxyPort); + }); + return; + } + log(`${NETLIFYDEVLOG} Starting Netlify Dev with ${settings.type}`); + const args = + settings.command === "npm" ? ["run", ...settings.args] : settings.args; + const ps = execa(settings.command, args, { + env: { ...settings.env, FORCE_COLOR: "true" }, + stdio: ["inherit", "pipe", "pipe"] + }); + ps.stdout.on("data", function(buffer) { + process.stdout.write(buffer.toString("utf8")); + }); + ps.stderr.on("data", function(buffer) { + process.stderr.write(buffer.toString("utf8")); + }); + ps.on("close", code => process.exit(code)); + ps.on("SIGINT", process.exit); + ps.on("SIGTERM", process.exit); +} + +class DevCommand extends Command { + async run() { + this.log(`${NETLIFYDEV}`); + let { flags } = this.parse(DevCommand); + const { api, site, config } = this.netlify; + const functionsDir = + flags.functions || + (config.dev && config.dev.functions) || + (config.build && config.build.functions); + let addonUrls = {}; + + let accessToken = api.accessToken; + if (site.id && !flags.offline) { + const { addEnvVariables } = require("../../utils/dev"); + addonUrls = await addEnvVariables(api, site, accessToken); + } + process.env.NETLIFY_DEV = "true"; + + let settings = await serverSettings(Object.assign({}, config.dev, flags)); + + if (!(settings && settings.command)) { + this.log( + `${NETLIFYDEVWARN} No dev server detected, using simple static server` + ); + let dist = + (config.dev && config.dev.publish) || + (config.build && config.build.publish); + if (!dist) { + this.log(`${NETLIFYDEVLOG} Using current working directory`); + this.log( + `${NETLIFYDEVWARN} Unable to determine public folder to serve files from.` + ); + this.log( + `${NETLIFYDEVWARN} Setup a netlify.toml file with a [dev] section to specify your dev server settings.` + ); + this.log( + `${NETLIFYDEVWARN} See docs at: https://github.com/netlify/netlify-dev-plugin#project-detection` + ); + this.log( + `${NETLIFYDEVWARN} Using current working directory for now...` + ); + dist = process.cwd(); + } + settings = { + noCmd: true, + port: 8888, + proxyPort: await getPort({ port: 3999 }), + dist + }; + } + + // Reset port if not manually specified, to make it dynamic + if (!(config.dev && config.dev.port) && !flags.port) { + settings = { + port: await getPort({ port: settings.port }), + ...settings + }; + } + + startDevServer(settings, this.log); + + // serve functions from zip-it-and-ship-it + // env variables relies on `url`, careful moving this code + if (functionsDir) { + const functionBuilder = await detectFunctionsBuilder(settings); + if (functionBuilder) { + this.log( + `${NETLIFYDEVLOG} Function builder ${chalk.yellow( + functionBuilder.builderName + )} detected: Running npm script ${chalk.yellow( + functionBuilder.npmScript + )}` + ); + this.warn( + `${NETLIFYDEVWARN} This is a beta feature, please give us feedback on how to improve at https://github.com/netlify/netlify-dev-plugin/` + ); + await functionBuilder.build(); + const functionWatcher = chokidar.watch(functionBuilder.src); + functionWatcher.on("add", functionBuilder.build); + functionWatcher.on("change", functionBuilder.build); + functionWatcher.on("unlink", functionBuilder.build); + } + const functionsPort = await getPort({ port: 34567 }); + + // returns a value but we dont use it + await serveFunctions({ + ...settings, + port: functionsPort, + functionsDir + }); + settings.functionsPort = functionsPort; + } + + let { url, port } = await startProxy(settings, addonUrls); + if (!url) { + url = proxyUrl; + } + + if (flags.live) { + await waitPort({ port }); + const liveSession = await createTunnel(site.id, accessToken, this.log); + url = liveSession.session_url; + process.env.BASE_URL = url; + + await connectTunnel(liveSession, accessToken, port, this.log); + } + + // Todo hoist this telemetry `command` to CLI hook + track("command", { + command: "dev", + projectType: settings.type || "custom", + live: flags.live || false + }); + + // boxen doesnt support text wrapping yet https://github.com/sindresorhus/boxen/issues/16 + const banner = require("wrap-ansi")( + chalk.bold(`${NETLIFYDEVLOG} Server now ready on ${url}`), + 70 + ); + process.env.URL = url; + process.env.DEPLOY_URL = process.env.URL; + + this.log( + boxen(banner, { + padding: 1, + margin: 1, + align: "center", + borderColor: "#00c7b7" + }) + ); + } +} + +DevCommand.description = `Local dev server +The dev command will run a local dev server with Netlify's proxy and redirect rules +`; + +DevCommand.examples = [ + "$ netlify dev", + '$ netlify dev -c "yarn start"', + "$ netlify dev -c hugo" +]; + +DevCommand.strict = false; + +DevCommand.flags = { + command: flags.string({ + char: "c", + description: "command to run" + }), + port: flags.integer({ + char: "p", + description: "port of netlify dev" + }), + dir: flags.string({ + char: "d", + description: "dir with static files" + }), + functions: flags.string({ + char: "f", + description: "Specify a functions folder to serve" + }), + offline: flags.boolean({ + char: "o", + description: "disables any features that require network access" + }), + live: flags.boolean({ + char: "l", + description: "Start a public live session" + }) +}; + +module.exports = DevCommand; diff --git a/src/commands/functions/build.js b/src/commands/functions/build.js new file mode 100644 index 00000000000..25557c44904 --- /dev/null +++ b/src/commands/functions/build.js @@ -0,0 +1,61 @@ +const fs = require("fs"); +const { flags } = require("@oclif/command"); +const Command = require("@netlify/cli-utils"); +const { zipFunctions } = require("@netlify/zip-it-and-ship-it"); +const { + // NETLIFYDEV, + NETLIFYDEVLOG, + // NETLIFYDEVWARN, + NETLIFYDEVERR +} = require("netlify-cli-logo"); + +class FunctionsBuildCommand extends Command { + async run() { + const { flags } = this.parse(FunctionsBuildCommand); + const { config } = this.netlify; + + const src = flags.src || config.build.functionsSource; + const dst = flags.functions || config.build.functions; + + if (src === dst) { + this.log( + `${NETLIFYDEVERR} Source and destination for function build can't be the same` + ); + process.exit(1); + } + + if (!src || !dst) { + if (!src) + this.log( + `${NETLIFYDEVERR} Error: You must specify a source folder with a --src flag or a functionsSource field in your config` + ); + if (!dst) + this.log( + `${NETLIFYDEVERR} Error: You must specify a destination functions folder with a --functions flag or a functions field in your config` + ); + process.exit(1); + } + + fs.mkdirSync(dst, { recursive: true }); + + this.log(`${NETLIFYDEVLOG} Building functions`); + zipFunctions(src, dst, { skipGo: true }); + this.log(`${NETLIFYDEVLOG} Functions built to `, dst); + } +} + +FunctionsBuildCommand.description = `build functions locally +`; +FunctionsBuildCommand.aliases = ["function:build"]; +FunctionsBuildCommand.flags = { + functions: flags.string({ + char: "f", + description: "Specify a functions folder to build to" + }), + src: flags.string({ + char: "s", + description: "Specify the source folder for the functions" + }) +}; + +module.exports = FunctionsBuildCommand; diff --git a/src/commands/functions/create.js b/src/commands/functions/create.js new file mode 100644 index 00000000000..096e62c798b --- /dev/null +++ b/src/commands/functions/create.js @@ -0,0 +1,458 @@ +const fs = require("fs-extra"); +const path = require("path"); +const copy = require("copy-template-dir"); +const { flags } = require("@oclif/command"); +const Command = require("@netlify/cli-utils"); +const inquirer = require("inquirer"); +const { readRepoURL, validateRepoURL } = require("../../utils/read-repo-url"); +const { addEnvVariables } = require("../../utils/dev"); +const { createSiteAddon } = require("../../utils/addons"); +const fetch = require("node-fetch"); +const cp = require("child_process"); +const ora = require("ora"); +const { track } = require("@netlify/cli-utils/src/utils/telemetry"); +const chalk = require("chalk"); +const { + // NETLIFYDEV, + NETLIFYDEVLOG, + NETLIFYDEVWARN, + NETLIFYDEVERR +} = require("netlify-cli-logo"); + +const templatesDir = path.resolve(__dirname, "../../functions-templates"); + +/** + * Be very clear what is the SOURCE (templates dir) vs the DEST (functions dir) + */ +class FunctionsCreateCommand extends Command { + async run() { + const { flags, args } = this.parse(FunctionsCreateCommand); + const { config } = this.netlify; + const functionsDir = ensureFunctionDirExists.call(this, flags, config); + + /* either download from URL or scaffold from template */ + if (flags.url) { + await downloadFromURL.call(this, flags, args, functionsDir); + } else { + await scaffoldFromTemplate.call(this, flags, args, functionsDir); + } + track("command", { + command: "functions:create", + url: flags.url + }); + } +} + +FunctionsCreateCommand.args = [ + { + name: "name", + description: "name of your new function file inside your functions folder" + } +]; + +FunctionsCreateCommand.description = `create a new function locally`; + +FunctionsCreateCommand.examples = [ + "netlify functions:create", + "netlify functions:create hello-world", + "netlify functions:create --name hello-world" +]; +FunctionsCreateCommand.aliases = ["function:create"]; +FunctionsCreateCommand.flags = { + name: flags.string({ char: "n", description: "function name" }), + url: flags.string({ char: "u", description: "pull template from URL" }) +}; +module.exports = FunctionsCreateCommand; + +/** + * all subsections of code called from the main logic flow above + */ + +// prompt for a name if name not supplied +async function getNameFromArgs(args, flags, defaultName) { + if (flags.name && args.name) + throw new Error( + "function name specified in both flag and arg format, pick one" + ); + let name; + if (flags.name && !args.name) name = flags.name; + // use flag if exists + else if (!flags.name && args.name) name = args.name; + + // if neither are specified, prompt for it + if (!name) { + let responses = await inquirer.prompt([ + { + name: "name", + message: "name your function: ", + default: defaultName, + type: "input", + validate: val => Boolean(val) && /^[\w\-.]+$/i.test(val) + // make sure it is not undefined and is a valid filename. + // this has some nuance i have ignored, eg crossenv and i18n concerns + } + ]); + name = responses.name; + } + return name; +} + +// pick template from our existing templates +async function pickTemplate() { + // lazy loading on purpose + inquirer.registerPrompt( + "autocomplete", + require("inquirer-autocomplete-prompt") + ); + const fuzzy = require("fuzzy"); + // doesnt scale but will be ok for now + const [ + jsreg + // tsreg, goreg + ] = [ + "js" + // 'ts', 'go' + ].map(formatRegistryArrayForInquirer); + const specialCommands = [ + new inquirer.Separator(`----[Special Commands]----`), + { + name: `*** Clone template from Github URL ***`, + value: "url", + short: "gh-url" + }, + { + name: `*** Report issue with, or suggest a new template ***`, + value: "report", + short: "gh-report" + } + ]; + const { chosentemplate } = await inquirer.prompt({ + name: "chosentemplate", + message: "Pick a template", + type: "autocomplete", + // suggestOnly: true, // we can explore this for entering URL in future + source: async function(answersSoFar, input) { + if (!input || input === "") { + // show separators + return [ + new inquirer.Separator(`----[JS]----`), + ...jsreg, + // new inquirer.Separator(`----[TS]----`), + // ...tsreg, + // new inquirer.Separator(`----[GO]----`), + // ...goreg + ...specialCommands + ]; + } + // only show filtered results sorted by score + let ans = [ + ...filterRegistry(jsreg, input), + // ...filterRegistry(tsreg, input), + // ...filterRegistry(goreg, input) + ...specialCommands + ].sort((a, b) => b.score - a.score); + return ans; + } + }); + return chosentemplate; + function filterRegistry(registry, input) { + const temp = registry.map(x => x.name + x.description); + const filteredTemplates = fuzzy.filter(input, temp); + const filteredTemplateNames = filteredTemplates.map(x => + input ? x.string : x + ); + return registry + .filter(t => filteredTemplateNames.includes(t.name + t.description)) + .map(t => { + // add the score + const { score } = filteredTemplates.find( + f => f.string === t.name + t.description + ); + t.score = score; + return t; + }); + } + function formatRegistryArrayForInquirer(lang) { + const folderNames = fs.readdirSync(path.join(templatesDir, lang)); + const registry = folderNames + .filter(x => !x.endsWith(".md")) // filter out markdown files + .map(name => + require(path.join( + templatesDir, + lang, + name, + ".netlify-function-template.js" + )) + ) + .sort((a, b) => (a.priority || 999) - (b.priority || 999)) + .map(t => { + t.lang = lang; + return { + // confusing but this is the format inquirer wants + name: `[${t.name}] ` + t.description, + value: t, + short: lang + "-" + t.name + }; + }); + return registry; + } +} + +/* get functions dir (and make it if necessary) */ +function ensureFunctionDirExists(flags, config) { + const functionsDir = config.build && config.build.functions; + if (!functionsDir) { + this.log(`${NETLIFYDEVLOG} No functions folder specified in netlify.toml`); + process.exit(1); + } + if (!fs.existsSync(functionsDir)) { + this.log( + `${NETLIFYDEVLOG} functions folder ${chalk.magenta.inverse( + functionsDir + )} specified in netlify.toml but folder not found, creating it...` + ); + fs.mkdirSync(functionsDir); + this.log( + `${NETLIFYDEVLOG} functions folder ${chalk.magenta.inverse( + functionsDir + )} created` + ); + } + return functionsDir; +} + +// Download files from a given github URL +async function downloadFromURL(flags, args, functionsDir) { + const folderContents = await readRepoURL(flags.url); + const functionName = flags.url.split("/").slice(-1)[0]; + const nameToUse = await getNameFromArgs(args, flags, functionName); + const fnFolder = path.join(functionsDir, nameToUse); + if ( + fs.existsSync(fnFolder + ".js") && + fs.lstatSync(fnFolder + ".js").isFile() + ) { + this.log( + `${NETLIFYDEVWARN}: A single file version of the function ${nameToUse} already exists at ${fnFolder}.js. Terminating without further action.` + ); + process.exit(1); + } + + try { + fs.mkdirSync(fnFolder, { recursive: true }); + } catch (error) { + // Ignore + } + await Promise.all( + folderContents.map(({ name, download_url }) => { + return fetch(download_url) + .then(res => { + const finalName = + path.basename(name, ".js") === functionName + ? nameToUse + ".js" + : name; + const dest = fs.createWriteStream(path.join(fnFolder, finalName)); + res.body.pipe(dest); + }) + .catch(error => { + throw new Error( + "Error while retrieving " + download_url + ` ${error}` + ); + }); + }) + ); + + this.log(`${NETLIFYDEVLOG} Installing dependencies for ${nameToUse}...`); + cp.exec("npm i", { cwd: path.join(functionsDir, nameToUse) }, () => { + this.log( + `${NETLIFYDEVLOG} Installing dependencies for ${nameToUse} complete ` + ); + }); + + // read, execute, and delete function template file if exists + const fnTemplateFile = path.join(fnFolder, ".netlify-function-template.js"); + if (fs.existsSync(fnTemplateFile)) { + const { onComplete, addons = [] } = require(fnTemplateFile); + + await installAddons.call(this, addons, path.resolve(fnFolder)); + if (onComplete) { + await addEnvVariables( + this.netlify.api, + this.netlify.site, + this.netlify.api.accessToken + ); + await onComplete.call(this); + } + fs.unlinkSync(fnTemplateFile); // delete + } +} + +async function installDeps(functionPath) { + return new Promise(resolve => { + cp.exec("npm i", { cwd: path.join(functionPath) }, () => { + resolve(); + }); + }); +} + +// no --url flag specified, pick from a provided template +async function scaffoldFromTemplate(flags, args, functionsDir) { + const chosentemplate = await pickTemplate.call(this); // pull the rest of the metadata from the template + if (chosentemplate === "url") { + const { chosenurl } = await inquirer.prompt([ + { + name: "chosenurl", + message: "URL to clone: ", + type: "input", + validate: val => Boolean(validateRepoURL(val)) + // make sure it is not undefined and is a valid filename. + // this has some nuance i have ignored, eg crossenv and i18n concerns + } + ]); + flags.url = chosenurl.trim(); + try { + await downloadFromURL.call(this, flags, args, functionsDir); + } catch (error) { + this.error(`$${NETLIFYDEVERR} Error downloading from URL: ` + flags.url); + this.error(error); + process.exit(1); + } + } else if (chosentemplate === "report") { + this.log( + `${NETLIFYDEVLOG} Open in browser: https://github.com/netlify/netlify-dev-plugin/issues/new` + ); + } else { + const { + onComplete, + name: templateName, + lang, + addons = [] + } = chosentemplate; + + const pathToTemplate = path.join(templatesDir, lang, templateName); + if (!fs.existsSync(pathToTemplate)) { + throw new Error( + `there isnt a corresponding folder to the selected name, ${templateName} template is misconfigured` + ); + } + + const name = await getNameFromArgs(args, flags, templateName); + this.log(`${NETLIFYDEVLOG} Creating function ${chalk.cyan.inverse(name)}`); + const functionPath = ensureFunctionPathIsOk.call( + this, + functionsDir, + flags, + name + ); + + // // SWYX: note to future devs - useful for debugging source to output issues + // this.log('from ', pathToTemplate, ' to ', functionPath) + const vars = { NETLIFY_STUFF_TO_REPLACE: "REPLACEMENT" }; // SWYX: TODO + let hasPackageJSON = false; + copy(pathToTemplate, functionPath, vars, async (err, createdFiles) => { + if (err) throw err; + createdFiles.forEach(filePath => { + if (filePath.endsWith(".netlify-function-template.js")) return; + this.log( + `${NETLIFYDEVLOG} ${chalk.greenBright("Created")} ${filePath}` + ); + require("fs").chmodSync(path.resolve(filePath), 0o777); + if (filePath.includes("package.json")) hasPackageJSON = true; + }); + // delete function template file that was copied over by copydir + fs.unlinkSync(path.join(functionPath, ".netlify-function-template.js")); + // rename the root function file if it has a different name from default + if (name !== templateName) { + fs.renameSync( + path.join(functionPath, templateName + ".js"), + path.join(functionPath, name + ".js") + ); + } + // npm install + if (hasPackageJSON) { + const spinner = ora({ + text: `installing dependencies for ${name}`, + spinner: "moon" + }).start(); + await installDeps(functionPath); + spinner.succeed(`installed dependencies for ${name}`); + } + + installAddons.call(this, addons, path.resolve(functionPath)); + if (onComplete) { + await addEnvVariables( + this.netlify.api, + this.netlify.site, + this.netlify.api.accessToken + ); + await onComplete.call(this); + } + }); + } +} + +async function installAddons(addons = [], fnPath) { + if (addons.length > 0) { + const { api, site } = this.netlify; + const siteId = site.id; + if (!siteId) { + this.log( + "No site id found, please run inside a site folder or `netlify link`" + ); + return false; + } + this.log(`${NETLIFYDEVLOG} checking Netlify APIs...`); + + return api.getSite({ siteId }).then(async siteData => { + const accessToken = api.accessToken; + const arr = addons.map(({ addonName, addonDidInstall }) => { + this.log( + `${NETLIFYDEVLOG} installing addon: ` + + chalk.yellow.inverse(addonName) + ); + // will prompt for configs if not supplied - we do not yet allow for addon configs supplied by `netlify functions:create` command and may never do so + return createSiteAddon( + accessToken, + addonName, + siteId, + siteData, + this.log + ) + .then(async addonCreateMsg => { + if (addonCreateMsg) { + // spinner.success("installed addon: " + addonName); + if (addonDidInstall) { + const { addEnvVariables } = require("../../utils/dev"); + await addEnvVariables(api, site, accessToken); + const { confirmPostInstall } = await inquirer.prompt([ + { + type: "confirm", + name: "confirmPostInstall", + message: `This template has an optional setup script that runs after addon install. This can be helpful for first time users to try out templates. Run the script?`, + default: false + } + ]); + if (confirmPostInstall) addonDidInstall(fnPath); + } + } + }) + .catch(error => { + this.error(`${NETLIFYDEVERR} Error installing addon: `, error); + }); + }); + return Promise.all(arr); + }); + } +} + +// we used to allow for a --dir command, +// but have retired that to force every scaffolded function to be a folder +function ensureFunctionPathIsOk(functionsDir, flags, name) { + const functionPath = path.join(functionsDir, name); + if (fs.existsSync(functionPath)) { + this.log( + `${NETLIFYDEVLOG} Function ${functionPath} already exists, cancelling...` + ); + process.exit(1); + } + return functionPath; +} diff --git a/src/commands/functions/index.js b/src/commands/functions/index.js new file mode 100644 index 00000000000..e26129dd53c --- /dev/null +++ b/src/commands/functions/index.js @@ -0,0 +1,45 @@ +const chalk = require("chalk"); +const { Command } = require("@oclif/command"); +const { execSync } = require("child_process"); + +function showHelp(command) { + execSync(`netlify ${command} --help`, { stdio: [0, 1, 2] }); +} + +function isEmptyCommand(flags, args) { + if (!hasFlags(flags) && !hasArgs(args)) { + return true; + } + return false; +} + +function hasFlags(flags) { + return Object.keys(flags).length; +} + +function hasArgs(args) { + return Object.keys(args).length; +} + +class FunctionsCommand extends Command { + async run() { + const { flags, args } = this.parse(FunctionsCommand); + // run help command if no args passed + if (isEmptyCommand(flags, args)) { + showHelp(this.id); + this.exit(); + } + } +} + +const name = chalk.greenBright("`functions`"); +FunctionsCommand.aliases = ["function"]; +FunctionsCommand.description = `Manage netlify functions +The ${name} command will help you manage the functions in this site +`; +FunctionsCommand.examples = [ + "netlify functions:create --name function-xyz", + "netlify functions:build --name function-abc --timeout 30s" +]; + +module.exports = FunctionsCommand; diff --git a/src/commands/functions/invoke.js b/src/commands/functions/invoke.js new file mode 100644 index 00000000000..09d0e33ca36 --- /dev/null +++ b/src/commands/functions/invoke.js @@ -0,0 +1,274 @@ +const chalk = require("chalk"); +const Command = require("@netlify/cli-utils"); +const { flags } = require("@oclif/command"); +const inquirer = require("inquirer"); +const { serverSettings } = require("../../utils/detect-server"); +const fetch = require("node-fetch"); +const fs = require("fs"); +const path = require("path"); + +const { getFunctions } = require("../../utils/get-functions"); + +// https://www.netlify.com/docs/functions/#event-triggered-functions +const eventTriggeredFunctions = [ + "deploy-building", + "deploy-succeeded", + "deploy-failed", + "deploy-locked", + "deploy-unlocked", + "split-test-activated", + "split-test-deactivated", + "split-test-modified", + "submission-created", + "identity-validate", + "identity-signup", + "identity-login" +]; +class FunctionsInvokeCommand extends Command { + async run() { + let { flags, args } = this.parse(FunctionsInvokeCommand); + const { api, site, config } = this.netlify; + + const functionsDir = + flags.functions || + (config.dev && config.dev.functions) || + (config.build && config.build.functions); + if (typeof functionsDir === "undefined") { + this.error( + "functions directory is undefined, did you forget to set it in netlify.toml?" + ); + process.exit(1); + } + + let settings = await serverSettings(Object.assign({}, config.dev, flags)); + + if (!(settings && settings.command)) { + settings = { + noCmd: true, + port: 8888, + proxyPort: 3999 + }; + } + + const functions = getFunctions(functionsDir); + const functionToTrigger = await getNameFromArgs(functions, args, flags); + + let headers = {}; + let body = {}; + + if (eventTriggeredFunctions.includes(functionToTrigger)) { + /** handle event triggered fns */ + // https://www.netlify.com/docs/functions/#event-triggered-functions + const parts = functionToTrigger.split("-"); + if (parts[0] === "identity") { + // https://www.netlify.com/docs/functions/#identity-event-functions + body.event = parts[1]; + body.user = { + email: "foo@trust-this-company.com", + user_metadata: { + TODO: "mock our netlify identity user data better" + } + }; + } else { + // non identity functions seem to have a different shape + // https://www.netlify.com/docs/functions/#event-function-payloads + body.payload = { + TODO: "mock up payload data better" + }; + body.site = { + TODO: "mock up site data better" + }; + } + } else { + // NOT an event triggered function, but may still want to simulate authentication locally + let _isAuthed = false; + if (typeof flags.identity === "undefined") { + const { isAuthed } = await inquirer.prompt([ + { + type: "confirm", + name: "isAuthed", + message: `Invoke with emulated Netlify Identity authentication headers? (pass --identity/--no-identity to override)`, + default: true + } + ]); + _isAuthed = isAuthed; + } else { + _isAuthed = flags.identity; + } + if (_isAuthed) { + headers = { + authorization: + "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGZ1bmN0aW9uczp0cmlnZ2VyIiwidGVzdERhdGEiOiJORVRMSUZZX0RFVl9MT0NBTExZX0VNVUxBVEVEX0pXVCJ9.Xb6vOFrfLUZmyUkXBbCvU4bM7q8tPilF0F03Wupap_c" + }; + // you can decode this https://jwt.io/ + // { + // "source": "netlify functions:trigger", + // "testData": "NETLIFY_DEV_LOCALLY_EMULATED_JWT" + // } + } + } + const payload = processPayloadFromFlag(flags.payload); + body = Object.assign({}, body, payload); + + // fetch + fetch( + `http://localhost:${ + settings.port + }/.netlify/functions/${functionToTrigger}` + + formatQstring(flags.querystring), + { + method: "post", + headers, + body: JSON.stringify(body) + } + ) + .then(response => { + let data; + data = response.text(); + try { + // data = response.json(); + data = JSON.parse(data); + } catch (err) {} + return data; + }) + .then(console.log) + .catch(err => { + console.error("ran into an error invoking your function"); + console.error(err); + }); + } +} + +function formatQstring(querystring) { + if (querystring) { + return "?" + querystring; + } else { + return ""; + } +} + +/** process payloads from flag */ +function processPayloadFromFlag(payloadString) { + if (payloadString) { + // case 1: jsonstring + let payload = tryParseJSON(payloadString); + if (!!payload) return payload; + // case 2: jsonpath + const payloadpath = path.join(process.cwd(), payloadString); + const pathexists = fs.existsSync(payloadpath); + if (!payload && pathexists) { + try { + payload = require(payloadpath); // there is code execution potential here + return payload; + } catch (err) { + console.error(err); + payload = false; + } + } + // case 3: invalid string, invalid path + return false; + } +} + +// prompt for a name if name not supplied +// also used in functions:create +async function getNameFromArgs(functions, args, flags) { + // let functionToTrigger = flags.name; + // const isValidFn = Object.keys(functions).includes(functionToTrigger); + if (flags.name && args.name) { + console.error( + "function name specified in both flag and arg format, pick one" + ); + process.exit(1); + } + let functionToTrigger; + if (flags.name && !args.name) functionToTrigger = flags.name; + // use flag if exists + else if (!flags.name && args.name) functionToTrigger = args.name; + + const isValidFn = Object.keys(functions).includes(functionToTrigger); + if (!functionToTrigger || !isValidFn) { + if (!isValidFn) { + console.warn( + `Function name ${chalk.yellow( + functionToTrigger + )} supplied but no matching function found in your functions folder, forcing you to pick a valid one...` + ); + } + const { trigger } = await inquirer.prompt([ + { + type: "list", + message: "Pick a function to trigger", + name: "trigger", + choices: Object.keys(functions) + } + ]); + functionToTrigger = trigger; + } + + return functionToTrigger; +} + +FunctionsInvokeCommand.description = `trigger a function while in netlify dev with simulated data, good for testing function calls including Netlify's Event Triggered Functions`; +FunctionsInvokeCommand.aliases = ["function:trigger"]; + +FunctionsInvokeCommand.examples = [ + "$ netlify functions:invoke", + "$ netlify functions:invoke myfunction", + "$ netlify functions:invoke --name myfunction", + "$ netlify functions:invoke --name myfunction --identity", + "$ netlify functions:invoke --name myfunction --no-identity", + '$ netlify functions:invoke myfunction --payload "{"foo": 1}"', + '$ netlify functions:invoke myfunction --querystring "foo=1', + '$ netlify functions:invoke myfunction --payload "./pathTo.json"' +]; +FunctionsInvokeCommand.args = [ + { + name: "name", + description: "function name to invoke" + } +]; + +FunctionsInvokeCommand.flags = { + name: flags.string({ + char: "n", + description: "function name to invoke" + }), + functions: flags.string({ + char: "f", + description: "Specify a functions folder to parse, overriding netlify.toml" + }), + querystring: flags.string({ + char: "q", + description: "Querystring to add to your function invocation" + }), + payload: flags.string({ + char: "p", + description: + "Supply POST payload in stringified json, or a path to a json file" + }), + identity: flags.boolean({ + description: + "simulate Netlify Identity authentication JWT. pass --no-identity to affirm unauthenticated request", + allowNo: true + }) +}; + +module.exports = FunctionsInvokeCommand; + +// https://stackoverflow.com/questions/3710204/how-to-check-if-a-string-is-a-valid-json-string-in-javascript-without-using-try +function tryParseJSON(jsonString) { + try { + var o = JSON.parse(jsonString); + + // Handle non-exception-throwing cases: + // Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking, + // but... JSON.parse(null) returns null, and typeof null === "object", + // so we must check for that, too. Thankfully, null is falsey, so this suffices: + if (o && typeof o === "object") { + return o; + } + } catch (e) {} + + return false; +} diff --git a/src/commands/functions/list.js b/src/commands/functions/list.js new file mode 100644 index 00000000000..2ba48a7bac9 --- /dev/null +++ b/src/commands/functions/list.js @@ -0,0 +1,104 @@ +const chalk = require("chalk"); +const Command = require("@netlify/cli-utils"); +const { flags } = require("@oclif/command"); +const AsciiTable = require("ascii-table"); +const { getFunctions } = require("../../utils/get-functions"); +class FunctionsListCommand extends Command { + async run() { + let { flags } = this.parse(FunctionsListCommand); + const { api, site, config } = this.netlify; + + // get deployed site details + // copied from `netlify status` + const siteId = site.id; + if (!siteId) { + this.warn("Did you run `netlify link` yet?"); + this.error(`You don't appear to be in a folder that is linked to a site`); + } + let siteData; + try { + siteData = await api.getSite({ siteId }); + } catch (e) { + if (e.status === 401 /* unauthorized*/) { + this.warn( + `Log in with a different account or re-link to a site you have permission for` + ); + this.error( + `Not authorized to view the currently linked site (${siteId})` + ); + } + if (e.status === 404 /* missing */) { + this.error(`The site this folder is linked to can't be found`); + } + this.error(e); + } + const deploy = siteData.published_deploy || {}; + const deployed_functions = deploy.available_functions || []; + + const functionsDir = + flags.functions || + (config.dev && config.dev.functions) || + (config.build && config.build.functions); + if (typeof functionsDir === "undefined") { + this.error( + "functions directory is undefined, did you forget to set it in netlify.toml?" + ); + process.exit(1); + } + var table = new AsciiTable( + `Netlify Functions (based on local functions folder "${functionsDir}")` + ); + const functions = getFunctions(functionsDir); + + table.setHeading("Name", "Url", "moduleDir", "deployed"); + Object.entries(functions).forEach(([functionName, { moduleDir }]) => { + const isDeployed = deployed_functions + .map(({ n }) => n) + .includes(functionName); + + // this.log(`${chalk.yellow("function name")}: ${functionName}`); + // this.log( + // ` ${chalk.yellow( + // "url" + // )}: ${`/.netlify/functions/${functionName}`}` + // ); + // this.log(` ${chalk.yellow("moduleDir")}: ${moduleDir}`); + // this.log( + // ` ${chalk.yellow("deployed")}: ${ + // isDeployed ? chalk.green("yes") : chalk.yellow("no") + // }` + // ); + // this.log("----------"); + table.addRow( + functionName, + `/.netlify/functions/${functionName}`, + moduleDir, + isDeployed ? "yes" : "no" + ); + }); + this.log(table.toString()); + } +} + +FunctionsListCommand.description = `list functions that exist locally + +Helpful for making sure that you have formatted your functions correctly + +NOT the same as listing the functions that have been deployed. For that info you need to go to your Netlify deploy log. +`; +FunctionsListCommand.aliases = ["function:list"]; +FunctionsListCommand.flags = { + name: flags.string({ + char: "n", + description: "name to print" + }), + functions: flags.string({ + char: "f", + description: "Specify a functions folder to serve" + }) +}; + +// TODO make visible once implementation complete +FunctionsListCommand.hidden = true; + +module.exports = FunctionsListCommand; diff --git a/src/detectors/README.md b/src/detectors/README.md new file mode 100644 index 00000000000..7d3b43860b9 --- /dev/null +++ b/src/detectors/README.md @@ -0,0 +1,68 @@ +## writing a detector + +- write as many checks as possible to fit your project +- return false if its not your project +- if it definitely is, return an object with this shape: + +```ts +{ + type: String, // e.g. gatsby, vue-cli + command: String, // e.g. yarn, npm + port: Number, // e.g. 8888 + proxyPort: Number, // e.g. 3000 + env: Object, // env variables, see examples + possibleArgsArrs: [[String]], // e.g [['run develop]], so that the combined command is 'npm run develop', but we allow for multiple + urlRegexp: RegExp, // see examples + dist: String, // static folder where a _redirect file would be placed, e.g. 'public' or 'static'. NOT the build output folder +} +``` + +## things to note + +- Dev block overrides will supercede anything you write in your detector: https://github.com/netlify/netlify-dev-plugin#project-detection +- detectors are language agnostic. don't assume npm or yarn. +- if default args (like 'develop') are missing, that means the user has configured it, best to tell them to use the -c flag. + +## detector notes + +- metalsmith is popular but has no dev story so we have skipped it +- hub press doesnt even have cli https://github.com/HubPress/hubpress.io#what-is-hubpress +- gitbook: + +not sure if we want to support gitbook yet + +requires a global install: https://github.com/GitbookIO/gitbook/blob/master/docs/setup.md + +```js +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["README.md", "SUMMARY.md"])) return false; + // // REQUIRED DEPS + // if (!hasRequiredDeps(["hexo"])) return false; + + /** everything below now assumes that we are within gatsby */ + + const possibleArgsArrs = [["gitbook", "serve"]]; + // scanScripts({ + // preferredScriptsArr: ["start", "dev", "develop"], + // preferredCommand: "hexo server" + // }); + + return { + type: "gitbook", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 4000, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${4000}(/)?`, "g"), + dist: "public" + }; +}; +``` diff --git a/src/detectors/brunch.js b/src/detectors/brunch.js new file mode 100644 index 00000000000..a0ba2610477 --- /dev/null +++ b/src/detectors/brunch.js @@ -0,0 +1,30 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json", "brunch-config.js"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["brunch"])) return false; + + /** everything below now assumes that we are within gatsby */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["start"], + preferredCommand: "brunch watch --server" + }); + + return { + type: "brunch", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 3333, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${3333}(/)?`, "g"), + dist: "app/assets" + }; +}; diff --git a/src/detectors/cra.js b/src/detectors/cra.js new file mode 100644 index 00000000000..61d4dc536d5 --- /dev/null +++ b/src/detectors/cra.js @@ -0,0 +1,39 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); + +/** + * detection logic - artificial intelligence! + * */ +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["react-scripts"])) return false; + + /** everything below now assumes that we are within create-react-app */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["start", "serve", "run"], + preferredCommand: "react-scripts start" + }); + + if (possibleArgsArrs.length === 0) { + // ofer to run it when the user doesnt have any scripts setup! 🤯 + possibleArgsArrs.push(["react-scripts", "start"]); + } + + return { + type: "create-react-app", + command: getYarnOrNPMCommand(), + port: 8888, // the port that the Netlify Dev User will use + proxyPort: 3000, // the port that create-react-app normally outputs + env: { ...process.env, BROWSER: "none", PORT: 3000 }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), + dist: "public" + }; +}; diff --git a/src/detectors/docusaurus.js b/src/detectors/docusaurus.js new file mode 100644 index 00000000000..746948cb450 --- /dev/null +++ b/src/detectors/docusaurus.js @@ -0,0 +1,30 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json", "siteConfig.js"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["docusaurus"])) return false; + + /** everything below now assumes that we are within gatsby */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["start"], + preferredCommand: "docusaurus-start" + }); + + return { + type: "docusaurus", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 3000, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), + dist: "static" + }; +}; diff --git a/src/detectors/eleventy.js b/src/detectors/eleventy.js new file mode 100644 index 00000000000..045d70981c2 --- /dev/null +++ b/src/detectors/eleventy.js @@ -0,0 +1,24 @@ +const { + // hasRequiredDeps, + hasRequiredFiles + // scanScripts +} = require("./utils/jsdetect"); + +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json", ".eleventy.js"])) return false; + // commented this out because we're not sure if we want to require it + // // REQUIRED DEPS + // if (!hasRequiredDeps(["@11y/eleventy"])) return false; + + return { + type: "eleventy", + port: 8888, + proxyPort: 8080, + env: { ...process.env }, + command: "npx", + possibleArgsArrs: [["eleventy", "--serve", "--watch"]], + urlRegexp: new RegExp(`(http://)([^:]+:)${8080}(/)?`, "g"), + dist: "_site" + }; +}; diff --git a/src/detectors/gatsby.js b/src/detectors/gatsby.js new file mode 100644 index 00000000000..8caff8c705b --- /dev/null +++ b/src/detectors/gatsby.js @@ -0,0 +1,34 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json", "gatsby-config.js"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["gatsby"])) return false; + + /** everything below now assumes that we are within gatsby */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["start", "develop", "dev"], + preferredCommand: "gatsby develop" + }); + + if (possibleArgsArrs.length === 0) { + // ofer to run it when the user doesnt have any scripts setup! 🤯 + possibleArgsArrs.push(["gatsby", "develop"]); + } + return { + type: "gatsby", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 8000, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${8000}(/)?`, "g"), + dist: "public" + }; +}; diff --git a/src/detectors/gridsome.js b/src/detectors/gridsome.js new file mode 100644 index 00000000000..efe026c9ff3 --- /dev/null +++ b/src/detectors/gridsome.js @@ -0,0 +1,30 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json", "gridsome.config.js"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["gridsome"])) return false; + + /** everything below now assumes that we are within gridsome */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["develop"], + preferredCommand: "gridsome develop" + }); + + return { + type: "gridsome", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 8080, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${8080}(/)?`, "g"), + dist: "static" + }; +}; diff --git a/src/detectors/hexo.js b/src/detectors/hexo.js new file mode 100644 index 00000000000..a269a5c9428 --- /dev/null +++ b/src/detectors/hexo.js @@ -0,0 +1,34 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json", "_config.yml"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["hexo"])) return false; + + /** everything below now assumes that we are within gatsby */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["start", "dev", "develop"], + preferredCommand: "hexo server" + }); + + if (possibleArgsArrs.length === 0) { + // ofer to run it when the user doesnt have any scripts setup! 🤯 + possibleArgsArrs.push(["hexo", "server"]); + } + return { + type: "hexo", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 4000, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${4000}(/)?`, "g"), + dist: "public" + }; +}; diff --git a/src/detectors/hugo.js b/src/detectors/hugo.js new file mode 100644 index 00000000000..aac2b8da82f --- /dev/null +++ b/src/detectors/hugo.js @@ -0,0 +1,18 @@ +const { existsSync } = require("fs"); + +module.exports = function() { + if (!existsSync("config.toml") && !existsSync("config.yaml")) { + return false; + } + + return { + type: "hugo", + port: 8888, + proxyPort: 1313, + env: { ...process.env }, + command: "hugo", + possibleArgsArrs: [["server", "-w"]], + urlRegexp: new RegExp(`(http://)([^:]+:)${1313}(/)?`, "g"), + dist: "public" + }; +}; diff --git a/src/detectors/jekyll.js b/src/detectors/jekyll.js new file mode 100644 index 00000000000..88169a6e06b --- /dev/null +++ b/src/detectors/jekyll.js @@ -0,0 +1,18 @@ +const { existsSync } = require("fs"); + +module.exports = function() { + if (!existsSync("_config.yml")) { + return false; + } + + return { + type: "jekyll", + port: 8888, + proxyPort: 4000, + env: { ...process.env }, + command: "bundle", + possibleArgsArrs: [["exec", "jekyll", "serve", "-w", "-l"]], + urlRegexp: new RegExp(`(http://)([^:]+:)${4000}(/)?`, "g"), + dist: "_site" + }; +}; diff --git a/src/detectors/middleman.js b/src/detectors/middleman.js new file mode 100644 index 00000000000..f512bae7bc5 --- /dev/null +++ b/src/detectors/middleman.js @@ -0,0 +1,18 @@ +const { existsSync } = require("fs"); + +module.exports = function() { + if (!existsSync("config.rb")) { + return false; + } + + return { + type: "middleman", + port: 8888, + proxyPort: 4567, + env: { ...process.env }, + command: "bundle", + possibleArgsArrs: [["exec", "middleman", "server"]], + urlRegexp: new RegExp(`(http://)([^:]+:)${4567}(/)?`, "g"), + dist: "build" + }; +}; diff --git a/src/detectors/next.js b/src/detectors/next.js new file mode 100644 index 00000000000..67fbacee8b1 --- /dev/null +++ b/src/detectors/next.js @@ -0,0 +1,34 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["next"])) return false; + + /** everything below now assumes that we are within gatsby */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["dev", "develop", "start"], + preferredCommand: "next" + }); + + if (possibleArgsArrs.length === 0) { + // ofer to run it when the user doesnt have any scripts setup! 🤯 + possibleArgsArrs.push(["next"]); + } + return { + type: "next.js", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 3000, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), + dist: "out" + }; +}; diff --git a/src/detectors/nuxt.js b/src/detectors/nuxt.js new file mode 100644 index 00000000000..f81047223f5 --- /dev/null +++ b/src/detectors/nuxt.js @@ -0,0 +1,36 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); + +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["nuxt"])) return false; + + /** everything below now assumes that we are within vue */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["start", "dev", "run"], + preferredCommand: "nuxt start" + }); + + if (possibleArgsArrs.length === 0) { + // ofer to run it when the user doesnt have any scripts setup! 🤯 + possibleArgsArrs.push(["nuxt", "start"]); + } + + return { + type: "yarn", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 3000, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), + dist: ".nuxt" + }; +}; diff --git a/src/detectors/phenomic.js b/src/detectors/phenomic.js new file mode 100644 index 00000000000..485eb0392eb --- /dev/null +++ b/src/detectors/phenomic.js @@ -0,0 +1,30 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["@phenomic/core"])) return false; + + /** everything below now assumes that we are within gatsby */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["start"], + preferredCommand: "phenomic start" + }); + + return { + type: "phenomic", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 3333, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${3333}(/)?`, "g"), + dist: "public" + }; +}; diff --git a/src/detectors/quasar-v0.17.js b/src/detectors/quasar-v0.17.js new file mode 100644 index 00000000000..23a04a94918 --- /dev/null +++ b/src/detectors/quasar-v0.17.js @@ -0,0 +1,37 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); + +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["quasar-cli"])) return false; + + /** everything below now assumes that we are within Quasar */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["serve", "start", "run", "dev"] + // NOTE: this is comented out as it was picking this up in cordova related scripts. + // preferredCommand: "quasar dev" + }); + + if (possibleArgsArrs.length === 0) { + // ofer to run this default when the user doesnt have any matching scripts setup! + possibleArgsArrs.push(["quasar", "dev"]); + } + + return { + type: "quasar-cli-v0.17", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 8080, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${8080}(/)?`, "g"), + dist: ".quasar" + }; +}; diff --git a/src/detectors/quasar.js b/src/detectors/quasar.js new file mode 100644 index 00000000000..24ec3fb9b89 --- /dev/null +++ b/src/detectors/quasar.js @@ -0,0 +1,37 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); + +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["@quasar/app"])) return false; + + /** everything below now assumes that we are within Quasar */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["serve", "start", "run", "dev"] + // NOTE: this is comented out as it was picking this up in cordova related scripts. + // preferredCommand: "quasar dev" + }); + + if (possibleArgsArrs.length === 0) { + // ofer to run this default when the user doesnt have any matching scripts setup! + possibleArgsArrs.push(["quasar", "dev"]); + } + + return { + type: "quasar-cli", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 8080, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${8080}(/)?`, "g"), + dist: ".quasar" + }; +}; diff --git a/src/detectors/react-static.js b/src/detectors/react-static.js new file mode 100644 index 00000000000..08dfeb6693f --- /dev/null +++ b/src/detectors/react-static.js @@ -0,0 +1,34 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json", "static.config.js"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["react-static"])) return false; + + /** everything below now assumes that we are within react-static */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["start", "develop", "dev"], + preferredCommand: "react-static start" + }); + + if (possibleArgsArrs.length === 0) { + // ofer to run it when the user doesnt have any scripts setup! 🤯 + possibleArgsArrs.push(["react-static", "start"]); + } + return { + type: "react-static", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 3000, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), + dist: "dist" + }; +}; diff --git a/src/detectors/sapper.js b/src/detectors/sapper.js new file mode 100644 index 00000000000..3443b474f20 --- /dev/null +++ b/src/detectors/sapper.js @@ -0,0 +1,36 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); + +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["sapper"])) return false; + + /** everything below now assumes that we are within Sapper */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["dev", "start"], + preferredCommand: "sapper dev" + }); + + if (possibleArgsArrs.length === 0) { + // ofer to run it when the user doesnt have any scripts setup! 🤯 + possibleArgsArrs.push(["sapper", "dev"]); + } + + return { + type: "sapper", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 3000, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), + dist: "static" + }; +}; diff --git a/src/detectors/stencil.js b/src/detectors/stencil.js new file mode 100644 index 00000000000..0ed753ded84 --- /dev/null +++ b/src/detectors/stencil.js @@ -0,0 +1,34 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); + +/** + * detection logic - artificial intelligence! + * */ +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json", "stencil.config.ts"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["@stencil/core"])) return false; + + /** everything below now assumes that we are within stencil */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["start"], + preferredCommand: "stencil build --dev --watch --serve" + }); + + return { + type: "stencil", + command: getYarnOrNPMCommand(), + port: 8888, // the port that the Netlify Dev User will use + proxyPort: 3333, // the port that stencil normally outputs + env: { ...process.env, BROWSER: "none", PORT: 3000 }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), + dist: "www" + }; +}; diff --git a/src/detectors/svelte.js b/src/detectors/svelte.js new file mode 100644 index 00000000000..2cb33f5f09e --- /dev/null +++ b/src/detectors/svelte.js @@ -0,0 +1,38 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); + +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["svelte"])) return false; + // HAS DETECTOR, IT WILL BE PICKED UP BY SAPPER DETECTOR, avoid duplication https://github.com/netlify/cli/issues/347 + if (hasRequiredDeps(["sapper"])) return false; + + /** everything below now assumes that we are within svelte */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["dev", "start", "run"], + preferredCommand: "npm run dev" + }); + + if (possibleArgsArrs.length === 0) { + // ofer to run it when the user doesnt have any scripts setup! 🤯 + possibleArgsArrs.push(["npm", "dev"]); + } + + return { + type: "svelte", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 5000, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${5000}(/)?`, "g"), + dist: "public" + }; +}; diff --git a/src/detectors/utils/jsdetect.js b/src/detectors/utils/jsdetect.js new file mode 100644 index 00000000000..3cd2f820f44 --- /dev/null +++ b/src/detectors/utils/jsdetect.js @@ -0,0 +1,103 @@ +/** + * responsible for any js based projects + * and can therefore build in assumptions that only js projects have + * + */ +const { existsSync, readFileSync } = require("fs"); +let pkgJSON = null; +let yarnExists = false; +let warnedAboutEmptyScript = false; +const { NETLIFYDEVWARN } = require("netlify-cli-logo"); + +/** hold package.json in a singleton so we dont do expensive parsing repeatedly */ +function getPkgJSON() { + if (pkgJSON) { + return pkgJSON; + } + if (!existsSync("package.json")) + throw new Error( + "dont call this method unless you already checked for pkg json" + ); + pkgJSON = JSON.parse(readFileSync("package.json", { encoding: "utf8" })); + return pkgJSON; +} +function getYarnOrNPMCommand() { + if (!yarnExists) { + yarnExists = existsSync("yarn.lock") ? "yes" : "no"; + } + return yarnExists === "yes" ? "yarn" : "npm"; +} + +/** + * real utiltiies are down here + * + */ + +function hasRequiredDeps(requiredDepArray) { + const { dependencies, devDependencies } = getPkgJSON(); + for (let depName of requiredDepArray) { + const hasItInDeps = dependencies && dependencies[depName]; + const hasItInDevDeps = devDependencies && devDependencies[depName]; + if (!hasItInDeps && !hasItInDevDeps) { + return false; + } + } + return true; +} +function hasRequiredFiles(filenameArr) { + for (const filename of filenameArr) { + if (!existsSync(filename)) { + return false; + } + } + return true; +} + +// preferredScriptsArr is in decreasing order of preference +function scanScripts({ preferredScriptsArr, preferredCommand }) { + const { scripts } = getPkgJSON(); + + if (!scripts && !warnedAboutEmptyScript) { + // eslint-disable-next-line no-console + console.log( + `${NETLIFYDEVWARN} You have a package.json without any npm scripts.` + ); + // eslint-disable-next-line no-console + console.log( + `${NETLIFYDEVWARN} Netlify Dev's detector system works best with a script, or you can specify a command to run in the netlify.toml [dev] block ` + ); + warnedAboutEmptyScript = true; // dont spam message with every detector + return []; // not going to match any scripts anyway + } + /** + * + * NOTE: we return an array of arrays (args) + * because we may want to supply extra args in some setups + * + * e.g. ['eleventy', '--serve', '--watch'] + * + * array will in future be sorted by likelihood of what we want + * + * */ + // this is very simplistic logic, we can offer far more intelligent logic later + // eg make a dependency tree of npm scripts and offer the parentest node first + let possibleArgsArrs = preferredScriptsArr + .filter(s => Object.keys(scripts).includes(s)) + .filter(s => !scripts[s].includes("netlify dev")) // prevent netlify dev calling netlify dev + .map(x => [x]); // make into arr of arrs + + Object.entries(scripts) + .filter(([k]) => !preferredScriptsArr.includes(k)) + .forEach(([k, v]) => { + if (v.includes(preferredCommand)) possibleArgsArrs.push([k]); + }); + + return possibleArgsArrs; +} + +module.exports = { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +}; diff --git a/src/detectors/vue.js b/src/detectors/vue.js new file mode 100644 index 00000000000..c69bdb31448 --- /dev/null +++ b/src/detectors/vue.js @@ -0,0 +1,36 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); + +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["@vue/cli-service"])) return false; + + /** everything below now assumes that we are within vue */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["serve", "start", "run"], + preferredCommand: "vue-cli-service serve" + }); + + if (possibleArgsArrs.length === 0) { + // ofer to run it when the user doesnt have any scripts setup! 🤯 + possibleArgsArrs.push(["vue-cli-service", "serve"]); + } + + return { + type: "vue-cli", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 8080, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${8080}(/)?`, "g"), + dist: "dist" + }; +}; diff --git a/src/detectors/vuepress.js b/src/detectors/vuepress.js new file mode 100644 index 00000000000..ef20cf5fb63 --- /dev/null +++ b/src/detectors/vuepress.js @@ -0,0 +1,36 @@ +const { + hasRequiredDeps, + hasRequiredFiles, + getYarnOrNPMCommand, + scanScripts +} = require("./utils/jsdetect"); + +module.exports = function() { + // REQUIRED FILES + if (!hasRequiredFiles(["package.json"])) return false; + // REQUIRED DEPS + if (!hasRequiredDeps(["vuepress"])) return false; + + /** everything below now assumes that we are within vue */ + + const possibleArgsArrs = scanScripts({ + preferredScriptsArr: ["docs:dev", "dev", "run"], + preferredCommand: "vuepress dev" + }); + + if (possibleArgsArrs.length === 0) { + // ofer to run it when the user doesnt have any scripts setup! 🤯 + possibleArgsArrs.push(["vuepress", "dev"]); + } + + return { + type: "vuepress", + command: getYarnOrNPMCommand(), + port: 8888, + proxyPort: 8080, + env: { ...process.env }, + possibleArgsArrs, + urlRegexp: new RegExp(`(http://)([^:]+:)${8080}(/)?`, "g"), + dist: ".vuepress/dist" + }; +}; diff --git a/src/function-builder-detectors/README.md b/src/function-builder-detectors/README.md new file mode 100644 index 00000000000..6ad98e714cb --- /dev/null +++ b/src/function-builder-detectors/README.md @@ -0,0 +1,16 @@ +## function builder detectors + +similar to project detectors, each file here detects function builders. this is so that netlify dev never manages the webpack or other config. the expected output is very simple: + +```js +module.exports = { + src: "functions-source", // source for your functions + build: () => {}, // chokidar will call this to build and rebuild your function + npmScript: "build:functions" // optional, the matching package.json script that calls your function builder +} +``` + +example + +- [src](https://github.com/netlify/netlify-dev-plugin/blob/6a3992746ae490881105fbed2e11ca444f79e44e/src/function-builder-detectors/netlify-lambda.js#L29) +- [npmScript](https://github.com/netlify/netlify-dev-plugin/blob/6a3992746ae490881105fbed2e11ca444f79e44e/src/function-builder-detectors/netlify-lambda.js#L30) diff --git a/src/function-builder-detectors/netlify-lambda.js b/src/function-builder-detectors/netlify-lambda.js new file mode 100644 index 00000000000..55fba88a31c --- /dev/null +++ b/src/function-builder-detectors/netlify-lambda.js @@ -0,0 +1,41 @@ +const { existsSync, readFileSync } = require("fs"); +const execa = require("execa"); + +module.exports = function() { + if (!existsSync("package.json")) { + return false; + } + + const packageSettings = JSON.parse( + readFileSync("package.json", { encoding: "utf8" }) + ); + const { dependencies, devDependencies, scripts } = packageSettings; + if ( + !( + (dependencies && dependencies["netlify-lambda"]) || + (devDependencies && devDependencies["netlify-lambda"]) + ) + ) { + return false; + } + + const yarnExists = existsSync("yarn.lock"); + const settings = {}; + + for (const key in scripts) { + const script = scripts[key]; + const match = script.match(/netlify-lambda build (\S+)/); + if (match) { + settings.src = match[1]; + settings.npmScript = key; + break; + } + } + + if (settings.npmScript) { + settings.build = () => + execa(yarnExists ? "yarn" : "npm", ["run", settings.npmScript]); + settings.builderName = "netlify-lambda"; + return settings; + } +}; diff --git a/src/functions-templates/js/README.md b/src/functions-templates/js/README.md new file mode 100644 index 00000000000..ddcb4518c21 --- /dev/null +++ b/src/functions-templates/js/README.md @@ -0,0 +1,36 @@ +## note to devs + +place new templates here and our CLI will pick it up. each template must be in its own folder. + +## not a long term solution + +we dont want people to update their CLI every time we add a template. see https://github.com/netlify/netlify-dev-plugin/issues/42 for how we may solve in future + +## template lifecycles + +- onComplete + - meant for messages, logging, light cleanup +- onAllAddonsInstalled? + - not implemented yet + - meant for heavier work, but not sure if different from onComplete + +## template addons + +specify an array of objects of this shape: + +```ts +{ + addonName: String, + addonDidInstall?: Function // for executing arbitrary postinstall code for a SINGLE addon +} +``` + +## why place templates in a separate folder + +we dont colocate this inside `src/commands/functions` because oclif will think it's a new command. + +every function should be registered with their respective `template-registry.js`. + +## typescript and go + +we have some templates here but they are unused for now until Netlify Dev supports them. diff --git a/src/functions-templates/js/apollo-graphql-rest/.netlify-function-template.js b/src/functions-templates/js/apollo-graphql-rest/.netlify-function-template.js new file mode 100644 index 00000000000..29e6b9ed294 --- /dev/null +++ b/src/functions-templates/js/apollo-graphql-rest/.netlify-function-template.js @@ -0,0 +1,5 @@ +module.exports = { + name: "apollo-graphql-rest", + description: + "GraphQL function to wrap REST API using apollo-server-lambda and apollo-datasource-rest!" +}; diff --git a/src/functions-templates/js/apollo-graphql-rest/apollo-graphql-rest.js b/src/functions-templates/js/apollo-graphql-rest/apollo-graphql-rest.js new file mode 100644 index 00000000000..7c12b2fd2eb --- /dev/null +++ b/src/functions-templates/js/apollo-graphql-rest/apollo-graphql-rest.js @@ -0,0 +1,68 @@ +/* eslint-disable */ +const { ApolloServer, gql } = require("apollo-server-lambda"); +const RandomUser = require("./random-user.js"); +// example from: https://medium.com/yld-engineering-blog/easier-graphql-wrappers-for-your-rest-apis-1410b0b5446d + +const typeDefs = gql` + """ + Example Description for Name Type + + It's multiline and you can use **markdown**! [more docs](https://www.apollographql.com/docs/apollo-server/essentials/schema#documentation)! + """ + type Name { + "Description for first" + title: String + "Description for title" + first: String + "Description for last" + last: String + } + type Location { + street: String + city: String + state: String + postcode: String + } + type Picture { + large: String + medium: String + thumbnail: String + } + type User { + gender: String + name: Name + location: Location + email: String + phone: String + cell: String + picture: Picture + nat: String + } + type Query { + """ + Example Description for getUser + + It's multiline and you can use **markdown**! + """ + getUser(gender: String): User + getUsers(people: Int, gender: String): [User] + } +`; +const resolvers = { + Query: { + getUser: async (_, { gender }, { dataSources }) => + dataSources.RandomUser.getUser(gender), + getUsers: async (_, { people, gender }, { dataSources }) => + dataSources.RandomUser.getUsers(people, gender) + } +}; + +const server = new ApolloServer({ + typeDefs, + resolvers, + dataSources: () => ({ + RandomUser: new RandomUser() + }) +}); + +exports.handler = server.createHandler(); diff --git a/src/functions-templates/js/apollo-graphql-rest/package.json b/src/functions-templates/js/apollo-graphql-rest/package.json new file mode 100644 index 00000000000..866c939d874 --- /dev/null +++ b/src/functions-templates/js/apollo-graphql-rest/package.json @@ -0,0 +1,22 @@ +{ + "name": "apollo-graphql-rest", + "version": "1.0.0", + "description": "netlify functions:create - GraphQL function to wrap REST API using apollo-server-lambda and apollo-datasource-rest!", + "main": "apollo-graphql-rest.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js", + "apollo" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "apollo-server-lambda": "^2.4.8", + "apollo-datasource-rest": "^0.3.2", + "graphql": "^14.1.1" + } +} diff --git a/src/functions-templates/js/apollo-graphql-rest/random-user.js b/src/functions-templates/js/apollo-graphql-rest/random-user.js new file mode 100644 index 00000000000..a81447f7107 --- /dev/null +++ b/src/functions-templates/js/apollo-graphql-rest/random-user.js @@ -0,0 +1,20 @@ +const { RESTDataSource } = require("apollo-datasource-rest"); + +class RandomUser extends RESTDataSource { + constructor() { + super(); + this.baseURL = "https://randomuser.me/api"; + } + + async getUser(gender = "all") { + const user = await this.get(`/?gender=${gender}`); + return user.results[0]; + } + + async getUsers(people = 10, gender = "all") { + const user = await this.get(`/?results=${people}&gender=${gender}`); + return user.results; + } +} + +module.exports = RandomUser; diff --git a/src/functions-templates/js/apollo-graphql/.netlify-function-template.js b/src/functions-templates/js/apollo-graphql/.netlify-function-template.js new file mode 100644 index 00000000000..992deeab3fd --- /dev/null +++ b/src/functions-templates/js/apollo-graphql/.netlify-function-template.js @@ -0,0 +1,4 @@ +module.exports = { + name: "apollo-graphql", + description: "GraphQL function using Apollo-Server-Lambda!" +}; diff --git a/src/functions-templates/js/apollo-graphql/apollo-graphql.js b/src/functions-templates/js/apollo-graphql/apollo-graphql.js new file mode 100644 index 00000000000..655a058edff --- /dev/null +++ b/src/functions-templates/js/apollo-graphql/apollo-graphql.js @@ -0,0 +1,46 @@ +const { ApolloServer, gql } = require("apollo-server-lambda"); + +const typeDefs = gql` + type Query { + hello: String + allAuthors: [Author!] + author(id: Int!): Author + authorByName(name: String!): Author + } + type Author { + id: ID! + name: String! + married: Boolean! + } +`; + +const authors = [ + { id: 1, name: "Terry Pratchett", married: false }, + { id: 2, name: "Stephen King", married: true }, + { id: 3, name: "JK Rowling", married: false } +]; + +const resolvers = { + Query: { + hello: (root, args, context) => { + return "Hello, world!"; + }, + allAuthors: (root, args, context) => { + return authors; + }, + author: (root, args, context) => { + return; + }, + authorByName: (root, args, context) => { + console.log("hihhihi", args.name); + return authors.find(x => x.name === args.name) || "NOTFOUND"; + } + } +}; + +const server = new ApolloServer({ + typeDefs, + resolvers +}); + +exports.handler = server.createHandler(); diff --git a/src/functions-templates/js/apollo-graphql/package.json b/src/functions-templates/js/apollo-graphql/package.json new file mode 100644 index 00000000000..3707c1636e8 --- /dev/null +++ b/src/functions-templates/js/apollo-graphql/package.json @@ -0,0 +1,21 @@ +{ + "name": "apollo-graphql", + "version": "1.0.0", + "description": "netlify functions:create - set up for apollo graphql", + "main": "apollo-graphql.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js", + "apollo" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "apollo-server-lambda": "^2.4.8", + "graphql": "^14.1.1" + } +} diff --git a/src/functions-templates/js/auth-fetch/.netlify-function-template.js b/src/functions-templates/js/auth-fetch/.netlify-function-template.js new file mode 100644 index 00000000000..bf60b2ac18a --- /dev/null +++ b/src/functions-templates/js/auth-fetch/.netlify-function-template.js @@ -0,0 +1,10 @@ +module.exports = { + name: "auth-fetch", + description: "Use `node-fetch` library and Netlify Identity to access APIs", + onComplete() { + console.log(`auth-fetch function created from template!`); + console.log( + "REMINDER: Make sure to call this function with the Netlify Identity JWT. See https://netlify-gotrue-in-react.netlify.com/ for demo" + ); + } +}; diff --git a/src/functions-templates/js/auth-fetch/auth-fetch.js b/src/functions-templates/js/auth-fetch/auth-fetch.js new file mode 100644 index 00000000000..77f20611455 --- /dev/null +++ b/src/functions-templates/js/auth-fetch/auth-fetch.js @@ -0,0 +1,34 @@ +/* eslint-disable */ +// for a full working demo of Netlify Identity + Functions, see https://netlify-gotrue-in-react.netlify.com/ + +const fetch = require("node-fetch"); +exports.handler = async function(event, context) { + if (!context.clientContext && !context.clientContext.identity) { + return { + statusCode: 500, + body: JSON.stringify({ + msg: "No identity instance detected. Did you enable it?" + }) // Could be a custom message or object i.e. JSON.stringify(err) + }; + } + const { identity, user } = context.clientContext; + try { + const response = await fetch("https://api.chucknorris.io/jokes/random"); + if (!response.ok) { + // NOT res.status >= 200 && res.status < 300 + return { statusCode: response.status, body: response.statusText }; + } + const data = await response.json(); + + return { + statusCode: 200, + body: JSON.stringify({ identity, user, msg: data.value }) + }; + } catch (err) { + console.log(err); // output to netlify function log + return { + statusCode: 500, + body: JSON.stringify({ msg: err.message }) // Could be a custom message or object i.e. JSON.stringify(err) + }; + } +}; diff --git a/src/functions-templates/js/auth-fetch/package-lock.json b/src/functions-templates/js/auth-fetch/package-lock.json new file mode 100644 index 00000000000..6cc2723eef7 --- /dev/null +++ b/src/functions-templates/js/auth-fetch/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "auth-fetch", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "node-fetch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", + "integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==" + } + } +} diff --git a/src/functions-templates/js/auth-fetch/package.json b/src/functions-templates/js/auth-fetch/package.json new file mode 100644 index 00000000000..ba2df044c92 --- /dev/null +++ b/src/functions-templates/js/auth-fetch/package.json @@ -0,0 +1,21 @@ +{ + "name": "auth-fetch", + "version": "1.0.0", + "description": "netlify functions:create - default template for auth fetch function", + "main": "auth-fetch.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js", + "identity", + "authentication" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.3.0" + } +} diff --git a/src/functions-templates/js/create-user/.netlify-function-template.js b/src/functions-templates/js/create-user/.netlify-function-template.js new file mode 100644 index 00000000000..030d6a2f543 --- /dev/null +++ b/src/functions-templates/js/create-user/.netlify-function-template.js @@ -0,0 +1,11 @@ +module.exports = { + name: "create-user", + description: + "Programmatically create a Netlify Identity user by invoking a function", + onComplete() { + console.log(`create-user function created from template!`); + console.log( + "REMINDER: Make sure to call this function with a Netlify Identity JWT. See https://netlify-gotrue-in-react.netlify.com/ for demo" + ); + } +}; diff --git a/src/functions-templates/js/create-user/create-user.js b/src/functions-templates/js/create-user/create-user.js new file mode 100644 index 00000000000..91b46f17e55 --- /dev/null +++ b/src/functions-templates/js/create-user/create-user.js @@ -0,0 +1,35 @@ +const fetch = require("node-fetch"); + +exports.handler = async (event, context) => { + if (event.httpMethod !== "POST") + return { statusCode: 400, body: "Must POST to this function" }; + + // send account information along with the POST + const { email, password, full_name } = JSON.parse(event.body); + if (!email) return { statusCode: 400, body: "email missing" }; + if (!password) return { statusCode: 400, body: "password missing" }; + if (!full_name) return { statusCode: 400, body: "full_name missing" }; + + // identity.token is a short lived admin token which + // is provided to all Netlify Functions to interact + // with the Identity API + const { identity } = context.clientContext; + + await fetch(`${identity.url}/admin/users`, { + method: "POST", + headers: { Authorization: `Bearer ${identity.token}` }, + body: JSON.stringify({ + email, + password, + confirm: true, + user_metadata: { + full_name + } + }) + }); + + return { + statusCode: 200, + body: "success!" + }; +}; diff --git a/src/functions-templates/js/create-user/package.json b/src/functions-templates/js/create-user/package.json new file mode 100644 index 00000000000..b36a1a778dc --- /dev/null +++ b/src/functions-templates/js/create-user/package.json @@ -0,0 +1,21 @@ +{ + "name": "create-user", + "version": "1.0.0", + "description": "netlify functions:create - Programmatically create a Netlify Identity user by invoking a function", + "main": "create-user.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js", + "identity", + "authentication" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.3.0" + } +} diff --git a/src/functions-templates/js/fauna-crud/.netlify-function-template.js b/src/functions-templates/js/fauna-crud/.netlify-function-template.js new file mode 100644 index 00000000000..9b8e462a93d --- /dev/null +++ b/src/functions-templates/js/fauna-crud/.netlify-function-template.js @@ -0,0 +1,16 @@ +const execa = require("execa"); +module.exports = { + name: "fauna-crud", + description: "CRUD function using Fauna DB", + addons: [ + { + addonName: "fauna", + addonDidInstall(fnPath) { + execa.sync(fnPath + "/create-schema.js", undefined, { + env: process.env, + stdio: "inherit" + }); + } + } + ] +}; diff --git a/src/functions-templates/js/fauna-crud/create-schema.js b/src/functions-templates/js/fauna-crud/create-schema.js new file mode 100755 index 00000000000..19c60a1834a --- /dev/null +++ b/src/functions-templates/js/fauna-crud/create-schema.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node + +/* bootstrap database in your FaunaDB account - use with `netlify dev:exec ` */ +const faunadb = require("faunadb"); + +const q = faunadb.query; + +function createFaunaDB() { + if (!process.env.FAUNADB_SERVER_SECRET) { + console.log("No FAUNADB_SERVER_SECRET in environment, skipping DB setup"); + } + console.log("Create the database!"); + const client = new faunadb.Client({ + secret: process.env.FAUNADB_SERVER_SECRET + }); + + /* Based on your requirements, change the schema here */ + return client + .query(q.Create(q.Ref("classes"), { name: "items" })) + .then(() => { + console.log("Created items class"); + return client.query( + q.Create(q.Ref("indexes"), { + name: "all_items", + source: q.Ref("classes/items"), + active: true + }) + ); + }) + + .catch(e => { + if ( + e.requestResult.statusCode === 400 && + e.message === "instance not unique" + ) { + console.log("DB already exists"); + } + throw e; + }); +} + +createFaunaDB(); diff --git a/src/functions-templates/js/fauna-crud/create.js b/src/functions-templates/js/fauna-crud/create.js new file mode 100644 index 00000000000..47cd9791112 --- /dev/null +++ b/src/functions-templates/js/fauna-crud/create.js @@ -0,0 +1,36 @@ +const faunadb = require("faunadb"); + +/* configure faunaDB Client with our secret */ +const q = faunadb.query; +const client = new faunadb.Client({ + secret: process.env.FAUNADB_SERVER_SECRET +}); + +/* export our lambda function as named "handler" export */ +exports.handler = async (event, context) => { + /* parse the string body into a useable JS object */ + const data = JSON.parse(event.body); + console.log("Function `create` invoked", data); + const item = { + data: data + }; + /* construct the fauna query */ + return client + .query(q.Create(q.Ref("classes/items"), item)) + .then(response => { + console.log("success", response); + /* Success! return the response with statusCode 200 */ + return { + statusCode: 200, + body: JSON.stringify(response) + }; + }) + .catch(error => { + console.log("error", error); + /* Error! return the error with statusCode 400 */ + return { + statusCode: 400, + body: JSON.stringify(error) + }; + }); +}; diff --git a/src/functions-templates/js/fauna-crud/delete.js b/src/functions-templates/js/fauna-crud/delete.js new file mode 100644 index 00000000000..f21c7ab8225 --- /dev/null +++ b/src/functions-templates/js/fauna-crud/delete.js @@ -0,0 +1,28 @@ +/* Import faunaDB sdk */ +const faunadb = require("faunadb"); + +const q = faunadb.query; +const client = new faunadb.Client({ + secret: process.env.FAUNADB_SERVER_SECRET +}); + +exports.handler = async (event, context) => { + const id = event.id; + console.log(`Function 'delete' invoked. delete id: ${id}`); + return client + .query(q.Delete(q.Ref(`classes/items/${id}`))) + .then(response => { + console.log("success", response); + return { + statusCode: 200, + body: JSON.stringify(response) + }; + }) + .catch(error => { + console.log("error", error); + return { + statusCode: 400, + body: JSON.stringify(error) + }; + }); +}; diff --git a/src/functions-templates/js/fauna-crud/fauna-crud.js b/src/functions-templates/js/fauna-crud/fauna-crud.js new file mode 100644 index 00000000000..3d54dbca5d7 --- /dev/null +++ b/src/functions-templates/js/fauna-crud/fauna-crud.js @@ -0,0 +1,55 @@ +/* eslint-disable */ +exports.handler = async (event, context) => { + const path = event.path.replace(/\.netlify\/functions\/[^\/]+/, ""); + const segments = path.split("/").filter(e => e); + + switch (event.httpMethod) { + case "GET": + // e.g. GET /.netlify/functions/fauna-crud + if (segments.length === 0) { + return require("./read-all").handler(event, context); + } + // e.g. GET /.netlify/functions/fauna-crud/123456 + if (segments.length === 1) { + event.id = segments[0]; + return require("./read").handler(event, context); + } else { + return { + statusCode: 500, + body: + "too many segments in GET request, must be either /.netlify/functions/fauna-crud or /.netlify/functions/fauna-crud/123456" + }; + } + case "POST": + // e.g. POST /.netlify/functions/fauna-crud with a body of key value pair objects, NOT strings + return require("./create").handler(event, context); + case "PUT": + // e.g. PUT /.netlify/functions/fauna-crud/123456 with a body of key value pair objects, NOT strings + if (segments.length === 1) { + event.id = segments[0]; + return require("./update").handler(event, context); + } else { + return { + statusCode: 500, + body: + "invalid segments in POST request, must be /.netlify/functions/fauna-crud/123456" + }; + } + case "DELETE": + // e.g. DELETE /.netlify/functions/fauna-crud/123456 + if (segments.length === 1) { + event.id = segments[0]; + return require("./delete").handler(event, context); + } else { + return { + statusCode: 500, + body: + "invalid segments in DELETE request, must be /.netlify/functions/fauna-crud/123456" + }; + } + } + return { + statusCode: 500, + body: "unrecognized HTTP Method, must be one of GET/POST/PUT/DELETE" + }; +}; diff --git a/src/functions-templates/js/fauna-crud/package.json b/src/functions-templates/js/fauna-crud/package.json new file mode 100644 index 00000000000..c6b343047e4 --- /dev/null +++ b/src/functions-templates/js/fauna-crud/package.json @@ -0,0 +1,20 @@ +{ + "name": "fauna-crud", + "version": "1.0.0", + "description": "netlify functions:create - CRUD functionality with Fauna DB", + "main": "fauna-crud.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js", + "faunadb" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "faunadb": "^2.6.1" + } +} diff --git a/src/functions-templates/js/fauna-crud/read-all.js b/src/functions-templates/js/fauna-crud/read-all.js new file mode 100644 index 00000000000..e271280b277 --- /dev/null +++ b/src/functions-templates/js/fauna-crud/read-all.js @@ -0,0 +1,34 @@ +/* Import faunaDB sdk */ +const faunadb = require("faunadb"); + +const q = faunadb.query; +const client = new faunadb.Client({ + secret: process.env.FAUNADB_SERVER_SECRET +}); + +exports.handler = async (event, context) => { + console.log("Function `read-all` invoked"); + return client + .query(q.Paginate(q.Match(q.Ref("indexes/all_items")))) + .then(response => { + const itemRefs = response.data; + // create new query out of item refs. http://bit.ly/2LG3MLg + const getAllItemsDataQuery = itemRefs.map(ref => { + return q.Get(ref); + }); + // then query the refs + return client.query(getAllItemsDataQuery).then(ret => { + return { + statusCode: 200, + body: JSON.stringify(ret) + }; + }); + }) + .catch(error => { + console.log("error", error); + return { + statusCode: 400, + body: JSON.stringify(error) + }; + }); +}; diff --git a/src/functions-templates/js/fauna-crud/read.js b/src/functions-templates/js/fauna-crud/read.js new file mode 100644 index 00000000000..84efa1706d3 --- /dev/null +++ b/src/functions-templates/js/fauna-crud/read.js @@ -0,0 +1,28 @@ +/* Import faunaDB sdk */ +const faunadb = require("faunadb"); + +const q = faunadb.query; +const client = new faunadb.Client({ + secret: process.env.FAUNADB_SERVER_SECRET +}); + +exports.handler = async (event, context) => { + const id = event.id; + console.log(`Function 'read' invoked. Read id: ${id}`); + return client + .query(q.Get(q.Ref(`classes/items/${id}`))) + .then(response => { + console.log("success", response); + return { + statusCode: 200, + body: JSON.stringify(response) + }; + }) + .catch(error => { + console.log("error", error); + return { + statusCode: 400, + body: JSON.stringify(error) + }; + }); +}; diff --git a/src/functions-templates/js/fauna-crud/update.js b/src/functions-templates/js/fauna-crud/update.js new file mode 100644 index 00000000000..ee563defb03 --- /dev/null +++ b/src/functions-templates/js/fauna-crud/update.js @@ -0,0 +1,29 @@ +/* Import faunaDB sdk */ +const faunadb = require("faunadb"); + +const q = faunadb.query; +const client = new faunadb.Client({ + secret: process.env.FAUNADB_SERVER_SECRET +}); + +exports.handler = async (event, context) => { + const data = JSON.parse(event.body); + const id = event.id; + console.log(`Function 'update' invoked. update id: ${id}`); + return client + .query(q.Update(q.Ref(`classes/items/${id}`), { data })) + .then(response => { + console.log("success", response); + return { + statusCode: 200, + body: JSON.stringify(response) + }; + }) + .catch(error => { + console.log("error", error); + return { + statusCode: 400, + body: JSON.stringify(error) + }; + }); +}; diff --git a/src/functions-templates/js/fauna-graphql/.netlify-function-template.js b/src/functions-templates/js/fauna-graphql/.netlify-function-template.js new file mode 100644 index 00000000000..9ae35c2199f --- /dev/null +++ b/src/functions-templates/js/fauna-graphql/.netlify-function-template.js @@ -0,0 +1,16 @@ +const execa = require("execa"); +module.exports = { + name: "fauna-graphql", + description: "GraphQL Backend using Fauna DB", + addons: [ + { + addonName: "fauna", + addonDidInstall(fnPath) { + execa.sync(fnPath + "/sync-schema.js", undefined, { + env: process.env, + stdio: "inherit" + }); + } + } + ] +}; diff --git a/src/functions-templates/js/fauna-graphql/fauna-graphql.js b/src/functions-templates/js/fauna-graphql/fauna-graphql.js new file mode 100644 index 00000000000..9509cfa2424 --- /dev/null +++ b/src/functions-templates/js/fauna-graphql/fauna-graphql.js @@ -0,0 +1,45 @@ +const { ApolloServer, gql } = require("apollo-server-lambda"); +const { createHttpLink } = require("apollo-link-http"); +const fetch = require("node-fetch"); +const { + introspectSchema, + makeRemoteExecutableSchema +} = require("graphql-tools"); + +exports.handler = async function(event, context) { + /** required for Fauna GraphQL auth */ + if (!process.env.FAUNADB_SERVER_SECRET) { + const msg = ` + FAUNADB_SERVER_SECRET missing. + Did you forget to install the fauna addon or forgot to run inside Netlify Dev? + `; + console.error(msg); + return { + statusCode: 500, + body: JSON.stringify({ msg }) + }; + } + const b64encodedSecret = Buffer.from( + process.env.FAUNADB_SERVER_SECRET + ":" // weird but they + ).toString("base64"); + const headers = { Authorization: `Basic ${b64encodedSecret}` }; + + /** standard creation of apollo-server executable schema */ + const link = createHttpLink({ + uri: "https://graphql.fauna.com/graphql", // modify as you see fit + fetch, + headers + }); + const schema = await introspectSchema(link); + const executableSchema = makeRemoteExecutableSchema({ + schema, + link + }); + const server = new ApolloServer({ + schema: executableSchema + }); + return new Promise((yay, nay) => { + const cb = (err, args) => (err ? nay(err) : yay(args)); + server.createHandler()(event, context, cb); + }); +}; diff --git a/src/functions-templates/js/fauna-graphql/package.json b/src/functions-templates/js/fauna-graphql/package.json new file mode 100644 index 00000000000..98fb0212940 --- /dev/null +++ b/src/functions-templates/js/fauna-graphql/package.json @@ -0,0 +1,26 @@ +{ + "name": "fauna-graphql", + "version": "1.0.0", + "description": "netlify functions:create - set up for fauna db + apollo graphql", + "main": "fauna-graphql.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js", + "apollo", + "fauna" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "apollo-link-http": "^1.5.14", + "apollo-link-context": "^1.0.17", + "apollo-server-lambda": "^2.4.8", + "graphql": "^14.1.1", + "graphql-tools": "^4.0.4", + "node-fetch": "^2.3.0" + } +} diff --git a/src/functions-templates/js/fauna-graphql/schema.graphql b/src/functions-templates/js/fauna-graphql/schema.graphql new file mode 100644 index 00000000000..c90dad504cc --- /dev/null +++ b/src/functions-templates/js/fauna-graphql/schema.graphql @@ -0,0 +1,8 @@ +type Todo { + title: String! + completed: Boolean! +} +type Query { + allTodos: [Todo!] + todosByCompletedFlag(completed: Boolean!): [Todo!] +} diff --git a/src/functions-templates/js/fauna-graphql/sync-schema.js b/src/functions-templates/js/fauna-graphql/sync-schema.js new file mode 100644 index 00000000000..aa146d9871b --- /dev/null +++ b/src/functions-templates/js/fauna-graphql/sync-schema.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +/* sync GraphQL schema to your FaunaDB account - use with `netlify dev:exec ` */ +function createFaunaGraphQL() { + if (!process.env.FAUNADB_SERVER_SECRET) { + console.log("No FAUNADB_SERVER_SECRET in environment, skipping DB setup"); + } + console.log("Upload GraphQL Schema!"); + + const fetch = require("node-fetch"); + const fs = require("fs"); + const path = require("path"); + var dataString = fs + .readFileSync(path.join(__dirname, "schema.graphql")) + .toString(); // name of your schema file + + // encoded authorization header similar to https://www.npmjs.com/package/request#http-authentication + const token = Buffer.from( + process.env.FAUNADB_SERVER_SECRET + ":" + ).toString("base64"); + + var options = { + method: "POST", + body: dataString, + headers: { Authorization: `Basic ${token}` } + }; + + fetch("https://graphql.fauna.com/import", options) + // // uncomment for debugging + .then(res => res.text()) + .then(body => { + console.log( + "Netlify Functions:Create - `fauna-graphql/sync-schema.js` success!" + ); + console.log(body); + }) + .catch(err => console.error("something wrong happened: ", { err })); +} + +createFaunaGraphQL(); diff --git a/src/functions-templates/js/google-analytics/.netlify-function-template.js b/src/functions-templates/js/google-analytics/.netlify-function-template.js new file mode 100644 index 00000000000..97672eacf26 --- /dev/null +++ b/src/functions-templates/js/google-analytics/.netlify-function-template.js @@ -0,0 +1,4 @@ +module.exports = { + name: "google-analytics", + description: "Google Analytics: proxy for GA on your domain to avoid adblock" +}; diff --git a/src/functions-templates/js/google-analytics/google-analytics.js b/src/functions-templates/js/google-analytics/google-analytics.js new file mode 100644 index 00000000000..cbad15cc745 --- /dev/null +++ b/src/functions-templates/js/google-analytics/google-analytics.js @@ -0,0 +1,129 @@ +// with thanks to https://github.com/codeniko/simple-tracker/blob/master/examples/server-examples/aws-lambda/google-analytics.js +const request = require("request"); +const querystring = require("querystring"); +const uuidv4 = require("uuid/v4"); + +const GA_ENDPOINT = `https://www.google-analytics.com/collect`; + +// Domains to whitelist. Replace with your own! +const originWhitelist = []; // keep this empty and append domains to whitelist using whiteListDomain() +whitelistDomain("test.com"); +whitelistDomain("nfeld.com"); + +function whitelistDomain(domain, addWww = true) { + const prefixes = ["https://", "http://"]; + if (addWww) { + prefixes.push("https://www."); + prefixes.push("http://www."); + } + prefixes.forEach(prefix => originWhitelist.push(prefix + domain)); +} + +function proxyToGoogleAnalytics(event, done) { + // get GA params whether GET or POST request + const params = + event.httpMethod.toUpperCase() === "GET" + ? event.queryStringParameters + : JSON.parse(event.body); + const headers = event.headers || {}; + + // attach other GA params, required for IP address since client doesn't have access to it. UA and CID can be sent from client + params.uip = headers["x-forwarded-for"] || headers["x-bb-ip"] || ""; // ip override. Look into headers for clients IP address, as opposed to IP address of host running lambda function + params.ua = params.ua || headers["user-agent"] || ""; // user agent override + params.cid = params.cid || uuidv4(); // REQUIRED: use given cid, or generate a new one as last resort. Generating should be avoided because one user can show up in GA multiple times. If user refresh page `n` times, you'll get `n` pageviews logged into GA from "different" users. Client should generate a uuid and store in cookies, local storage, or generate a fingerprint. Check simple-tracker client example + + console.info("proxying params:", params); + const qs = querystring.stringify(params); + + const reqOptions = { + method: "POST", + headers: { + "Content-Type": "image/gif" + }, + url: GA_ENDPOINT, + body: qs + }; + + request(reqOptions, (error, result) => { + if (error) { + console.info("googleanalytics error!", error); + } else { + console.info( + "googleanalytics status code", + result.statusCode, + result.statusMessage + ); + } + }); + + done(); +} + +exports.handler = function(event, context, callback) { + const origin = event.headers["origin"] || event.headers["Origin"] || ""; + console.log(`Received ${event.httpMethod} request from, origin: ${origin}`); + + const isOriginWhitelisted = originWhitelist.indexOf(origin) >= 0; + console.info("is whitelisted?", isOriginWhitelisted); + + const headers = { + //'Access-Control-Allow-Origin': '*', // allow all domains to POST. Use for localhost development only + "Access-Control-Allow-Origin": isOriginWhitelisted + ? origin + : originWhitelist[0], + "Access-Control-Allow-Methods": "GET,POST,OPTIONS", + "Access-Control-Allow-Headers": "Content-Type,Accept" + }; + + const done = () => { + callback(null, { + statusCode: 200, + headers, + body: "" + }); + }; + + const httpMethod = event.httpMethod.toUpperCase(); + + if (event.httpMethod === "OPTIONS") { + // CORS (required if you use a different subdomain to host this function, or a different domain entirely) + done(); + } else if ( + (httpMethod === "GET" || httpMethod === "POST") && + isOriginWhitelisted + ) { + // allow GET or POST, but only for whitelisted domains + proxyToGoogleAnalytics(event, done); + } else { + callback("Not found"); + } +}; + +/* + Docs on GA endpoint and example params + + https://developers.google.com/analytics/devguides/collection/protocol/v1/devguide + +v: 1 +_v: j67 +a: 751874410 +t: pageview +_s: 1 +dl: https://nfeld.com/contact.html +dr: https://google.com +ul: en-us +de: UTF-8 +dt: Nikolay Feldman - Software Engineer +sd: 24-bit +sr: 1440x900 +vp: 945x777 +je: 0 +_u: blabla~ +jid: +gjid: +cid: 1837873423.1522911810 +tid: UA-116530991-1 +_gid: 1828045325.1524815793 +gtm: u4d +z: 1379041260 +*/ diff --git a/src/functions-templates/js/google-analytics/package-lock.json b/src/functions-templates/js/google-analytics/package-lock.json new file mode 100644 index 00000000000..9bd0c933e3c --- /dev/null +++ b/src/functions-templates/js/google-analytics/package-lock.json @@ -0,0 +1,351 @@ +{ + "name": "google-analytics", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "mime-db": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + }, + "mime-types": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "requires": { + "mime-db": "~1.38.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + } +} diff --git a/src/functions-templates/js/google-analytics/package.json b/src/functions-templates/js/google-analytics/package.json new file mode 100644 index 00000000000..a12b9b59221 --- /dev/null +++ b/src/functions-templates/js/google-analytics/package.json @@ -0,0 +1,23 @@ +{ + "name": "google-analytics", + "version": "1.0.0", + "description": "netlify functions:create - Google Analytics: proxy for GA on your domain to avoid adblock", + "main": "google-analytics.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "apis", + "email", + "js" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "querystring": "^0.2.0", + "request": "^2.88.0", + "uuid": "^3.3.2" + } +} diff --git a/src/functions-templates/js/graphql-gateway/.netlify-function-template.js b/src/functions-templates/js/graphql-gateway/.netlify-function-template.js new file mode 100644 index 00000000000..fe34af1f346 --- /dev/null +++ b/src/functions-templates/js/graphql-gateway/.netlify-function-template.js @@ -0,0 +1,5 @@ +module.exports = { + name: "graphql-gateway", + description: + "Apollo Server Lambda Gateway stitching schemas from other GraphQL Functions!" +}; diff --git a/src/functions-templates/js/graphql-gateway/example-sibling-function-graphql-1.js b/src/functions-templates/js/graphql-gateway/example-sibling-function-graphql-1.js new file mode 100644 index 00000000000..249b6b9d0c2 --- /dev/null +++ b/src/functions-templates/js/graphql-gateway/example-sibling-function-graphql-1.js @@ -0,0 +1,48 @@ +// not meant to be run inside the graqhql-gateway function +// but just shows a copy-pastable example sibling function +// that would work with graphql-gateway +const { ApolloServer, gql } = require("apollo-server-lambda"); + +const typeDefs = gql` + type Query { + hello: String + allAuthors: [Author!] + author(id: Int!): Author + authorByName(name: String!): Author + } + type Author { + id: ID! + name: String! + age: Int! + } +`; + +const authors = [ + { id: 1, name: "Terry Pratchett", age: 67 }, + { id: 2, name: "Stephen King", age: 71 }, + { id: 3, name: "JK Rowling", age: 53 } +]; + +const resolvers = { + Query: { + hello: (root, args, context) => { + return "Hello, world!"; + }, + allAuthors: (root, args, context) => { + return authors; + }, + author: (root, args, context) => { + return; + }, + authorByName: (root, args, context) => { + return authors.find(x => x.name === args.name) || "NOTFOUND"; + } + } +}; + +const server = new ApolloServer({ + typeDefs, + resolvers +}); + +exports.handler = server.createHandler(); diff --git a/src/functions-templates/js/graphql-gateway/example-sibling-function-graphql-2.js b/src/functions-templates/js/graphql-gateway/example-sibling-function-graphql-2.js new file mode 100644 index 00000000000..7ee3beaf0e1 --- /dev/null +++ b/src/functions-templates/js/graphql-gateway/example-sibling-function-graphql-2.js @@ -0,0 +1,84 @@ +// not meant to be run inside the graqhql-gateway function +// but just shows a copy-pastable example sibling function +// that would work with graphql-gateway +const { ApolloServer, gql } = require("apollo-server-lambda"); + +const typeDefs = gql` + type Query { + hello: String + allBooks: [Book] + book(id: Int!): Book + } + type Book { + id: ID! + year: Int! + title: String! + authorName: String! + } +`; + +const books = [ + { + id: 1, + title: "The Philosopher's Stone", + year: 1997, + authorName: "JK Rowling" + }, + { + id: 2, + title: "Pet Sematary", + year: 1983, + authorName: "Stephen King" + }, + { + id: 3, + title: "Going Postal", + year: 2004, + authorName: "Terry Pratchett" + }, + { + id: 4, + title: "Small Gods", + year: 1992, + authorName: "Terry Pratchett" + }, + { + id: 5, + title: "Night Watch", + year: 2002, + authorName: "Terry Pratchett" + }, + { + id: 6, + title: "The Shining", + year: 1977, + authorName: "Stephen King" + }, + { + id: 7, + title: "The Deathly Hallows", + year: 2007, + authorName: "JK Rowling" + } +]; + +const resolvers = { + Query: { + hello: (root, args, context) => { + return "Hello, world!"; + }, + allBooks: (root, args, context) => { + return books; + }, + book: (root, args, context) => { + return find(books, { id: args.id }); + } + } +}; + +const server = new ApolloServer({ + typeDefs, + resolvers +}); + +exports.handler = server.createHandler(); diff --git a/src/functions-templates/js/graphql-gateway/graphql-gateway.js b/src/functions-templates/js/graphql-gateway/graphql-gateway.js new file mode 100644 index 00000000000..8de5ede1870 --- /dev/null +++ b/src/functions-templates/js/graphql-gateway/graphql-gateway.js @@ -0,0 +1,74 @@ +/** + * This code assumes you have other graphql Netlify functions + * and shows you how to stitch them together in a "gateway". + * + * Of course, feel free to modify this gateway to suit your needs. + */ + +const { + introspectSchema, + makeRemoteExecutableSchema, + mergeSchemas +} = require("graphql-tools"); +const { createHttpLink } = require("apollo-link-http"); +const fetch = require("node-fetch"); +const { ApolloServer, gql } = require("apollo-server-lambda"); + +exports.handler = async function(event, context) { + const schema1 = await getSchema("graphql-1"); // other Netlify functions which are graphql lambdas + const schema2 = await getSchema("graphql-2"); // other Netlify functions which are graphql lambdas + const schemas = [schema1, schema2]; + + /** + * resolving -between- schemas + * https://www.apollographql.com/docs/graphql-tools/schema-stitching#adding-resolvers + */ + const linkTypeDefs = ` + extend type Book { + author: Author + } + `; + schemas.push(linkTypeDefs); + const resolvers = { + Book: { + author: { + fragment: `... on Book { authorName }`, + resolve(book, args, context, info) { + return info.mergeInfo.delegateToSchema({ + schema: schema1, + operation: "query", + fieldName: "authorByName", // reuse what's implemented in schema1 + args: { + name: book.authorName + }, + context, + info + }); + } + } + } + }; + + // more docs https://www.apollographql.com/docs/graphql-tools/schema-stitching#api + const schema = mergeSchemas({ + schemas, + resolvers + }); + const server = new ApolloServer({ schema }); + return new Promise((yay, nay) => { + const cb = (err, args) => (err ? nay(err) : yay(args)); + server.createHandler()(event, context, cb); + }); +}; + +async function getSchema(endpoint) { + // you can't use relative URLs within Netlify Functions so need a base URL + // process.env.URL is one of many build env variables: + // https://www.netlify.com/docs/continuous-deployment/#build-environment-variables + // Netlify Dev only supports URL and DEPLOY URL for now + const uri = process.env.URL + "/.netlify/functions/" + endpoint; + const link = createHttpLink({ uri, fetch }); + const schema = await introspectSchema(link); + const executableSchema = makeRemoteExecutableSchema({ schema, link }); + return executableSchema; +} diff --git a/src/functions-templates/js/graphql-gateway/package.json b/src/functions-templates/js/graphql-gateway/package.json new file mode 100644 index 00000000000..92394f648fd --- /dev/null +++ b/src/functions-templates/js/graphql-gateway/package.json @@ -0,0 +1,24 @@ +{ + "name": "graphql-gateway", + "version": "1.0.0", + "description": "netlify functions:create - Apollo Server Lambda Gateway stitching schemas from other GraphQL Functions!", + "main": "graphql-gateway.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js", + "apollo" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "apollo-link-http": "^1.5.14", + "apollo-server-lambda": "^2.4.8", + "graphql": "^14.2.1", + "graphql-tools": "^4.0.4", + "node-fetch": "^2.3.0" + } +} diff --git a/src/functions-templates/js/hasura-event-triggered/.netlify-function-template.js b/src/functions-templates/js/hasura-event-triggered/.netlify-function-template.js new file mode 100644 index 00000000000..3701d9703d1 --- /dev/null +++ b/src/functions-templates/js/hasura-event-triggered/.netlify-function-template.js @@ -0,0 +1,5 @@ +module.exports = { + name: "hasura-event-triggered", + description: + "Hasura Cleaning: process a Hasura event and fire off a GraphQL mutation with processed text data" +}; diff --git a/src/functions-templates/js/hasura-event-triggered/hasura-event-triggered.js b/src/functions-templates/js/hasura-event-triggered/hasura-event-triggered.js new file mode 100644 index 00000000000..2b98c2222ee --- /dev/null +++ b/src/functions-templates/js/hasura-event-triggered/hasura-event-triggered.js @@ -0,0 +1,37 @@ +// with thanks to https://github.com/vnovick/netlify-function-example/blob/master/functions/bad-words.js +const axios = require("axios"); +const Filter = require("bad-words"); +const filter = new Filter(); +const hgeEndpoint = "https://live-coding-netlify.herokuapp.com"; + +const query = ` +mutation verifiedp($id: uuid!, $title: String!, $content: String!) { + update_posts(_set: { verified: true, content: $content, title: $title }, + where:{ id: { _eq: $id } }) { + returning { + id + } + } +} +`; + +exports.handler = async (event, context) => { + let request; + try { + request = JSON.parse(event.body); + } catch (e) { + return { statusCode: 400, body: "c annot parse hasura event" }; + } + + const variables = { + id: request.event.data.new.id, + title: filter.clean(request.event.data.new.title), + content: filter.clean(request.event.data.new.content) + }; + try { + await axios.post(hgeEndpoint + "/v1alpha1/graphql", { query, variables }); + return { statusCode: 200, body: "success" }; + } catch (err) { + return { statusCode: 500, body: err.toString() }; + } +}; diff --git a/src/functions-templates/js/hasura-event-triggered/package.json b/src/functions-templates/js/hasura-event-triggered/package.json new file mode 100644 index 00000000000..0ceb1e66ad8 --- /dev/null +++ b/src/functions-templates/js/hasura-event-triggered/package.json @@ -0,0 +1,21 @@ +{ + "name": "hasura-event-triggered", + "version": "1.0.0", + "description": "netlify functions:create - Serverless function to process a Hasura event and fire off a GraphQL mutation with cleaned text data", + "main": "hasura-event-triggered.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js", + "hasura" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "axios": "^0.18.0", + "bad-words": "^3.0.2" + } +} diff --git a/src/functions-templates/js/hello-world/.netlify-function-template.js b/src/functions-templates/js/hello-world/.netlify-function-template.js new file mode 100644 index 00000000000..fcb9a19f010 --- /dev/null +++ b/src/functions-templates/js/hello-world/.netlify-function-template.js @@ -0,0 +1,6 @@ +module.exports = { + name: "hello-world", + priority: 1, + description: + "Basic function that shows async/await usage, and response formatting" +}; diff --git a/src/functions-templates/js/hello-world/hello-world.js b/src/functions-templates/js/hello-world/hello-world.js new file mode 100644 index 00000000000..98ca5fae0ca --- /dev/null +++ b/src/functions-templates/js/hello-world/hello-world.js @@ -0,0 +1,15 @@ +// Docs on event and context https://www.netlify.com/docs/functions/#the-handler-method +exports.handler = async (event, context) => { + try { + const subject = event.queryStringParameters.name || "World"; + return { + statusCode: 200, + body: JSON.stringify({ message: `Hello ${subject}` }) + // // more keys you can return: + // headers: { "headerName": "headerValue", ... }, + // isBase64Encoded: true, + }; + } catch (err) { + return { statusCode: 500, body: err.toString() }; + } +}; diff --git a/src/functions-templates/js/identity-signup/.netlify-function-template.js b/src/functions-templates/js/identity-signup/.netlify-function-template.js new file mode 100644 index 00000000000..c0ce20dc37a --- /dev/null +++ b/src/functions-templates/js/identity-signup/.netlify-function-template.js @@ -0,0 +1,5 @@ +module.exports = { + name: "identity-signup", + description: + "Identity Signup: Triggered when a new Netlify Identity user confirms. Assigns roles and extra metadata" +}; diff --git a/src/functions-templates/js/identity-signup/identity-signup.js b/src/functions-templates/js/identity-signup/identity-signup.js new file mode 100644 index 00000000000..a8f0f2ff033 --- /dev/null +++ b/src/functions-templates/js/identity-signup/identity-signup.js @@ -0,0 +1,29 @@ +// note - this function MUST be named `identity-signup` to work +// we do not yet offer local emulation of this functionality in Netlify Dev +// +// more: +// https://www.netlify.com/blog/2019/02/21/the-role-of-roles-and-how-to-set-them-in-netlify-identity/ +// https://www.netlify.com/docs/functions/#identity-and-functions + +exports.handler = async function(event, context) { + const data = JSON.parse(event.body); + const { user } = data; + + const responseBody = { + app_metadata: { + roles: + user.email.split("@")[1] === "trust-this-company.com" + ? ["editor"] + : ["visitor"], + my_user_info: "this is some user info" + }, + user_metadata: { + ...user.user_metadata, // append current user metadata + custom_data_from_function: "hurray this is some extra metadata" + } + }; + return { + statusCode: 200, + body: JSON.stringify(responseBody) + }; +}; diff --git a/src/functions-templates/js/node-fetch/.netlify-function-template.js b/src/functions-templates/js/node-fetch/.netlify-function-template.js new file mode 100644 index 00000000000..a363b9aba5a --- /dev/null +++ b/src/functions-templates/js/node-fetch/.netlify-function-template.js @@ -0,0 +1,5 @@ +module.exports = { + name: "node-fetch", + description: + "Fetch function: uses node-fetch to hit an external API without CORS issues" +}; diff --git a/src/functions-templates/js/node-fetch/node-fetch.js b/src/functions-templates/js/node-fetch/node-fetch.js new file mode 100644 index 00000000000..4693b007da3 --- /dev/null +++ b/src/functions-templates/js/node-fetch/node-fetch.js @@ -0,0 +1,25 @@ +/* eslint-disable */ +const fetch = require("node-fetch"); +exports.handler = async function(event, context) { + try { + const response = await fetch("https://icanhazdadjoke.com", { + headers: { Accept: "application/json" } + }); + if (!response.ok) { + // NOT res.status >= 200 && res.status < 300 + return { statusCode: response.status, body: response.statusText }; + } + const data = await response.json(); + + return { + statusCode: 200, + body: JSON.stringify({ msg: data.joke }) + }; + } catch (err) { + console.log(err); // output to netlify function log + return { + statusCode: 500, + body: JSON.stringify({ msg: err.message }) // Could be a custom message or object i.e. JSON.stringify(err) + }; + } +}; diff --git a/src/functions-templates/js/node-fetch/package.json b/src/functions-templates/js/node-fetch/package.json new file mode 100644 index 00000000000..3bf7b10f120 --- /dev/null +++ b/src/functions-templates/js/node-fetch/package.json @@ -0,0 +1,19 @@ +{ + "name": "node-fetch", + "version": "1.0.0", + "description": "netlify functions:create - default template for node fetch function", + "main": "node-fetch.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.3.0" + } +} diff --git a/src/functions-templates/js/oauth-passport/.netlify-function-template.js b/src/functions-templates/js/oauth-passport/.netlify-function-template.js new file mode 100644 index 00000000000..58a06653c50 --- /dev/null +++ b/src/functions-templates/js/oauth-passport/.netlify-function-template.js @@ -0,0 +1,5 @@ +module.exports = { + name: "oauth-passport", + description: + "oauth-passport: template for Oauth workflow using Passport + Express.js" +}; diff --git a/src/functions-templates/js/oauth-passport/oauth-passport.js b/src/functions-templates/js/oauth-passport/oauth-passport.js new file mode 100644 index 00000000000..b7bc7d547cf --- /dev/null +++ b/src/functions-templates/js/oauth-passport/oauth-passport.js @@ -0,0 +1,42 @@ +// details: https://markus.oberlehner.net/blog/implementing-an-authentication-flow-with-passport-and-netlify-functions/ + +const bodyParser = require("body-parser"); +const cookieParser = require("cookie-parser"); +const express = require("express"); +const passport = require("passport"); +const serverless = require("serverless-http"); + +require("./utils/auth"); + +const { COOKIE_SECURE, ENDPOINT } = require("./utils/config"); + +const app = express(); + +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json()); +app.use(cookieParser()); +app.use(passport.initialize()); + +const handleCallback = () => (req, res) => { + res + .cookie("jwt", req.user.jwt, { httpOnly: true, COOKIE_SECURE }) + .redirect("/"); +}; + +app.get( + `${ENDPOINT}/auth/github`, + passport.authenticate("github", { session: false }) +); +app.get( + `${ENDPOINT}/auth/github/callback`, + passport.authenticate("github", { failureRedirect: "/", session: false }), + handleCallback() +); + +app.get( + `${ENDPOINT}/auth/status`, + passport.authenticate("jwt", { session: false }), + (req, res) => res.json({ email: req.user.email }) +); + +module.exports.handler = serverless(app); diff --git a/src/functions-templates/js/oauth-passport/package.json b/src/functions-templates/js/oauth-passport/package.json new file mode 100644 index 00000000000..7d3082c3a40 --- /dev/null +++ b/src/functions-templates/js/oauth-passport/package.json @@ -0,0 +1,25 @@ +{ + "name": "oauth-passport", + "version": "1.0.0", + "description": "netlify functions:create - template for Oauth workflow using Passport + Express.js", + "main": "oauth-passport.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "cookie-parser": "^1.4.4", + "express": "^4.17.1", + "jsonwebtoken": "^8.5.1", + "passport": "^0.4.0", + "passport-github2": "^0.1.11", + "passport-jwt": "^4.0.0", + "serverless-http": "^2.0.2" + } +} diff --git a/src/functions-templates/js/oauth-passport/utils/auth.js b/src/functions-templates/js/oauth-passport/utils/auth.js new file mode 100644 index 00000000000..cb7d0ccaab7 --- /dev/null +++ b/src/functions-templates/js/oauth-passport/utils/auth.js @@ -0,0 +1,56 @@ +const { sign } = require('jsonwebtoken') +const { Strategy: GitHubStrategy } = require('passport-github2') +const passport = require('passport') +const passportJwt = require('passport-jwt') + +const { BASE_URL, ENDPOINT, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, SECRET } = require('./config') + +function authJwt(email) { + return sign({ user: { email } }, SECRET) +} + +passport.use( + new GitHubStrategy( + { + clientID: GITHUB_CLIENT_ID, + clientSecret: GITHUB_CLIENT_SECRET, + callbackURL: `${BASE_URL}${ENDPOINT}/auth/github/callback`, + scope: ['user:email'], + }, + async (accessToken, refreshToken, profile, done) => { + try { + const email = profile.emails[0].value + // Here you'd typically create a new or load an existing user and + // store the bare necessary informations about the user in the JWT. + const jwt = authJwt(email) + + return done(null, { email, jwt }) + } catch (error) { + return done(error) + } + }, + ), +) + +passport.use( + new passportJwt.Strategy( + { + jwtFromRequest(req) { + if (!req.cookies) throw new Error('Missing cookie-parser middleware') + return req.cookies.jwt + }, + secretOrKey: SECRET, + }, + async ({ user: { email } }, done) => { + try { + // Here you'd typically load an existing user + // and use the data to create the JWT. + const jwt = authJwt(email) + + return done(null, { email, jwt }) + } catch (error) { + return done(error) + } + }, + ), +) diff --git a/src/functions-templates/js/oauth-passport/utils/config.js b/src/functions-templates/js/oauth-passport/utils/config.js new file mode 100644 index 00000000000..81c401ad7d8 --- /dev/null +++ b/src/functions-templates/js/oauth-passport/utils/config.js @@ -0,0 +1,13 @@ +// lambda/utils/config.js +// Circumvent problem with Netlify CLI. +// https://github.com/netlify/netlify-dev-plugin/issues/147 +exports.BASE_URL = process.env.NODE_ENV === 'development' ? 'http://localhost:8888' : process.env.BASE_URL + +exports.COOKIE_SECURE = process.env.NODE_ENV !== 'development' + +exports.ENDPOINT = process.env.NODE_ENV === 'development' ? '/.netlify/functions' : '/api' + +exports.GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID +exports.GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET + +exports.SECRET = process.env.SECRET || 'SUPERSECRET' diff --git a/src/functions-templates/js/protected-function/.netlify-function-template.js b/src/functions-templates/js/protected-function/.netlify-function-template.js new file mode 100644 index 00000000000..b5ca416ceaa --- /dev/null +++ b/src/functions-templates/js/protected-function/.netlify-function-template.js @@ -0,0 +1,4 @@ +module.exports = { + name: "protected-function", + description: "Function protected Netlify Identity authentication" +}; diff --git a/src/functions-templates/js/protected-function/protected-function.js b/src/functions-templates/js/protected-function/protected-function.js new file mode 100644 index 00000000000..7c96c71b7f3 --- /dev/null +++ b/src/functions-templates/js/protected-function/protected-function.js @@ -0,0 +1,23 @@ +exports.handler = async (event, context) => { + console.log("protected function!"); + // Reading the context.clientContext will give us the current user + const claims = context.clientContext && context.clientContext.user; + console.log("user claims", claims); + + if (!claims) { + console.log("No claims! Begone!"); + return { + statusCode: 401, + body: JSON.stringify({ + data: "NOT ALLOWED" + }) + }; + } + + return { + statusCode: 200, + body: JSON.stringify({ + data: "auth true" + }) + }; +}; diff --git a/src/functions-templates/js/send-email/.netlify-function-template.js b/src/functions-templates/js/send-email/.netlify-function-template.js new file mode 100644 index 00000000000..0b5e9247b94 --- /dev/null +++ b/src/functions-templates/js/send-email/.netlify-function-template.js @@ -0,0 +1,4 @@ +module.exports = { + name: "send-email", + description: "Send Email: Send email with no SMTP server via 'sendmail' pkg" +}; diff --git a/src/functions-templates/js/send-email/package.json b/src/functions-templates/js/send-email/package.json new file mode 100644 index 00000000000..a65e13f3ddc --- /dev/null +++ b/src/functions-templates/js/send-email/package.json @@ -0,0 +1,21 @@ +{ + "name": "send-email", + "version": "1.0.0", + "description": "netlify functions:create - Send email with no SMTP server via 'sendmail' pkg", + "main": "send-email.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "apis", + "email", + "js" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "sendmail": "1.4.1" + } +} diff --git a/src/functions-templates/js/send-email/send-email.js b/src/functions-templates/js/send-email/send-email.js new file mode 100644 index 00000000000..80a4d19901f --- /dev/null +++ b/src/functions-templates/js/send-email/send-email.js @@ -0,0 +1,62 @@ +// with thanks to https://github.com/Urigo/graphql-modules/blob/8cb2fd7d9938a856f83e4eee2081384533771904/website/lambda/contact.js +const sendMail = require("sendmail")(); +const { validateEmail, validateLength } = require("./validations"); + +exports.handler = (event, context, callback) => { + if (!process.env.CONTACT_EMAIL) { + return callback(null, { + statusCode: 500, + body: "process.env.CONTACT_EMAIL must be defined" + }); + } + + const body = JSON.parse(event.body); + + try { + validateLength("body.name", body.name, 3, 50); + } catch (e) { + return callback(null, { + statusCode: 403, + body: e.message + }); + } + + try { + validateEmail("body.email", body.email); + } catch (e) { + return callback(null, { + statusCode: 403, + body: e.message + }); + } + + try { + validateLength("body.details", body.details, 10, 1000); + } catch (e) { + return callback(null, { + statusCode: 403, + body: e.message + }); + } + + const descriptor = { + from: `"${body.email}" `, + to: process.env.CONTACT_EMAIL, + subject: `${body.name} sent you a message from gql-modules.com`, + text: body.details + }; + + sendMail(descriptor, e => { + if (e) { + callback(null, { + statusCode: 500, + body: e.message + }); + } else { + callback(null, { + statusCode: 200, + body: "" + }); + } + }); +}; diff --git a/src/functions-templates/js/send-email/validations.js b/src/functions-templates/js/send-email/validations.js new file mode 100644 index 00000000000..6c52428c777 --- /dev/null +++ b/src/functions-templates/js/send-email/validations.js @@ -0,0 +1,35 @@ +exports.validateEmail = (ctx, str) => { + if (typeof str !== "string" && !(str instanceof String)) { + throw TypeError(`${ctx} must be a string`); + } + + exports.validateLength(ctx, str, 5, 30); + + if (!/^[\w.-]+@[\w.-]+\.\w+$/.test(str)) { + throw TypeError(`${ctx} is not an email address`); + } +}; + +exports.validateLength = (ctx, str, ...args) => { + let min, max; + + if (args.length === 1) { + min = 0; + max = args[0]; + } else { + min = args[0]; + max = args[1]; + } + + if (typeof str !== "string" && !(str instanceof String)) { + throw TypeError(`${ctx} must be a string`); + } + + if (str.length < min) { + throw TypeError(`${ctx} must be at least ${min} chars long`); + } + + if (str.length > max) { + throw TypeError(`${ctx} must contain ${max} chars at most`); + } +}; diff --git a/src/functions-templates/js/serverless-ssr/.netlify-function-template.js b/src/functions-templates/js/serverless-ssr/.netlify-function-template.js new file mode 100644 index 00000000000..e7092cf186b --- /dev/null +++ b/src/functions-templates/js/serverless-ssr/.netlify-function-template.js @@ -0,0 +1,4 @@ +module.exports = { + name: "serverless-ssr", + description: "Dynamic serverside rendering via functions" +}; diff --git a/src/functions-templates/js/serverless-ssr/app/index.js b/src/functions-templates/js/serverless-ssr/app/index.js new file mode 100644 index 00000000000..952511cb62f --- /dev/null +++ b/src/functions-templates/js/serverless-ssr/app/index.js @@ -0,0 +1,118 @@ +/* Express App */ +const express = require("express"); +const cors = require("cors"); +const morgan = require("morgan"); +const bodyParser = require("body-parser"); +const compression = require("compression"); + +/* My express App */ +module.exports = function expressApp(functionName) { + const app = express(); + const router = express.Router(); + + // gzip responses + router.use(compression()); + + // Set router base path for local dev + const routerBasePath = + process.env.NODE_ENV === "dev" + ? `/${functionName}` + : `/.netlify/functions/${functionName}/`; + + /* define routes */ + router.get("/", (req, res) => { + const html = ` + + + + + +

Express via '${functionName}' ⊂◉‿◉つ

+ +

I'm using Express running via a Netlify Function.

+ +

Choose a route:

+ +
+ View /users route +
+ +
+ View /hello route +
+ +
+
+ +
+ + Go back to demo homepage + +
+ +
+
+ +
+ + See the source code on github + +
+ + + `; + res.send(html); + }); + + router.get("/users", (req, res) => { + res.json({ + users: [ + { + name: "steve" + }, + { + name: "joe" + } + ] + }); + }); + + router.get("/hello/", function(req, res) { + res.send("hello world"); + }); + + // Attach logger + app.use(morgan(customLogger)); + + // Setup routes + app.use(routerBasePath, router); + + // Apply express middlewares + router.use(cors()); + router.use(bodyParser.json()); + router.use(bodyParser.urlencoded({ extended: true })); + + return app; +}; + +function customLogger(tokens, req, res) { + const log = [ + tokens.method(req, res), + tokens.url(req, res), + tokens.status(req, res), + tokens.res(req, res, "content-length"), + "-", + tokens["response-time"](req, res), + "ms" + ].join(" "); + + if (process.env.NODE_ENV !== "dev") { + // Log only in AWS context to get back function logs + console.log(log); + } + return log; +} diff --git a/src/functions-templates/js/serverless-ssr/package.json b/src/functions-templates/js/serverless-ssr/package.json new file mode 100644 index 00000000000..743b04c340d --- /dev/null +++ b/src/functions-templates/js/serverless-ssr/package.json @@ -0,0 +1,25 @@ +{ + "name": "serverless-ssr", + "version": "1.0.0", + "description": "netlify functions:create - default template for a serverless SSR function", + "main": "serverless-ssr.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "body-parser": "^1.18.3", + "compression": "^1.7.4", + "cors": "^2.8.5", + "express": "^4.16.4", + "morgan": "^1.9.1", + "node-fetch": "^2.3.0", + "serverless-http": "^1.9.1" + } +} diff --git a/src/functions-templates/js/serverless-ssr/serverless-http.js b/src/functions-templates/js/serverless-ssr/serverless-http.js new file mode 100644 index 00000000000..cb4800d246e --- /dev/null +++ b/src/functions-templates/js/serverless-ssr/serverless-http.js @@ -0,0 +1,12 @@ +// for a full working demo check https://express-via-functions.netlify.com/.netlify/functions/serverless-http +const serverless = require("serverless-http"); +const expressApp = require("./app"); + +// We need to define our function name for express routes to set the correct base path +const functionName = "serverless-http"; + +// Initialize express app +const app = expressApp(functionName); + +// Export lambda handler +exports.handler = serverless(app); diff --git a/src/functions-templates/js/serverless-ssr/serverless-ssr.js b/src/functions-templates/js/serverless-ssr/serverless-ssr.js new file mode 100644 index 00000000000..cb4800d246e --- /dev/null +++ b/src/functions-templates/js/serverless-ssr/serverless-ssr.js @@ -0,0 +1,12 @@ +// for a full working demo check https://express-via-functions.netlify.com/.netlify/functions/serverless-http +const serverless = require("serverless-http"); +const expressApp = require("./app"); + +// We need to define our function name for express routes to set the correct base path +const functionName = "serverless-http"; + +// Initialize express app +const app = expressApp(functionName); + +// Export lambda handler +exports.handler = serverless(app); diff --git a/src/functions-templates/js/set-cookie/.netlify-function-template.js b/src/functions-templates/js/set-cookie/.netlify-function-template.js new file mode 100644 index 00000000000..c0a9498127a --- /dev/null +++ b/src/functions-templates/js/set-cookie/.netlify-function-template.js @@ -0,0 +1,4 @@ +module.exports = { + name: "set-cookie", + description: "Set a cookie alongside your function" +}; diff --git a/src/functions-templates/js/set-cookie/package.json b/src/functions-templates/js/set-cookie/package.json new file mode 100644 index 00000000000..dccf0e31f8c --- /dev/null +++ b/src/functions-templates/js/set-cookie/package.json @@ -0,0 +1,19 @@ +{ + "name": "set-cookie", + "version": "1.0.0", + "description": "netlify functions:create - set a cookie with your Netlify Function", + "main": "set-cookie", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "cookie": "^0.3.1" + } +} diff --git a/src/functions-templates/js/set-cookie/set-cookie.js b/src/functions-templates/js/set-cookie/set-cookie.js new file mode 100644 index 00000000000..77b6022352b --- /dev/null +++ b/src/functions-templates/js/set-cookie/set-cookie.js @@ -0,0 +1,41 @@ +const cookie = require("cookie"); + +exports.handler = async (event, context) => { + var hour = 3600000; + var twoWeeks = 14 * 24 * hour; + const myCookie = cookie.serialize("my_cookie", "lolHi", { + secure: true, + httpOnly: true, + path: "/", + maxAge: twoWeeks + }); + + const redirectUrl = "https://google.com"; + // Do redirects via html + const html = ` + + + + + + + + + `; + + return { + statusCode: 200, + headers: { + "Set-Cookie": myCookie, + "Cache-Control": "no-cache", + "Content-Type": "text/html" + }, + body: html + }; +}; diff --git a/src/functions-templates/js/slack-rate-limit/.netlify-function-template.js b/src/functions-templates/js/slack-rate-limit/.netlify-function-template.js new file mode 100644 index 00000000000..698bfd1088c --- /dev/null +++ b/src/functions-templates/js/slack-rate-limit/.netlify-function-template.js @@ -0,0 +1,5 @@ +module.exports = { + name: "slack-rate-limit", + description: + "Slack Rate-limit: post to Slack, at most once an hour, using Neltify Identity metadata" +}; diff --git a/src/functions-templates/js/slack-rate-limit/package.json b/src/functions-templates/js/slack-rate-limit/package.json new file mode 100644 index 00000000000..edd17b8fc29 --- /dev/null +++ b/src/functions-templates/js/slack-rate-limit/package.json @@ -0,0 +1,20 @@ +{ + "name": "slack-rate-limit", + "version": "1.0.0", + "description": "netlify functions:create - post to Slack, at most once an hour, using Neltify Identity metadata", + "main": "node-fetch.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "slack", + "js" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.3.0" + } +} diff --git a/src/functions-templates/js/slack-rate-limit/slack-rate-limit.js b/src/functions-templates/js/slack-rate-limit/slack-rate-limit.js new file mode 100644 index 00000000000..1484c1c3852 --- /dev/null +++ b/src/functions-templates/js/slack-rate-limit/slack-rate-limit.js @@ -0,0 +1,129 @@ +// code walkthrough: https://www.netlify.com/blog/2018/03/29/jamstack-architecture-on-netlify-how-identity-and-functions-work-together/#updating-user-data-with-the-identity-api +// demo repo: https://github.com/biilmann/testing-slack-tutorial/tree/v3-one-message-an-hour +// note: requires SLACK_WEBHOOK_URL environment variable +const slackURL = process.env.SLACK_WEBHOOK_URL; +const fetch = require("node-fetch"); + +class IdentityAPI { + constructor(apiURL, token) { + this.apiURL = apiURL; + this.token = token; + } + + headers(headers = {}) { + return { + "Content-Type": "application/json", + Authorization: `Bearer ${this.token}`, + ...headers + }; + } + + parseJsonResponse(response) { + return response.json().then(json => { + if (!response.ok) { + return Promise.reject({ status: response.status, json }); + } + + return json; + }); + } + + request(path, options = {}) { + const headers = this.headers(options.headers || {}); + return fetch(this.apiURL + path, { ...options, headers }).then(response => { + const contentType = response.headers.get("Content-Type"); + if (contentType && contentType.match(/json/)) { + return this.parseJsonResponse(response); + } + + if (!response.ok) { + return response.text().then(data => { + return Promise.reject({ stauts: response.status, data }); + }); + } + return response.text().then(data => { + data; + }); + }); + } +} + +/* + Fetch a user from GoTrue via id +*/ +function fetchUser(identity, id) { + const api = new IdentityAPI(identity.url, identity.token); + return api.request(`/admin/users/${id}`); +} + +/* + Update the app_metadata of a user +*/ +function updateUser(identity, user, app_metadata) { + const api = new IdentityAPI(identity.url, identity.token); + const new_app_metadata = { ...user.app_metadata, ...app_metadata }; + + return api.request(`/admin/users/${user.id}`, { + method: "PUT", + body: JSON.stringify({ app_metadata: new_app_metadata }) + }); +} + +const oneHour = 60 * 60 * 1000; +export function handler(event, context, callback) { + if (event.httpMethod !== "POST") { + return callback(null, { + statusCode: 410, + body: "Unsupported Request Method" + }); + } + + const claims = context.clientContext && context.clientContext.user; + if (!claims) { + return callback(null, { + statusCode: 401, + body: "You must be signed in to call this function" + }); + } + + fetchUser(context.clientContext.identity, claims.sub).then(user => { + const lastMessage = new Date( + user.app_metadata.last_message_at || 0 + ).getTime(); + const cutOff = new Date().getTime() - oneHour; + if (lastMessage > cutOff) { + return callback(null, { + statusCode: 401, + body: "Only one message an hour allowed" + }); + } + + try { + const payload = JSON.parse(event.body); + + fetch(slackURL, { + method: "POST", + body: JSON.stringify({ + text: payload.text, + attachments: [{ text: `From ${user.email}` }] + }) + }) + .then(() => + updateUser(context.clientContext.identity, user, { + last_message_at: new Date().getTime() + }) + ) + .then(() => { + callback(null, { statusCode: 204 }); + }) + .catch(err => { + callback(null, { + statusCode: 500, + body: "Internal Server Error: " + e + }); + }); + } catch (e) { + callback(null, { statusCode: 500, body: "Internal Server Error: " + e }); + } + }); +} diff --git a/src/functions-templates/js/stripe-charge/.netlify-function-template.js b/src/functions-templates/js/stripe-charge/.netlify-function-template.js new file mode 100644 index 00000000000..3cf418f9457 --- /dev/null +++ b/src/functions-templates/js/stripe-charge/.netlify-function-template.js @@ -0,0 +1,31 @@ +const chalk = require("chalk"); + +module.exports = { + name: "stripe-charge", + description: "Stripe Charge: Charge a user with Stripe", + async onComplete() { + console.log( + `${chalk.yellow("stripe-charge")} function created from template!` + ); + if (!process.env.STRIPE_SECRET_KEY) { + console.log( + `note this function requires ${chalk.yellow( + "STRIPE_SECRET_KEY" + )} build environment variable set in your Netlify Site.` + ); + let siteData = { name: "YOURSITENAMEHERE" }; + try { + siteData = await this.netlify.api.getSite({ + siteId: this.netlify.site.id + }); + } catch (e) { + // silent error, not important + } + console.log( + `Set it at: https://app.netlify.com/sites/${ + siteData.name + }/settings/deploys#environment-variables (must have CD setup)` + ); + } + } +}; diff --git a/src/functions-templates/js/stripe-charge/package-lock.json b/src/functions-templates/js/stripe-charge/package-lock.json new file mode 100644 index 00000000000..024f7967a3a --- /dev/null +++ b/src/functions-templates/js/stripe-charge/package-lock.json @@ -0,0 +1,39 @@ +{ + "name": "stripe-charge", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "stripe": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-6.28.0.tgz", + "integrity": "sha512-4taF37geIr9DqvWEm3G9VCz2iJSV/DFc3PcElCQdQK5GUMI/MOj6XE0oJRYMOAHz0Oq8pT+4yDQmkh3SDI3nQA==", + "requires": { + "lodash.isplainobject": "^4.0.6", + "qs": "^6.6.0", + "safe-buffer": "^5.1.1", + "uuid": "^3.3.2" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } +} diff --git a/src/functions-templates/js/stripe-charge/package.json b/src/functions-templates/js/stripe-charge/package.json new file mode 100644 index 00000000000..6025273c245 --- /dev/null +++ b/src/functions-templates/js/stripe-charge/package.json @@ -0,0 +1,21 @@ +{ + "name": "stripe-charge", + "version": "1.0.0", + "description": "netlify functions:create - Charge a user with Stripe", + "main": "stripe-charge.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "apis", + "stripe", + "js" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "stripe": "^6.28.0" + } +} diff --git a/src/functions-templates/js/stripe-charge/stripe-charge.js b/src/functions-templates/js/stripe-charge/stripe-charge.js new file mode 100644 index 00000000000..64fa6258dbd --- /dev/null +++ b/src/functions-templates/js/stripe-charge/stripe-charge.js @@ -0,0 +1,65 @@ +// with thanks https://github.com/alexmacarthur/netlify-lambda-function-example/blob/68a0cdc05e201d68fe80b0926b0af7ff88f15802/lambda-src/purchase.js + +const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); + +const statusCode = 200; +const headers = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type" +}; + +exports.handler = function(event, context, callback) { + //-- We only care to do anything if this is our POST request. + if (event.httpMethod !== "POST" || !event.body) { + callback(null, { + statusCode, + headers, + body: "" + }); + } + + //-- Parse the body contents into an object. + const data = JSON.parse(event.body); + + //-- Make sure we have all required data. Otherwise, escape. + if (!data.token || !data.amount || !data.idempotency_key) { + console.error("Required information is missing."); + + callback(null, { + statusCode, + headers, + body: JSON.stringify({ status: "missing-information" }) + }); + + return; + } + + stripe.charges.create( + { + currency: "usd", + amount: data.amount, + source: data.token.id, + receipt_email: data.token.email, + description: `charge for a widget` + }, + { + idempotency_key: data.idempotency_key + }, + (err, charge) => { + if (err !== null) { + console.log(err); + } + + let status = + charge === null || charge.status !== "succeeded" + ? "failed" + : charge.status; + + callback(null, { + statusCode, + headers, + body: JSON.stringify({ status }) + }); + } + ); +}; diff --git a/src/functions-templates/js/stripe-subscription/.netlify-function-template.js b/src/functions-templates/js/stripe-subscription/.netlify-function-template.js new file mode 100644 index 00000000000..9c8d13233b3 --- /dev/null +++ b/src/functions-templates/js/stripe-subscription/.netlify-function-template.js @@ -0,0 +1,31 @@ +const chalk = require("chalk"); + +module.exports = { + name: "stripe-subscription", + description: "Stripe subscription: Create a subscription with Stripe", + async onComplete() { + console.log( + `${chalk.yellow("stripe-subscription")} function created from template!` + ); + if (!process.env.STRIPE_SECRET_KEY) { + console.log( + `note this function requires ${chalk.yellow( + "STRIPE_SECRET_KEY" + )} build environment variable set in your Netlify Site.` + ); + let siteData = { name: "YOURSITENAMEHERE" }; + try { + siteData = await this.netlify.api.getSite({ + siteId: this.netlify.site.id + }); + } catch (e) { + // silent error, not important + } + console.log( + `Set it at: https://app.netlify.com/sites/${ + siteData.name + }/settings/deploys#environment-variables (must have CD setup)` + ); + } + } +}; diff --git a/src/functions-templates/js/stripe-subscription/package-lock.json b/src/functions-templates/js/stripe-subscription/package-lock.json new file mode 100644 index 00000000000..024f7967a3a --- /dev/null +++ b/src/functions-templates/js/stripe-subscription/package-lock.json @@ -0,0 +1,39 @@ +{ + "name": "stripe-charge", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "stripe": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-6.28.0.tgz", + "integrity": "sha512-4taF37geIr9DqvWEm3G9VCz2iJSV/DFc3PcElCQdQK5GUMI/MOj6XE0oJRYMOAHz0Oq8pT+4yDQmkh3SDI3nQA==", + "requires": { + "lodash.isplainobject": "^4.0.6", + "qs": "^6.6.0", + "safe-buffer": "^5.1.1", + "uuid": "^3.3.2" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } +} diff --git a/src/functions-templates/js/stripe-subscription/package.json b/src/functions-templates/js/stripe-subscription/package.json new file mode 100644 index 00000000000..ed77893b6e4 --- /dev/null +++ b/src/functions-templates/js/stripe-subscription/package.json @@ -0,0 +1,21 @@ +{ + "name": "stripe-subscription", + "version": "1.0.0", + "description": "netlify functions:create - Create a subscription with Stripe", + "main": "stripe-subscription.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "apis", + "stripe", + "js" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "stripe": "^6.28.0" + } +} diff --git a/src/functions-templates/js/stripe-subscription/stripe-subscription.js b/src/functions-templates/js/stripe-subscription/stripe-subscription.js new file mode 100644 index 00000000000..020842f71b9 --- /dev/null +++ b/src/functions-templates/js/stripe-subscription/stripe-subscription.js @@ -0,0 +1,58 @@ +// with thanks https://github.com/LukeMwila/stripe-subscriptions-backend/blob/master/stripe-api/index.ts + +const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); + +const respond = (fulfillmentText: any): any => { + return { + statusCode: 200, + body: JSON.stringify(fulfillmentText), + headers: { + "Access-Control-Allow-Credentials": true, + "Access-Control-Allow-Origin": "*", + "Content-Type": "application/json" + } + }; +}; + +exports.handler = async function(event, context) { + try { + const incoming = JSON.parse(event.body); + const { stripeToken, email, productPlan } = incoming; + } catch (err) { + console.error(`error with parsing function parameters: `, err); + return { + statusCode: 400, + body: JSON.stringify(err) + }; + } + try { + const data = await createCustomerAndSubscribeToPlan( + stripeToken, + email, + productPlan + ); + return respond(data); + } catch (err) { + return respond(err); + } +}; + +async function createCustomerAndSubscribeToPlan( + stripeToken: string, + email: string, + productPlan: string +) { + // create a customer + const customer = await stripe.customers.create({ + email: email, + source: stripeToken + }); + // retrieve created customer id to add customer to subscription plan + const customerId = customer.id; + // create a subscription for the newly created customer + const subscription = await stripe.subscriptions.create({ + customer: customerId, + items: [{ plan: productPlan }] + }); + return subscription; +} diff --git a/src/functions-templates/js/submission-created/.netlify-function-template.js b/src/functions-templates/js/submission-created/.netlify-function-template.js new file mode 100644 index 00000000000..275090099f5 --- /dev/null +++ b/src/functions-templates/js/submission-created/.netlify-function-template.js @@ -0,0 +1,4 @@ +module.exports = { + name: 'submission-created', + description: 'submission-created: template for event triggered function when a new Netlify Form is submitted', +} diff --git a/src/functions-templates/js/submission-created/package.json b/src/functions-templates/js/submission-created/package.json new file mode 100644 index 00000000000..0b56da84ae4 --- /dev/null +++ b/src/functions-templates/js/submission-created/package.json @@ -0,0 +1,19 @@ +{ + "name": "submission-created", + "version": "1.0.0", + "description": "netlify functions:create - template for submission-created event triggered function", + "main": "submission-created.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.3.0" + } +} diff --git a/src/functions-templates/js/submission-created/submission-created.js b/src/functions-templates/js/submission-created/submission-created.js new file mode 100644 index 00000000000..04f1366be8c --- /dev/null +++ b/src/functions-templates/js/submission-created/submission-created.js @@ -0,0 +1,25 @@ +/* eslint-disable */ + +// // optionally configure local env vars +// require('dotenv').config() + +// // details in https://css-tricks.com/using-netlify-forms-and-netlify-functions-to-build-an-email-sign-up-widget +const fetch = require('node-fetch') +const { EMAIL_TOKEN } = process.env +exports.handler = async (event) => { + const email = JSON.parse(event.body).payload.email + console.log(`Recieved a submission: ${email}`) + return fetch('https://api.buttondown.email/v1/subscribers', { + method: 'POST', + headers: { + Authorization: `Token ${EMAIL_TOKEN}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email }), + }) + .then((response) => response.json()) + .then((data) => { + console.log(`Submitted to Buttondown:\n ${data}`) + }) + .catch((error) => ({ statusCode: 422, body: String(error) })) +} diff --git a/src/functions-templates/js/token-hider/.netlify-function-template.js b/src/functions-templates/js/token-hider/.netlify-function-template.js new file mode 100644 index 00000000000..13e9fd7860a --- /dev/null +++ b/src/functions-templates/js/token-hider/.netlify-function-template.js @@ -0,0 +1,34 @@ +const chalk = require("chalk"); + +module.exports = { + name: "token-hider", + description: "Token Hider: access APIs without exposing your API keys", + async onComplete() { + console.log( + `${chalk.yellow("token-hider")} function created from template!` + ); + if (!process.env.API_URL || !process.env.API_TOKEN) { + console.log( + `note this function requires ${chalk.yellow( + "API_URL" + )} and ${chalk.yellow( + "API_TOKEN" + )} build environment variables set in your Netlify Site.` + ); + + let siteData = { name: "YOURSITENAMEHERE" }; + try { + siteData = await this.netlify.api.getSite({ + siteId: this.netlify.site.id + }); + } catch (e) { + // silent error, not important + } + console.log( + `Set them at: https://app.netlify.com/sites/${ + siteData.name + }/settings/deploys#environment-variables (must have CD setup)` + ); + } + } +}; diff --git a/src/functions-templates/js/token-hider/package-lock.json b/src/functions-templates/js/token-hider/package-lock.json new file mode 100644 index 00000000000..1a13afe67b8 --- /dev/null +++ b/src/functions-templates/js/token-hider/package-lock.json @@ -0,0 +1,48 @@ +{ + "name": "token-hider", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "axios": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", + "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", + "requires": { + "follow-redirects": "^1.3.0", + "is-buffer": "^1.1.5" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "follow-redirects": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz", + "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==", + "requires": { + "debug": "^3.2.6" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } +} diff --git a/src/functions-templates/js/token-hider/package.json b/src/functions-templates/js/token-hider/package.json new file mode 100644 index 00000000000..88d706c45f1 --- /dev/null +++ b/src/functions-templates/js/token-hider/package.json @@ -0,0 +1,21 @@ +{ + "name": "token-hider", + "version": "1.0.0", + "description": "netlify functions:create - how to hide API tokens from your users", + "main": "token-hider.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "apis", + "js" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "axios": "^0.18.0", + "qs": "^6.7.0" + } +} diff --git a/src/functions-templates/js/token-hider/token-hider.js b/src/functions-templates/js/token-hider/token-hider.js new file mode 100644 index 00000000000..37ccee34eae --- /dev/null +++ b/src/functions-templates/js/token-hider/token-hider.js @@ -0,0 +1,39 @@ +const axios = require("axios"); +const qs = require("qs"); + +exports.handler = function(event, context, callback) { + // apply our function to the queryStringParameters and assign it to a variable + const API_PARAMS = qs.stringify(event.queryStringParameters); + // Get env var values defined in our Netlify site UI + const { API_TOKEN, API_URL } = process.env; + // In this example, the API Key needs to be passed in the params with a key of key. + // We're assuming that the ApiParams var will contain the initial ? + const URL = `${API_URL}?${API_PARAMS}&key=${API_TOKEN}`; + + // Let's log some stuff we already have. + console.log("Injecting token to", API_URL); + console.log("logging event.....", event); + console.log("Constructed URL is ...", URL); + + // Here's a function we'll use to define how our response will look like when we call callback + const pass = body => { + callback(null, { + statusCode: 200, + body: JSON.stringify(body) + }); + }; + + // Perform the API call. + const get = () => { + axios + .get(URL) + .then(response => { + console.log(response.data); + pass(response.data); + }) + .catch(err => pass(err)); + }; + if (event.httpMethod == "GET") { + get(); + } +}; diff --git a/src/functions-templates/js/url-shortener/.netlify-function-template.js b/src/functions-templates/js/url-shortener/.netlify-function-template.js new file mode 100644 index 00000000000..9821b651ae0 --- /dev/null +++ b/src/functions-templates/js/url-shortener/.netlify-function-template.js @@ -0,0 +1,34 @@ +const chalk = require("chalk"); + +module.exports = { + name: "url-shortener", + description: "URL Shortener: simple URL shortener with Netlify Forms!", + async onComplete() { + console.log( + `${chalk.yellow("url-shortener")} function created from template!` + ); + if (!process.env.ROUTES_FORM_ID || !process.env.API_AUTH) { + console.log( + `note this function requires ${chalk.yellow( + "ROUTES_FORM_ID" + )} and ${chalk.yellow( + "API_AUTH" + )} build environment variables set in your Netlify Site.` + ); + + let siteData = { name: "YOURSITENAMEHERE" }; + try { + siteData = await this.netlify.api.getSite({ + siteId: this.netlify.site.id + }); + } catch (e) { + // silent error, not important + } + console.log( + `Set them at: https://app.netlify.com/sites/${ + siteData.name + }/settings/deploys#environment-variables (must have CD setup)` + ); + } + } +}; diff --git a/src/functions-templates/js/url-shortener/generate-route.js b/src/functions-templates/js/url-shortener/generate-route.js new file mode 100644 index 00000000000..2057ca92fe2 --- /dev/null +++ b/src/functions-templates/js/url-shortener/generate-route.js @@ -0,0 +1,56 @@ +"use strict"; + +var request = require("request"); +var Hashids = require("hashids"); + +export function handler(event, context, callback) { + // Set the root URL according to the Netlify site we are within + var rootURL = process.env.URL + "/"; + + // get the details of what we are creating + var destination = event.queryStringParameters["to"]; + + // generate a unique short code (stupidly for now) + var hash = new Hashids(); + var number = Math.round(new Date().getTime() / 100); + var code = hash.encode(number); + + // ensure that a protocol was provided + if (destination.indexOf("://") == -1) { + destination = "http://" + destination; + } + + // prepare a payload to post + var payload = { + "form-name": "routes", + destination: destination, + code: code, + expires: "" + }; + + // post the new route to the Routes form + request.post({ url: rootURL, formData: payload }, function( + err, + httpResponse, + body + ) { + var msg; + if (err) { + msg = "Post to Routes stash failed: " + err; + } else { + msg = "Route registered. Site deploying to include it. " + rootURL + code; + } + console.log(msg); + // tell the user what their shortcode will be + return callback(null, { + statusCode: 200, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url: rootURL + code }) + }); + }); + + // ENHANCEMENT: check for uniqueness of shortcode + // ENHANCEMENT: let the user provide their own shortcode + // ENHANCEMENT: dont' duplicate existing routes, return the current one + // ENHANCEMENT: allow the user to specify how long the redirect should exist for +} diff --git a/src/functions-templates/js/url-shortener/get-route.js b/src/functions-templates/js/url-shortener/get-route.js new file mode 100644 index 00000000000..77ec6bc7f63 --- /dev/null +++ b/src/functions-templates/js/url-shortener/get-route.js @@ -0,0 +1,47 @@ +"use strict"; + +var request = require("request"); + +export function handler(event, context, callback) { + // which URL code are we trying to retrieve? + var code = event.queryStringParameters["code"]; + + // where is the data? + var url = + "https://api.netlify.com/api/v1/forms/" + + process.env.ROUTES_FORM_ID + + "/submissions/?access_token=" + + process.env.API_AUTH; + + request(url, function(err, response, body) { + // look for this code in our stash + if (!err && response.statusCode === 200) { + var routes = JSON.parse(body); + + for (var item in routes) { + // return the result when we find the match + if (routes[item].data.code == code) { + console.log( + "We searched for " + + code + + " and we found " + + routes[item].data.destination + ); + return callback(null, { + statusCode: 200, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + code: code, + url: routes[item].data.destination + }) + }); + } + } + } else { + return callback(null, { + statusCode: 200, + body: err + }); + } + }); +} diff --git a/src/functions-templates/js/url-shortener/package-lock.json b/src/functions-templates/js/url-shortener/package-lock.json new file mode 100644 index 00000000000..b0787363b19 --- /dev/null +++ b/src/functions-templates/js/url-shortener/package-lock.json @@ -0,0 +1,351 @@ +{ + "name": "url-shortener", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "hashids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/hashids/-/hashids-1.2.2.tgz", + "integrity": "sha512-dEHCG2LraR6PNvSGxosZHIRgxF5sNLOIBFEHbj8lfP9WWmu/PWPMzsip1drdVSOFi51N2pU7gZavrgn7sbGFuw==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "mime-db": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + }, + "mime-types": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "requires": { + "mime-db": "~1.38.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + } +} diff --git a/src/functions-templates/js/url-shortener/package.json b/src/functions-templates/js/url-shortener/package.json new file mode 100644 index 00000000000..a827260c672 --- /dev/null +++ b/src/functions-templates/js/url-shortener/package.json @@ -0,0 +1,22 @@ +{ + "name": "url-shortener", + "version": "1.0.0", + "description": "", + "main": "url-shortener.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "apis", + "url", + "js" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "hashids": "^1.2.2", + "request": "^2.88.0" + } +} diff --git a/src/functions-templates/js/url-shortener/url-shortener.js b/src/functions-templates/js/url-shortener/url-shortener.js new file mode 100644 index 00000000000..887790736c8 --- /dev/null +++ b/src/functions-templates/js/url-shortener/url-shortener.js @@ -0,0 +1,21 @@ +exports.handler = (event, context, callback) => { + const path = event.path.replace(/\.netlify\/functions\/[^\/]+/, ""); + const segments = path.split("/").filter(e => e); + + switch (event.httpMethod) { + case "GET": + // e.g. GET /.netlify/functions/url-shortener + return require("./get-route").handler(event, context, callback); + case "POST": + // e.g. POST /.netlify/functions/url-shortener + return require("./generate-route").handler(event, context, callback); + case "PUT": + // your code here + case "DELETE": + // your code here + } + return callback({ + statusCode: 500, + body: "unrecognized HTTP Method, must be one of GET/POST/PUT/DELETE" + }); +}; diff --git a/src/functions-templates/js/using-middleware/.netlify-function-template.js b/src/functions-templates/js/using-middleware/.netlify-function-template.js new file mode 100644 index 00000000000..39c36d3be60 --- /dev/null +++ b/src/functions-templates/js/using-middleware/.netlify-function-template.js @@ -0,0 +1,4 @@ +module.exports = { + name: "using-middleware", + description: "Using Middleware with middy" +}; diff --git a/src/functions-templates/js/using-middleware/package.json b/src/functions-templates/js/using-middleware/package.json new file mode 100644 index 00000000000..497c6f57117 --- /dev/null +++ b/src/functions-templates/js/using-middleware/package.json @@ -0,0 +1,19 @@ +{ + "name": "using-middleware", + "version": "1.0.0", + "description": "netlify functions:create - using middleware with your netlify function", + "main": "using-middleware.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "js" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "middy": "^0.23.0" + } +} diff --git a/src/functions-templates/js/using-middleware/using-middleware.js b/src/functions-templates/js/using-middleware/using-middleware.js new file mode 100644 index 00000000000..4326c66c861 --- /dev/null +++ b/src/functions-templates/js/using-middleware/using-middleware.js @@ -0,0 +1,63 @@ +const middy = require("middy"); +const { + jsonBodyParser, + validator, + httpErrorHandler, + httpHeaderNormalizer +} = require("middy/middlewares"); + +/* Normal lambda code */ +const businessLogic = (event, context, callback) => { + // event.body has already been turned into an object by `jsonBodyParser` middleware + const { name } = event.body; + return callback(null, { + statusCode: 200, + body: JSON.stringify({ + result: "success", + message: `Hi ${name} ⊂◉‿◉つ` + }) + }); +}; + +/* Input & Output Schema */ +const schema = { + input: { + type: "object", + properties: { + body: { + type: "object", + required: ["name"], + properties: { + name: { type: "string" } + } + } + }, + required: ["body"] + }, + output: { + type: "object", + properties: { + body: { + type: "string", + required: ["result", "message"], + properties: { + result: { type: "string" }, + message: { type: "string" } + } + } + }, + required: ["body"] + } +}; + +/* Export inputSchema & outputSchema for automatic documentation */ +exports.schema = schema; + +exports.handler = middy(businessLogic) + .use(httpHeaderNormalizer()) + // parses the request body when it's a JSON and converts it to an object + .use(jsonBodyParser()) + // validates the input + .use(validator({ inputSchema: schema.input })) + // handles common http errors and returns proper responses + .use(httpErrorHandler()); diff --git a/src/functions-templates/unused_go/hello-world/hello-world.go b/src/functions-templates/unused_go/hello-world/hello-world.go new file mode 100644 index 00000000000..32ff8b493a3 --- /dev/null +++ b/src/functions-templates/unused_go/hello-world/hello-world.go @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" +) + +func handler(request events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) { + return &events.APIGatewayProxyResponse{ + StatusCode: 200, + Body: "Hello, World", + }, nil +} + +func main() { + // Make the handler available for Remote Procedure Call by AWS Lambda + lambda.Start(handler) +} \ No newline at end of file diff --git a/src/functions-templates/unused_ts/hello-world/hello-world.ts b/src/functions-templates/unused_ts/hello-world/hello-world.ts new file mode 100644 index 00000000000..7a1c9a61810 --- /dev/null +++ b/src/functions-templates/unused_ts/hello-world/hello-world.ts @@ -0,0 +1,21 @@ +import { Handler, Context, Callback, APIGatewayEvent } from 'aws-lambda' + +interface HelloResponse { + statusCode: number + body: string +} + +const handler: Handler = (event: APIGatewayEvent, context: Context, callback: Callback) => { + const params = event.queryStringParameters + const response: HelloResponse = { + statusCode: 200, + body: JSON.stringify({ + msg: `Hello world ${Math.floor(Math.random() * 10)}`, + params + }) + } + + callback(undefined, response) +} + +export { handler } diff --git a/src/functions-templates/unused_ts/hello-world/package.json b/src/functions-templates/unused_ts/hello-world/package.json new file mode 100644 index 00000000000..68aa2f23324 --- /dev/null +++ b/src/functions-templates/unused_ts/hello-world/package.json @@ -0,0 +1,22 @@ +{ + "name": "hello-world", + "version": "1.0.0", + "description": "netlify functions:create - hello world in typescript", + "main": "hello-world.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "typescript" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.3.0", + "@types/node": "^10.12.12", + "typescript": "^3.2.2", + "@types/aws-lambda": "^8.10.15" + } +} diff --git a/src/functions-templates/unused_ts/node-fetch/node-fetch.ts b/src/functions-templates/unused_ts/node-fetch/node-fetch.ts new file mode 100644 index 00000000000..03453f8daa8 --- /dev/null +++ b/src/functions-templates/unused_ts/node-fetch/node-fetch.ts @@ -0,0 +1,26 @@ +// example of async handler using async-await +// https://github.com/netlify/netlify-lambda/issues/43#issuecomment-444618311 + +import fetch from 'node-fetch' +import { Context } from 'aws-lambda' +export async function handler(event: any, context: Context) { + try { + const response = await fetch('https://api.chucknorris.io/jokes/random') + if (!response.ok) { + // NOT res.status >= 200 && res.status < 300 + return { statusCode: response.status, body: response.statusText } + } + const data = await response.json() + + return { + statusCode: 200, + body: JSON.stringify({ msg: data.value }) + } + } catch (err) { + console.log(err) // output to netlify function log + return { + statusCode: 500, + body: JSON.stringify({ msg: err.message }) // Could be a custom message or object i.e. JSON.stringify(err) + } + } +} diff --git a/src/functions-templates/unused_ts/node-fetch/package.json b/src/functions-templates/unused_ts/node-fetch/package.json new file mode 100644 index 00000000000..33596f8d35d --- /dev/null +++ b/src/functions-templates/unused_ts/node-fetch/package.json @@ -0,0 +1,23 @@ +{ + "name": "node-fetch", + "version": "1.0.0", + "description": "netlify functions:create - using node-fetch in typescript", + "main": "node-fetch.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "netlify", + "serverless", + "typescript" + ], + "author": "Netlify", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.3.0", + "@types/node-fetch": "^2.1.4", + "@types/node": "^10.12.12", + "typescript": "^3.2.2", + "@types/aws-lambda": "^8.10.15" + } +} diff --git a/src/utils/addons.js b/src/utils/addons.js new file mode 100644 index 00000000000..0e8199db179 --- /dev/null +++ b/src/utils/addons.js @@ -0,0 +1,327 @@ +/* eslint no-console: 0 */ +const { getAddons, createAddon } = require("netlify/src/addons"); +// const chalk = require("chalk"); +// const fetch = require("node-fetch"); + +/** main section - shamelessly adapted from CLI. we can extract and dedupe later. */ +/** but we can DRY things up later. */ +// eslint-disable-next-line max-params +module.exports.createSiteAddon = async function( + accessToken, + addonName, + siteId, + siteData, + log +) { + const addons = await getAddons(siteId, accessToken); + if (typeof addons === "object" && addons.error) { + log("API Error", addons); + return false; + } + // Filter down addons to current args.name + const currentAddon = addons.find( + addon => addon.service_path === `/.netlify/${addonName}` + ); + const rawFlags = {}; + + if (currentAddon && currentAddon.id) { + log(`The "${addonName} add-on" already exists for ${siteData.name}`); + // // just exit + // log() + // const cmd = chalk.cyan(`\`netlify addons:config ${addonName}\``) + // log(`- To update this add-on run: ${cmd}`) + // const deleteCmd = chalk.cyan(`\`netlify addons:delete ${addonName}\``) + // log(`- To remove this add-on run: ${deleteCmd}`) + // log() + return false; + } + + // const manifest = await getAddonManifest(addonName, accessToken); + + let configValues = rawFlags; + // if (manifest.config) { + // const required = requiredConfigValues(manifest.config); + // console.log(`Starting the setup for "${addonName} add-on"`); + // console.log(); + + // // const missingValues = missingConfigValues(required, rawFlags); + // // if (Object.keys(rawFlags).length) { + // // const newConfig = updateConfigValues(manifest.config, {}, rawFlags) + + // // if (missingValues.length) { + // // /* Warn user of missing required values */ + // // console.log( + // // `${chalk.redBright.underline.bold(`Error: Missing required configuration for "${addonName} add-on"`)}` + // // ) + // // console.log() + // // render.missingValues(missingValues, manifest) + // // console.log() + // // const msg = `netlify addons:create ${addonName}` + // // console.log(`Please supply the configuration values as CLI flags`) + // // console.log() + // // console.log(`Alternatively, you can run ${chalk.cyan(msg)} with no flags to walk through the setup steps`) + // // console.log() + // // return false + // // } + + // // await createSiteAddon({ + // // addonName, + // // settings: { + // // siteId: siteId, + // // addon: addonName, + // // config: newConfig + // // }, + // // accessToken, + // // siteData + // // }) + // // return false + // // } + + // const words = `The ${addonName} add-on has the following configurable options:`; + // console.log(` ${chalk.yellowBright.bold(words)}`); + // render.configValues(addonName, manifest.config); + // console.log(); + // console.log(` ${chalk.greenBright.bold("Lets configure those!")}`); + + // console.log(); + // console.log( + // ` - Hit ${chalk.white.bold("enter")} to confirm value or set empty value` + // ); + // console.log( + // ` - Hit ${chalk.white.bold("ctrl + C")} to cancel & exit configuration` + // ); + // console.log(); + + // const prompts = generatePrompts({ + // config: manifest.config, + // configValues: rawFlags + // }); + + // const userInput = await inquirer.prompt(prompts); + // // Merge user input with the flags specified + // configValues = updateConfigValues(manifest.config, rawFlags, userInput); + // const missingRequiredValues = missingConfigValues(required, configValues); + // if (missingRequiredValues && missingRequiredValues.length) { + // missingRequiredValues.forEach(val => { + // console.log( + // `Missing required value "${val}". Please run the command again` + // ); + // }); + // return false; + // } + // } + + await actuallyCreateSiteAddon({ + addonName, + settings: { + siteId: siteId, + addon: addonName, + config: configValues + }, + accessToken, + siteData + }); + return addonName; // we dont really use this right now but may be helpful to know that an addon installation was successful +}; + +async function actuallyCreateSiteAddon({ + addonName, + settings, + accessToken, + siteData +}) { + const addonResponse = await createAddon(settings, accessToken); + + if (addonResponse.code === 404) { + console.log( + `No add-on "${addonName}" found. Please double check your add-on name and try again` + ); + return false; + } + console.log(`Add-on "${addonName}" created for ${siteData.name}`); + if (addonResponse.config && addonResponse.config.message) { + console.log(); + console.log(`${addonResponse.config.message}`); + } + return addonResponse; +} + +/** all the utils used in the main section */ + +// async function getAddonManifest(addonName, netlifyApiToken) { +// const url = `https://api.netlify.com/api/v1/services/${addonName}/manifest`; +// const response = await fetch(url, { +// method: "GET", +// headers: { +// "Content-Type": "application/json", +// Authorization: `Bearer ${netlifyApiToken}` +// } +// }); + +// const data = await response.json(); + +// if (response.status === 422) { +// throw new Error(`Error ${JSON.stringify(data)}`); +// } + +// return data; +// } + +// function requiredConfigValues(config) { +// return Object.keys(config).filter(key => { +// return config[key].required; +// }); +// } + +// function missingConfigValues(requiredConfig, providedConfig) { +// return requiredConfig.filter(key => { +// return !providedConfig[key]; +// }); +// } + +// function missingConfigValues(allowedConfig, currentConfig, newConfig) { +// return Object.keys(allowedConfig).reduce((acc, key) => { +// if (newConfig[key]) { +// acc[key] = newConfig[key]; +// return acc; +// } +// acc[key] = currentConfig[key]; +// return acc; +// }, {}); +// } + +// const chalk = require('chalk') + +// /* programmatically generate CLI prompts */ +// function generatePrompts(settings) { +// const { config, configValues } = settings; +// const configItems = Object.keys(config); + +// const prompts = configItems +// .map((key, i) => { +// const setting = config[key]; +// // const { type, displayName } = setting +// let prompt; +// // Tell user to use types +// if (!setting.type) { +// console.log( +// `⚠️ ${chalk.yellowBright( +// `Warning: no \`type\` is set for config key: ${configItems[i]}` +// )}` +// ); +// console.log( +// `It's highly recommended that you type your configuration values. It will help with automatic documentation, sharing of your services, and make your services configurable through a GUI` +// ); +// console.log(""); +// } + +// // Handle shorthand config. Probably will be removed. Severly limited + not great UX +// if (typeof setting === "string" || typeof setting === "boolean") { +// if (typeof setting === "string") { +// prompt = { +// type: "input", +// name: key, +// message: `Enter string value for '${key}':` +// }; +// // if current stage value set show as default +// if (configValues[key]) { +// prompt.default = configValues[key]; +// } +// } else if (typeof setting === "boolean") { +// prompt = { +// type: "confirm", +// name: key, +// message: `Do you want '${key}':` +// }; +// } +// return prompt; +// } + +// // For future use. Once UX is decided +// // const defaultValidation = (setting.required) ? validateRequired : noValidate +// const defaultValidation = noValidate; +// const validateFunction = setting.pattern +// ? validate(setting.pattern) +// : defaultValidation; +// const isRequiredText = setting.required +// ? ` (${chalk.yellow("required")})` +// : ""; +// if (setting.type === "string" || setting.type.match(/string/)) { +// prompt = { +// type: "input", +// name: key, +// message: +// `${chalk.white(key)}${isRequiredText} - ${setting.displayName}` || +// `Please enter value for ${key}`, +// validate: validateFunction +// }; +// // if value previously set show it +// if (configValues[key]) { +// prompt.default = configValues[key]; +// // else show default value if provided +// } else if (setting.default) { +// prompt.default = setting.default; +// } +// return prompt; +// } +// return undefined; +// }) +// .filter(item => { +// return typeof item !== "undefined"; +// }); +// return prompts; +// } + +// function noValidate() { +// return true; +// } + +// function validate(pattern) { +// return function(value) { +// const regex = new RegExp(pattern); +// if (value.match(regex)) { +// return true; +// } +// return `Please enter a value matching regex pattern: /${chalk.yellowBright( +// pattern +// )}/`; +// }; +// } + +// const chalk = require('chalk') +// const AsciiTable = require("ascii-table"); + +// function missingValues(values, manifest) { +// const display = values +// .map(item => { +// const itemDisplay = chalk.redBright.bold(`${item}`); +// const niceNameDisplay = manifest.config[item].displayName; +// return ` - ${itemDisplay} ${niceNameDisplay}`; +// }) +// .join("\n"); +// console.log(display); +// } + +// function configValues(addonName, configValues, currentValue) { +// const table = new AsciiTable(`${addonName} add-on settings`); + +// const tableHeader = currentValue +// ? ["Setting Name", "Current Value", "Description"] +// : ["Setting Name", "Description", "Type", "Required"]; + +// table.setHeading(...tableHeader); + +// Object.keys(configValues).map(key => { +// const { type, displayName, required } = configValues[key]; +// let requiredText = required ? `true` : `false`; +// const typeInfo = type || ""; +// const description = displayName || ""; +// if (currentValue) { +// const value = currentValue[key] || "Not supplied"; +// table.addRow(key, value, description); +// } else { +// table.addRow(key, description, typeInfo, requiredText); +// } +// }); +// console.log(table.toString()); +// } diff --git a/src/utils/detect-functions-builder.js b/src/utils/detect-functions-builder.js new file mode 100644 index 00000000000..dc63b07119f --- /dev/null +++ b/src/utils/detect-functions-builder.js @@ -0,0 +1,17 @@ +const path = require("path"); + +const detectors = require("fs") + .readdirSync(path.join(__dirname, "..", "function-builder-detectors")) + .filter(x => x.endsWith(".js")) // only accept .js detector files + .map(det => + require(path.join(__dirname, "..", `function-builder-detectors/${det}`)) + ); + +module.exports.detectFunctionsBuilder = function() { + for (const i in detectors) { + const settings = detectors[i](); + if (settings) { + return settings; + } + } +}; diff --git a/src/utils/detect-server.js b/src/utils/detect-server.js new file mode 100644 index 00000000000..b2b306456b7 --- /dev/null +++ b/src/utils/detect-server.js @@ -0,0 +1,154 @@ +const path = require("path"); +const chalk = require("chalk"); +const { NETLIFYDEVLOG } = require("netlify-cli-logo"); +const inquirer = require("inquirer"); +const fs = require("fs"); +const detectors = fs + .readdirSync(path.join(__dirname, "..", "detectors")) + .filter(x => x.endsWith(".js")) // only accept .js detector files + .map(det => { + try { + return require(path.join(__dirname, "..", `detectors/${det}`)); + } catch (err) { + console.error( + `failed to load detector: ${chalk.yellow( + det + )}, this is likely a bug in the detector, please file an issue in netlify-dev-plugin`, + err + ); + return null; + } + }) + .filter(Boolean); + +module.exports.serverSettings = async devConfig => { + let settingsArr = []; + let settings = null; + for (const i in detectors) { + const detectorResult = detectors[i](); + if (detectorResult) settingsArr.push(detectorResult); + } + if (settingsArr.length === 1) { + // vast majority of projects will only have one matching detector + settings = settingsArr[0]; + settings.args = settings.possibleArgsArrs[0]; // just pick the first one + if (!settings.args) { + const { scripts } = JSON.parse( + fs.readFileSync("package.json", { encoding: "utf8" }) + ); + // eslint-disable-next-line no-console + console.error( + "empty args assigned, this is an internal Netlify Dev bug, please report your settings and scripts so we can improve", + { scripts, settings } + ); + // eslint-disable-next-line no-process-exit + process.exit(1); + } + } else if (settingsArr.length > 1) { + /** multiple matching detectors, make the user choose */ + // lazy loading on purpose + inquirer.registerPrompt( + "autocomplete", + require("inquirer-autocomplete-prompt") + ); + const fuzzy = require("fuzzy"); + const scriptInquirerOptions = formatSettingsArrForInquirer(settingsArr); + const { chosenSetting } = await inquirer.prompt({ + name: "chosenSetting", + message: `Multiple possible start commands found`, + type: "autocomplete", + source: async function(_, input) { + if (!input || input === "") { + return scriptInquirerOptions; + } + // only show filtered results + return filterSettings(scriptInquirerOptions, input); + } + }); + settings = chosenSetting; // finally! we have a selected option + // TODO: offer to save this setting to netlify.toml so you dont keep doing this + + /** utiltities for the inquirer section above */ + function filterSettings(scriptInquirerOptions, input) { + const filteredSettings = fuzzy.filter( + input, + scriptInquirerOptions.map(x => x.name) + ); + const filteredSettingNames = filteredSettings.map(x => + input ? x.string : x + ); + return scriptInquirerOptions.filter(t => + filteredSettingNames.includes(t.name) + ); + } + + /** utiltities for the inquirer section above */ + function formatSettingsArrForInquirer(settingsArr) { + let ans = []; + settingsArr.forEach(setting => { + setting.possibleArgsArrs.forEach(args => { + ans.push({ + name: `[${chalk.yellow(setting.type)}] ${ + setting.command + } ${args.join(" ")}`, + value: { ...setting, args }, + short: setting.type + "-" + args.join(" ") + }); + }); + }); + return ans; + } + } + + /** everything below assumes we have settled on one detector */ + const tellUser = settingsField => dV => + // eslint-disable-next-line no-console + console.log( + `${NETLIFYDEVLOG} Overriding ${chalk.yellow( + settingsField + )} with setting derived from netlify.toml [dev] block: `, + dV + ); + + if (devConfig) { + settings = settings || {}; + if (devConfig.command) { + settings.command = assignLoudly( + devConfig.command.split(/\s/)[0], + settings.command || null, + tellUser("command") + ); // if settings.command is empty, its bc no settings matched + let devConfigArgs = devConfig.command.split(/\s/).slice(1); + if (devConfigArgs[0] === "run") devConfigArgs = devConfigArgs.slice(1); + settings.args = assignLoudly( + devConfigArgs, + settings.command || null, + tellUser("command") + ); // if settings.command is empty, its bc no settings matched + } + if (devConfig.port) { + settings.proxyPort = devConfig.port || settings.proxyPort; + const regexp = + devConfig.urlRegexp || + new RegExp(`(http://)([^:]+:)${devConfig.port}(/)?`, "g"); + settings.urlRegexp = settings.urlRegexp || regexp; + } + settings.dist = devConfig.publish || settings.dist; // dont loudassign if they dont need it + } + return settings; +}; + +// if first arg is undefined, use default, but tell user about it in case it is unintentional +function assignLoudly( + optionalValue, + defaultValue, + // eslint-disable-next-line no-console + tellUser = dV => console.log(`No value specified, using fallback of `, dV) +) { + if (defaultValue === undefined) throw new Error("must have a defaultValue"); + if (defaultValue !== optionalValue && optionalValue === undefined) { + tellUser(defaultValue); + return defaultValue; + } + return optionalValue; +} diff --git a/src/utils/dev.js b/src/utils/dev.js new file mode 100644 index 00000000000..eee38db7a76 --- /dev/null +++ b/src/utils/dev.js @@ -0,0 +1,118 @@ +/* eslint no-console: 0 */ + +// reusable code for netlify dev +// bit of a hasty abstraction but recommended by oclif +const { getAddons } = require("netlify/src/addons"); +const chalk = require("chalk"); +const { + NETLIFYDEVLOG, + // NETLIFYDEVWARN, + NETLIFYDEVERR +} = require("netlify-cli-logo"); +/** + * inject environment variables from netlify addons and buildbot + * into your local dev process.env + * + * ``` + * // usage example + * const { site, api } = this.netlify + * if (site.id) { + * const accessToken = api.accessToken + * const addonUrls = await addEnvVariables(site, accessToken) + * // addonUrls is only for startProxy in netlify dev:index + * } + * ``` + */ +async function addEnvVariables(api, site, accessToken) { + /** from addons */ + const addonUrls = {}; + const addons = await getAddons(site.id, accessToken).catch(error => { + console.error(error); + switch (error.status) { + default: + console.error( + `${NETLIFYDEVERR} Error retrieving addons data for site ${chalk.yellow( + site.id + )}. Double-check your login status with 'netlify status' or contact support with details of your error.` + ); + process.exit(); + } + }); + if (Array.isArray(addons)) { + addons.forEach(addon => { + addonUrls[addon.slug] = `${addon.config.site_url}/.netlify/${addon.slug}`; + for (const key in addon.env) { + const msg = () => + console.log( + `${NETLIFYDEVLOG} Injected ${chalk.yellow.bold("addon")} env var: `, + chalk.yellow(key) + ); + process.env[key] = assignLoudly(process.env[key], addon.env[key], msg); + } + }); + } + + /** from web UI */ + const apiSite = await api.getSite({ site_id: site.id }).catch(error => { + console.error(error); + switch (error.status) { + case 401: + console.error( + `${NETLIFYDEVERR} Unauthorized error: This Site ID ${chalk.yellow( + site.id + )} does not belong to your account.` + ); + console.error( + `${NETLIFYDEVERR} If you cloned someone else's code, try running 'npm unlink' and then 'npm init' or 'npm link'.` + ); + + process.exit(); + default: + console.error( + `${NETLIFYDEVERR} Error retrieving site data for site ${chalk.yellow( + site.id + )}. Double-check your login status with 'netlify status' or contact support with details of your error.` + ); + process.exit(); + } + }); + // TODO: We should move the environment outside of build settings and possibly have a + // `/api/v1/sites/:site_id/environment` endpoint for it that we can also gate access to + // In the future and that we could make context dependend + if (apiSite.build_settings && apiSite.build_settings.env) { + for (const key in apiSite.build_settings.env) { + const msg = () => + console.log( + `${NETLIFYDEVLOG} Injected ${chalk.blue.bold( + "build setting" + )} env var: `, + chalk.yellow(key) + ); + process.env[key] = assignLoudly( + process.env[key], + apiSite.build_settings.env[key], + msg + ); + } + } + + return addonUrls; +} + +module.exports = { + addEnvVariables +}; + +// if first arg is undefined, use default, but tell user about it in case it is unintentional +function assignLoudly( + optionalValue, + defaultValue, + tellUser = dV => console.log(`No value specified, using fallback of `, dV) +) { + if (defaultValue === undefined) throw new Error("must have a defaultValue"); + if (defaultValue !== optionalValue && optionalValue === undefined) { + tellUser(defaultValue); + return defaultValue; + } + return optionalValue; +} diff --git a/src/utils/finders.js b/src/utils/finders.js new file mode 100644 index 00000000000..56d86e92700 --- /dev/null +++ b/src/utils/finders.js @@ -0,0 +1,189 @@ +const path = require("path"); +const fs = require("fs"); +const packList = require("npm-packlist"); +const precinct = require("precinct"); +const resolve = require("resolve"); +const readPkgUp = require("read-pkg-up"); +const requirePackageName = require("require-package-name"); +const alwaysIgnored = new Set(["aws-sdk"]); +const debug = require("debug")("netlify-dev-plugin:src/utils/finders"); + +const ignoredExtensions = new Set([ + ".log", + ".lock", + ".html", + ".md", + ".map", + ".ts", + ".png", + ".jpeg", + ".jpg", + ".gif", + ".css", + ".patch" +]); + +function ignoreMissing(dependency, optional) { + return alwaysIgnored.has(dependency) || (optional && dependency in optional); +} + +function includeModuleFile(packageJson, moduleFilePath) { + if (packageJson.files) { + return true; + } + + return !ignoredExtensions.has(path.extname(moduleFilePath)); +} + +function getDependencies(filename, basedir) { + const servicePath = basedir; + + const filePaths = new Set(); + const modulePaths = new Set(); + const pkgs = {}; + + const modulesToProcess = []; + const localFilesToProcess = [filename]; + + function handle(name, basedir, optionalDependencies) { + const moduleName = requirePackageName(name.replace(/\\/, "/")); + + if (alwaysIgnored.has(moduleName)) { + return; + } + + try { + const pathToModule = resolve.sync(path.join(moduleName, "package.json"), { + basedir + }); + const pkg = readPkgUp.sync({ cwd: pathToModule }); + + if (pkg) { + modulesToProcess.push(pkg); + } + } catch (e) { + if (e.code === "MODULE_NOT_FOUND") { + if (ignoreMissing(moduleName, optionalDependencies)) { + debug(`WARNING missing optional dependency: ${moduleName}`); + return null; + } + try { + // this resolves the requested import also against any set up NODE_PATH extensions, etc. + const resolved = require.resolve(name); + localFilesToProcess.push(resolved); + return; + } catch (e) { + throw new Error(`Could not find "${moduleName}" module in file: ${filename.replace( + path.dirname(basedir), + "" + )}. + +Please ensure "${moduleName}" is installed in the project.`); + } + } + throw e; + } + } + + while (localFilesToProcess.length) { + const currentLocalFile = localFilesToProcess.pop(); + + if (filePaths.has(currentLocalFile)) { + continue; + } + + filePaths.add(currentLocalFile); + precinct + .paperwork(currentLocalFile, { includeCore: false }) + .forEach(dependency => { + if (dependency.indexOf(".") === 0) { + const abs = resolve.sync(dependency, { + basedir: path.dirname(currentLocalFile) + }); + localFilesToProcess.push(abs); + } else { + handle(dependency, servicePath); + } + }); + } + + while (modulesToProcess.length) { + const currentModule = modulesToProcess.pop(); + const currentModulePath = path.join(currentModule.path, ".."); + const packageJson = currentModule.pkg; + + if (modulePaths.has(currentModulePath)) { + continue; + } + modulePaths.add(currentModulePath); + pkgs[currentModulePath] = packageJson; + ["dependencies", "peerDependencies", "optionalDependencies"].forEach( + key => { + const dependencies = packageJson[key]; + + if (dependencies) { + Object.keys(dependencies).forEach(dependency => { + handle( + dependency, + currentModulePath, + packageJson.optionalDependencies + ); + }); + } + } + ); + } + + modulePaths.forEach(modulePath => { + const packageJson = pkgs[modulePath]; + let moduleFilePaths; + + moduleFilePaths = packList.sync({ path: modulePath }); + + moduleFilePaths.forEach(moduleFilePath => { + if (includeModuleFile(packageJson, moduleFilePath)) { + filePaths.add(path.join(modulePath, moduleFilePath)); + } + }); + }); + + // TODO: get rid of this + const sizes = {}; + filePaths.forEach(filepath => { + const stat = fs.lstatSync(filepath); + const ext = path.extname(filepath); + sizes[ext] = (sizes[ext] || 0) + stat.size; + }); + debug("Sizes per extension: ", sizes); + + return [...filePaths]; +} + +function findModuleDir(dir) { + let basedir = dir; + while (!fs.existsSync(path.join(basedir, "package.json"))) { + const newBasedir = path.dirname(basedir); + if (newBasedir === basedir) { + return null; + } + basedir = newBasedir; + } + return basedir; +} + +function findHandler(functionPath) { + if (fs.lstatSync(functionPath).isFile()) { + return functionPath; + } + + const handlerPath = path.join( + functionPath, + `${path.basename(functionPath)}.js` + ); + if (!fs.existsSync(handlerPath)) { + return; + } + return handlerPath; +} + +module.exports = { getDependencies, findModuleDir, findHandler }; diff --git a/src/utils/get-functions.js b/src/utils/get-functions.js new file mode 100644 index 00000000000..0c1ae7c296d --- /dev/null +++ b/src/utils/get-functions.js @@ -0,0 +1,33 @@ +const fs = require("fs"); +const path = require("path"); +const { findModuleDir, findHandler } = require("./finders"); + +module.exports = { + getFunctions(dir) { + const functions = {}; + if (fs.existsSync(dir)) { + fs.readdirSync(dir).forEach(file => { + if (dir === "node_modules") { + return; + } + const functionPath = path.resolve(path.join(dir, file)); + const handlerPath = findHandler(functionPath); + if (!handlerPath) { + return; + } + if (path.extname(functionPath) === ".js") { + functions[file.replace(/\.js$/, "")] = { + functionPath, + moduleDir: findModuleDir(functionPath) + }; + } else if (fs.lstatSync(functionPath).isDirectory()) { + functions[file] = { + functionPath: handlerPath, + moduleDir: findModuleDir(functionPath) + }; + } + }); + } + return functions; + } +}; diff --git a/src/utils/live-tunnel.js b/src/utils/live-tunnel.js new file mode 100644 index 00000000000..842b1eb1272 --- /dev/null +++ b/src/utils/live-tunnel.js @@ -0,0 +1,143 @@ +const fetch = require("node-fetch"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const execa = require("execa"); +const chalk = require("chalk"); +const { fetchLatest, updateAvailable } = require("gh-release-fetch"); +const { + NETLIFYDEVLOG, + // NETLIFYDEVWARN, + NETLIFYDEVERR +} = require("netlify-cli-logo"); + +async function createTunnel(siteId, netlifyApiToken, log) { + await installTunnelClient(log); + + if (!siteId) { + // eslint-disable-next-line no-console + console.error( + `${NETLIFYDEVERR} Error: no siteId defined, did you forget to run ${chalk.yellow( + "netlify init" + )} or ${chalk.yellow("netlify link")}?` + ); + process.exit(1); + } + log(`${NETLIFYDEVLOG} Creating Live Tunnel for ` + siteId); + const url = `https://api.netlify.com/api/v1/live_sessions?site_id=${siteId}`; + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${netlifyApiToken}` + }, + body: JSON.stringify({}) + }); + + const data = await response.json(); + + if (response.status !== 201) { + throw new Error(data.message); + } + + return data; +} + +async function connectTunnel(session, netlifyApiToken, localPort, log) { + const execPath = path.join( + os.homedir(), + ".netlify", + "tunnel", + "bin", + "live-tunnel-client" + ); + const args = [ + "connect", + "-s", + session.id, + "-t", + netlifyApiToken, + "-l", + localPort + ]; + if (process.env.DEBUG) { + args.push("-v"); + log(execPath, args); + } + + const ps = execa(execPath, args, { stdio: "inherit" }); + ps.on("close", code => process.exit(code)); + ps.on("SIGINT", process.exit); + ps.on("SIGTERM", process.exit); +} + +async function installTunnelClient(log) { + const binPath = path.join(os.homedir(), ".netlify", "tunnel", "bin"); + const execPath = path.join(binPath, "live-tunnel-client"); + const newVersion = await fetchTunnelClient(execPath); + if (!newVersion) { + return; + } + + log(`${NETLIFYDEVLOG} Installing Live Tunnel Client`); + + const win = isWindows(); + const platform = win ? "windows" : process.platform; + const extension = win ? "zip" : "tar.gz"; + const release = { + repository: "netlify/live-tunnel-client", + package: `live-tunnel-client-${platform}-amd64.${extension}`, + destination: binPath, + extract: true + }; + await fetchLatest(release); +} + +async function fetchTunnelClient(execPath) { + if (!execExist(execPath)) { + return true; + } + + const { stdout } = await execa(execPath, ["version"]); + if (!stdout) { + return false; + } + + const match = stdout.match(/^live-tunnel-client\/v?([^\s]+)/); + if (!match) { + return false; + } + + return updateAvailable("netlify/live-tunnel-client", match[1]); +} + +function execExist(binPath) { + if (!fs.existsSync(binPath)) { + return false; + } + const stat = fs.statSync(binPath); + return stat && stat.isFile() && isExe(stat.mode, stat.gid, stat.uid); +} + +function isExe(mode, gid, uid) { + if (isWindows()) { + return true; + } + + const isGroup = gid ? process.getgid && gid === process.getgid() : true; + const isUser = uid ? process.getuid && uid === process.getuid() : true; + + return Boolean( + mode & 0o0001 || (mode & 0o0010 && isGroup) || (mode & 0o0100 && isUser) + ); +} + +function isWindows() { + return process.platform === "win32"; +} + +module.exports = { + createTunnel: createTunnel, + connectTunnel: connectTunnel +}; diff --git a/src/utils/read-repo-url.js b/src/utils/read-repo-url.js new file mode 100644 index 00000000000..09d6f4e1ba4 --- /dev/null +++ b/src/utils/read-repo-url.js @@ -0,0 +1,66 @@ +const url = require("url"); +const fetch = require("node-fetch"); +const { safeJoin } = require("safe-join"); + +// supported repo host types +const GITHUB = Symbol("GITHUB"); +// const BITBUCKET = Symbol('BITBUCKET') +// const GITLAB = Symbol('GITLAB') + +/** + * Takes a url like https://github.com/netlify-labs/all-the-functions/tree/master/functions/9-using-middleware + * and returns https://api.github.com/repos/netlify-labs/all-the-functions/contents/functions/9-using-middleware + */ +async function readRepoURL(_url) { + const URL = url.parse(_url); + const repoHost = validateRepoURL(_url); + if (repoHost !== GITHUB) + throw new Error("only github repos are supported for now"); + const [owner_and_repo, contents_path] = parseRepoURL(repoHost, URL); + const folderContents = await getRepoURLContents( + repoHost, + owner_and_repo, + contents_path + ); + return folderContents; +} + +async function getRepoURLContents(repoHost, owner_and_repo, contents_path) { + // naive joining strategy for now + if (repoHost === GITHUB) { + // https://developer.github.com/v3/repos/contents/#get-contents + const APIURL = safeJoin( + "https://api.github.com/repos", + owner_and_repo, + "contents", + contents_path + ); + return fetch(APIURL) + .then(x => x.json()) + .catch( + error => console.error("Error occurred while fetching ", APIURL, error) // eslint-disable-line no-console + ); + } + throw new Error("unsupported host ", repoHost); +} + +function validateRepoURL(_url) { + const URL = url.parse(_url); + if (URL.host !== "github.com") return null; + // other validation logic here + return GITHUB; +} +function parseRepoURL(repoHost, URL) { + // naive splitting strategy for now + if (repoHost === GITHUB) { + // https://developer.github.com/v3/repos/contents/#get-contents + const [owner_and_repo, contents_path] = URL.path.split("/tree/master"); // what if it's not master? note that our contents retrieval may assume it is master + return [owner_and_repo, contents_path]; + } + throw new Error("unsupported host ", repoHost); +} + +module.exports = { + readRepoURL, + validateRepoURL +}; diff --git a/src/utils/serve-functions.js b/src/utils/serve-functions.js new file mode 100644 index 00000000000..31afb4a5f41 --- /dev/null +++ b/src/utils/serve-functions.js @@ -0,0 +1,255 @@ +const fs = require("fs"); +const express = require("express"); +const bodyParser = require("body-parser"); +const expressLogging = require("express-logging"); +const queryString = require("querystring"); +const path = require("path"); +const getPort = require("get-port"); +const chokidar = require("chokidar"); +const jwtDecode = require("jwt-decode"); +const chalk = require("chalk"); +const { + NETLIFYDEVLOG, + // NETLIFYDEVWARN, + NETLIFYDEVERR +} = require("netlify-cli-logo"); +const { getFunctions } = require("./get-functions"); + +const defaultPort = 34567; + +function handleErr(err, response) { + response.statusCode = 500; + response.write( + `${NETLIFYDEVERR} Function invocation failed: ` + err.toString() + ); + response.end(); + console.log(`${NETLIFYDEVERR} Error during invocation: `, err); // eslint-disable-line no-console +} + +// function getHandlerPath(functionPath) { +// if (functionPath.match(/\.js$/)) { +// return functionPath; +// } +// return path.join(functionPath, `${path.basename(functionPath)}.js`); +// } + +function buildClientContext(headers) { + // inject a client context based on auth header, ported over from netlify-lambda (https://github.com/netlify/netlify-lambda/pull/57) + if (!headers.authorization) return; + + const parts = headers.authorization.split(" "); + if (parts.length !== 2 || parts[0] !== "Bearer") return; + + try { + return { + identity: { + url: + "https://netlify-dev-locally-emulated-identity.netlify.com/.netlify/identity", + token: + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGRldiIsInRlc3REYXRhIjoiTkVUTElGWV9ERVZfTE9DQUxMWV9FTVVMQVRFRF9JREVOVElUWSJ9.2eSDqUOZAOBsx39FHFePjYj12k0LrxldvGnlvDu3GMI" + // you can decode this with https://jwt.io/ + // just says + // { + // "source": "netlify dev", + // "testData": "NETLIFY_DEV_LOCALLY_EMULATED_IDENTITY" + // } + }, + user: jwtDecode(parts[1]) + }; + } catch (_) { + // Ignore errors - bearer token is not a JWT, probably not intended for us + } +} + +function createHandler(dir) { + const functions = getFunctions(dir); + + const clearCache = action => path => { + console.log(`${NETLIFYDEVLOG} ${path} ${action}, reloading...`); // eslint-disable-line no-console + Object.keys(require.cache).forEach(k => { + delete require.cache[k]; + }); + }; + const watcher = chokidar.watch(dir, { ignored: /node_modules/ }); + watcher + .on("change", clearCache("modified")) + .on("unlink", clearCache("deleted")); + + return function(request, response) { + // handle proxies without path re-writes (http-servr) + const cleanPath = request.path.replace(/^\/.netlify\/functions/, ""); + + const func = cleanPath.split("/").filter(function(e) { + return e; + })[0]; + if (!functions[func]) { + response.statusCode = 404; + response.end("Function not found..."); + return; + } + const { functionPath, moduleDir } = functions[func]; + let handler; + let before = module.paths; + try { + module.paths = [moduleDir]; + handler = require(functionPath); + if (typeof handler.handler !== "function") { + throw new Error( + `function ${functionPath} must export a function named handler` + ); + } + module.paths = before; + } catch (error) { + module.paths = before; + handleErr(error, response); + return; + } + + const body = request.body.toString(); + var isBase64Encoded = Buffer.from(body, 'base64').toString('base64') === body; + + let remoteAddress = (request.headers['x-forwarded-for'] || request.headers['X-Forwarded-for'] || request.connection.remoteAddress || '') + remoteAddress = remoteAddress.split(remoteAddress.includes('.') ? ':' : ',').pop().trim() + + const lambdaRequest = { + path: request.path, + httpMethod: request.method, + queryStringParameters: queryString.parse(request.url.split(/\?(.+)/)[1]), + headers: Object.assign({}, request.headers, { 'client-ip': remoteAddress }), + body: body, + isBase64Encoded: isBase64Encoded + }; + + let callbackWasCalled = false; + const callback = createCallback(response); + // we already checked that it exports a function named handler above + const promise = handler.handler( + lambdaRequest, + { clientContext: buildClientContext(request.headers) || {} }, + callback + ); + /** guard against using BOTH async and callback */ + if (callbackWasCalled && promise && typeof promise.then === "function") { + throw new Error( + "Error: your function seems to be using both a callback and returning a promise (aka async function). This is invalid, pick one. (Hint: async!)" + ); + } else { + // it is definitely an async function with no callback called, good. + promiseCallback(promise, callback); + } + + /** need to keep createCallback in scope so we can know if cb was called AND handler is async */ + function createCallback(response) { + return function(err, lambdaResponse) { + callbackWasCalled = true; + if (err) { + return handleErr(err, response); + } + if (lambdaResponse === undefined) { + return handleErr( + "lambda response was undefined. check your function code again.", + response + ); + } + if (!Number(lambdaResponse.statusCode)) { + console.log( + `${NETLIFYDEVERR} Your function response must have a numerical statusCode. You gave: $`, + lambdaResponse.statusCode + ); + return handleErr("Incorrect function response statusCode", response); + } + if (typeof lambdaResponse.body !== "string") { + console.log( + `${NETLIFYDEVERR} Your function response must have a string body. You gave:`, + lambdaResponse.body + ); + return handleErr("Incorrect function response body", response); + } + + response.statusCode = lambdaResponse.statusCode; + // eslint-disable-line guard-for-in + for (const key in lambdaResponse.headers) { + response.setHeader(key, lambdaResponse.headers[key]); + } + response.write( + lambdaResponse.isBase64Encoded + ? Buffer.from(lambdaResponse.body, "base64") + : lambdaResponse.body + ); + response.end(); + }; + } + }; +} + +function promiseCallback(promise, callback) { + if (!promise) return; // means no handler was written + if (typeof promise.then !== "function") return; + if (typeof callback !== "function") return; + + promise.then( + function(data) { + callback(null, data); + }, + function(err) { + callback(err, null); + } + ); +} + +async function serveFunctions(settings) { + const app = express(); + const dir = settings.functionsDir; + const port = await getPort({ + port: assignLoudly(settings.port, defaultPort) + }); + + app.use( + bodyParser.text({ + limit: "6mb", + type: ["text/*", "application/json", "multipart/form-data"] + }) + ); + app.use(bodyParser.raw({ limit: "6mb", type: "*/*" })); + app.use( + expressLogging(console, { + blacklist: ["/favicon.ico"] + }) + ); + + app.get("/favicon.ico", function(req, res) { + res.status(204).end(); + }); + app.all("*", createHandler(dir)); + + app.listen(port, function(err) { + if (err) { + console.error(`${NETLIFYDEVERR} Unable to start lambda server: `, err); // eslint-disable-line no-console + process.exit(1); + } + + // add newline because this often appears alongside the client devserver's output + console.log(`\n${NETLIFYDEVLOG} Lambda server is listening on ${port}`); // eslint-disable-line no-console + }); + + return Promise.resolve({ + port + }); +} + +module.exports = { serveFunctions }; + +// if first arg is undefined, use default, but tell user about it in case it is unintentional +function assignLoudly( + optionalValue, + fallbackValue, + tellUser = dV => + console.log(`${NETLIFYDEVLOG} No port specified, using defaultPort of `, dV) // eslint-disable-line no-console +) { + if (fallbackValue === undefined) throw new Error("must have a fallbackValue"); + if (fallbackValue !== optionalValue && optionalValue === undefined) { + tellUser(fallbackValue); + return fallbackValue; + } + return optionalValue; +}