diff --git a/.eslintignore b/.eslintignore index 9f9b48d05b..e63bfb096f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ public/**/*.js +seed/unpacked/**/*.js diff --git a/.gitignore b/.gitignore index c591aad2cc..857f24aa5d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ coverage server/*.bundle.js public/js/bundle* +seed/unpacked *.map diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 124f56764a..f3bf74aa8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -409,7 +409,7 @@ nothing to commit, working directory clean `fix/short-fix-description` or `feature/short-feature-description`. Review the [Contribution Guidelines](#contribution-guidelines) for more detail. -3. Edit your file(s) locally with the editor of your choice +3. Edit your file(s) locally with the editor of your choice. To edit challenges, you may want to use `unpack` and `repack` -- see [seed/README.md](seed/README.md) for instructions. 4. Check your `git status` to see unstaged files. diff --git a/package-lock.json b/package-lock.json index 71b9cac704..7e784d2d6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -186,6 +186,24 @@ } } }, + "acorn-node": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.3.0.tgz", + "integrity": "sha512-efP54n3d1aLfjL2UMdaXa6DsswwzJeI5rqhbFvXMrKiJ6eJFpf+7R0zN7t8IC+XKn2YOAFAv6xbBNgHUkoHWLw==", + "dev": true, + "requires": { + "acorn": "5.5.3", + "xtend": "4.0.1" + }, + "dependencies": { + "acorn": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", + "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==", + "dev": true + } + } + }, "addressparser": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.1.tgz", @@ -473,6 +491,12 @@ "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -493,6 +517,18 @@ "es-abstract": "1.9.0" } }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", @@ -575,6 +611,23 @@ "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=", "dev": true }, + "astw": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/astw/-/astw-2.2.0.tgz", + "integrity": "sha1-e9QXhNMkk5h66yOba04cV6hzuRc=", + "dev": true, + "requires": { + "acorn": "4.0.13" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "dev": true + } + } + }, "async": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/async/-/async-2.1.5.tgz", @@ -2122,12 +2175,43 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "dev": true }, + "browser-pack": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.0.4.tgz", + "integrity": "sha512-Q4Rvn7P6ObyWfc4stqLWHtG1MJ8vVtjgT24Zbu+8UTzxYuZouqZsmNRRTFVMY/Ux0eIKv1d+JWzsInTX+fdHPQ==", + "dev": true, + "requires": { + "JSONStream": "1.0.3", + "combine-source-map": "0.8.0", + "defined": "1.0.0", + "safe-buffer": "5.1.1", + "through2": "2.0.3", + "umd": "3.0.3" + } + }, "browser-process-hrtime": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz", "integrity": "sha1-Ql1opY00R/AqBKqJQYf86K+Le44=", "dev": true }, + "browser-resolve": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", + "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, "browser-sync": { "version": "2.18.13", "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.18.13.tgz", @@ -2339,6 +2423,132 @@ "weinre": "2.0.0-pre-I0Z7U9OV" } }, + "browserify": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.1.1.tgz", + "integrity": "sha512-iSH21jK0+IApV8YHOfmGt1qsGd74oflQ1Ko/28JOkWLFNBngAQfKb6WYIJ9CufH8vycqKX1sYU3y7ZrVhwevAg==", + "dev": true, + "requires": { + "JSONStream": "1.0.3", + "assert": "1.4.1", + "browser-pack": "6.0.4", + "browser-resolve": "1.11.2", + "browserify-zlib": "0.2.0", + "buffer": "5.1.0", + "cached-path-relative": "1.0.1", + "concat-stream": "1.6.0", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.12.0", + "defined": "1.0.0", + "deps-sort": "2.0.0", + "domain-browser": "1.2.0", + "duplexer2": "0.1.4", + "events": "2.0.0", + "glob": "7.1.2", + "has": "1.0.1", + "htmlescape": "1.1.1", + "https-browserify": "1.0.0", + "inherits": "2.0.3", + "insert-module-globals": "7.0.2", + "labeled-stream-splicer": "2.0.0", + "mkdirp": "0.5.1", + "module-deps": "6.0.0", + "os-browserify": "0.3.0", + "parents": "1.0.1", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "read-only-stream": "2.0.0", + "readable-stream": "2.2.7", + "resolve": "1.5.0", + "shasum": "1.0.2", + "shell-quote": "1.6.1", + "stream-browserify": "2.0.1", + "stream-http": "2.7.2", + "string_decoder": "1.0.3", + "subarg": "1.0.0", + "syntax-error": "1.4.0", + "through2": "2.0.3", + "timers-browserify": "1.4.2", + "tty-browserify": "0.0.1", + "url": "0.11.0", + "util": "0.10.3", + "vm-browserify": "0.0.4", + "xtend": "4.0.1" + }, + "dependencies": { + "base64-js": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.3.tgz", + "integrity": "sha512-MsAhsUW1GxCdgYSO6tAfZrNapmUKk7mWx/k5mFY/A1gBtkaCaNapTg+FExCw1r9yeaZhqx/xPg43xgTFH6KL5w==", + "dev": true + }, + "buffer": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.1.0.tgz", + "integrity": "sha512-YkIRgwsZwJWTnyQrsBTWefizHh+8GYj3kbL1BTiAQ/9pwpino0G7B2gp5tx/FUBqUlvtxV85KNR3mwfAtv15Yw==", + "dev": true, + "requires": { + "base64-js": "1.2.3", + "ieee754": "1.1.8" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "1.0.0", + "browserify-sign": "4.0.4", + "create-ecdh": "4.0.0", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "diffie-hellman": "5.0.2", + "inherits": "2.0.3", + "pbkdf2": "3.0.14", + "public-encrypt": "4.0.0", + "randombytes": "2.0.5", + "randomfill": "1.0.3" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "events": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-2.0.0.tgz", + "integrity": "sha512-r/M5YkNg9zwI8QbSf7tsDWWJvO3PGwZXyG7GpFAxtMASnHL2eblFd7iHiGPtyGKKFPZ59S63NeX10Ws6WqGDcg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "dev": true, + "requires": { + "process": "0.11.10" + } + }, + "tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true + } + } + }, "browserify-aes": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.1.tgz", @@ -2545,6 +2755,12 @@ } } }, + "cached-path-relative": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz", + "integrity": "sha1-0JxLUoAKpMB44t2BqGmqyQ0uVOc=", + "dev": true + }, "cachedir": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-1.1.1.tgz", @@ -3147,6 +3363,26 @@ "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", "dev": true }, + "combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", + "dev": true, + "requires": { + "convert-source-map": "1.1.3", + "inline-source-map": "0.6.2", + "lodash.memoize": "3.0.4", + "source-map": "0.5.7" + }, + "dependencies": { + "convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + } + } + }, "combined-stream": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", @@ -4257,6 +4493,18 @@ "integrity": "sha1-+cmvVGSvoeepcUWKi97yqpTVuxk=", "dev": true }, + "deps-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz", + "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=", + "dev": true, + "requires": { + "JSONStream": "1.0.3", + "shasum": "1.0.2", + "subarg": "1.0.0", + "through2": "2.0.3" + } + }, "des.js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", @@ -4320,9 +4568,9 @@ "dev": true }, "diff": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", - "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, "diffie-hellman": { @@ -8311,6 +8559,12 @@ "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", "dev": true }, + "htmlescape": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", + "dev": true + }, "htmlparser2": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", @@ -8541,6 +8795,15 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=" }, + "inline-source-map": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, "inline-style-prefixer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-2.0.5.tgz", @@ -8570,6 +8833,79 @@ "through": "2.3.8" } }, + "insert-module-globals": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.0.2.tgz", + "integrity": "sha512-p3s7g96Nm62MbHRuj9ZXab0DuJNWD7qcmdUXCOQ/ZZn42DtDXfsLill7bq19lDCx3K3StypqUnuE3H2VmIJFUw==", + "dev": true, + "requires": { + "JSONStream": "1.0.3", + "combine-source-map": "0.7.2", + "concat-stream": "1.5.2", + "is-buffer": "1.1.6", + "lexical-scope": "1.2.0", + "process": "0.11.10", + "through2": "2.0.3", + "xtend": "4.0.1" + }, + "dependencies": { + "combine-source-map": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.7.2.tgz", + "integrity": "sha1-CHAxKFazB6h8xKxIbzqaYq7MwJ4=", + "dev": true, + "requires": { + "convert-source-map": "1.1.3", + "inline-source-map": "0.6.2", + "lodash.memoize": "3.0.4", + "source-map": "0.5.7" + } + }, + "concat-stream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.0.6", + "typedarray": "0.0.6" + } + }, + "convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, "interpret": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.4.tgz", @@ -9550,6 +9886,25 @@ "graceful-fs": "4.1.11" } }, + "labeled-stream-splicer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz", + "integrity": "sha1-pS4dE4AkwAuGscDJH2d5GLiuClk=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "isarray": "0.0.1", + "stream-splicer": "2.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, "latest-version": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-2.0.0.tgz", @@ -9788,6 +10143,15 @@ "type-check": "0.3.2" } }, + "lexical-scope": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/lexical-scope/-/lexical-scope-1.2.0.tgz", + "integrity": "sha1-/Ope3HBKSzqHls3KQZw6CvryLfQ=", + "dev": true, + "requires": { + "astw": "2.2.0" + } + }, "libbase64": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz", @@ -10561,6 +10925,12 @@ "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", "dev": true }, + "lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", + "dev": true + }, "lodash.merge": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz", @@ -11705,6 +12075,12 @@ "ms": "0.7.1" } }, + "diff": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", + "dev": true + }, "escape-string-regexp": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", @@ -11781,6 +12157,48 @@ "integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=", "dev": true }, + "module-deps": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.0.0.tgz", + "integrity": "sha512-BKsMhJJENEM4dTgqq2MDTTHXRHcNUFegoAwlG4HO4VMdUyMcJDKgfgI+MOv6tR5Iv8G3MKZFgsSiyP3ZoosRMw==", + "dev": true, + "requires": { + "JSONStream": "1.0.3", + "browser-resolve": "1.11.2", + "cached-path-relative": "1.0.1", + "concat-stream": "1.6.0", + "defined": "1.0.0", + "detective": "5.1.0", + "duplexer2": "0.1.4", + "inherits": "2.0.3", + "parents": "1.0.1", + "readable-stream": "2.2.7", + "resolve": "1.5.0", + "stream-combiner2": "1.1.1", + "subarg": "1.0.0", + "through2": "2.0.3", + "xtend": "4.0.1" + }, + "dependencies": { + "detective": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.1.0.tgz", + "integrity": "sha512-TFHMqfOvxlgrfVzTEkNBSh9SvSNX/HfF4OFI2QFGCyPm02EsyILqnUeb5P6q7JZ3SFNTBL5t2sePRgrN4epUWQ==", + "dev": true, + "requires": { + "acorn-node": "1.3.0", + "defined": "1.0.0", + "minimist": "1.2.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "module-details-from-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", @@ -13287,6 +13705,15 @@ "readable-stream": "2.2.7" } }, + "parents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "dev": true, + "requires": { + "path-platform": "0.11.15" + } + }, "parse-asn1": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", @@ -13534,6 +13961,12 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" }, + "path-platform": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", + "dev": true + }, "path-root": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", @@ -14383,6 +14816,15 @@ "readable-stream": "2.2.7" } }, + "read-only-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "dev": true, + "requires": { + "readable-stream": "2.2.7" + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -15486,6 +15928,27 @@ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.0.2.tgz", "integrity": "sha512-zlVXeVUKvo+HEv1e2KQF/csyeMKx2oHvatQ9l6XjCUj3agvC8XGf6R9HvIPDSmp8FNPvx7b5kaEJTRi7CqxtEw==" }, + "shasum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", + "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", + "dev": true, + "requires": { + "json-stable-stringify": "0.0.1", + "sha.js": "2.4.9" + }, + "dependencies": { + "json-stable-stringify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", + "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", + "dev": true, + "requires": { + "jsonify": "0.0.0" + } + } + } + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -15499,6 +15962,18 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true, + "requires": { + "array-filter": "0.0.1", + "array-map": "0.0.0", + "array-reduce": "0.0.0", + "jsonify": "0.0.0" + } + }, "shelljs": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", @@ -16458,6 +16933,16 @@ "duplexer": "0.0.4" } }, + "stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "dev": true, + "requires": { + "duplexer2": "0.1.4", + "readable-stream": "2.2.7" + } + }, "stream-consume": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.0.tgz", @@ -16497,6 +16982,16 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" }, + "stream-splicer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz", + "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.2.7" + } + }, "stream-throttle": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", @@ -16849,6 +17344,23 @@ "integrity": "sha1-ISqQDfq1rgTmKOIbWABjK9DZ40c=", "dev": true }, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "dev": true, + "requires": { + "minimist": "1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "sum-up": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sum-up/-/sum-up-1.0.3.tgz", @@ -16898,6 +17410,15 @@ "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", "dev": true }, + "syntax-error": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", + "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", + "dev": true, + "requires": { + "acorn-node": "1.3.0" + } + }, "table": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", @@ -17781,6 +18302,12 @@ "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", "dev": true }, + "umd": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", + "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", + "dev": true + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", diff --git a/package.json b/package.json index 371497d009..4f698f7c40 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "prelint-js": "npm run create-rev", "pretest": "npm run create-rev && npm run lint", "prestart-production": "gulp build -p", + "seed": "node seed", "snyk-protect": "snyk protect", "start": "babel-node server/server.js", "start-production": "node pm2Start", @@ -31,7 +32,9 @@ "test-js": "npm run test-js-client && npm run test-js-common && npm run test-js-server", "test-js-client": "tape -r babel-register \"client/**/*.test.js\" | tap-spec", "test-js-common": "tape -r babel-register \"common/**/*.test.js\" | tap-spec", - "test-js-server": "tape -r babel-register \"server/**/*.test.js\" | tap-spec" + "test-js-server": "tape -r babel-register \"server/**/*.test.js\" | tap-spec", + "unpack": "babel-node ./seed/unpack.js", + "repack": "babel-node ./seed/repack.js" }, "license": "(BSD-3-Clause AND CC-BY-SA-4.0)", "dependencies": { @@ -156,10 +159,12 @@ "babel-preset-stage-0": "^6.3.13", "babel-preset-stage-3": "^6.24.1", "browser-sync": "^2.9.12", + "browserify": "^16.1.1", "chunk-manifest-webpack-plugin": "^1.1.2", "commitizen": "^2.9.6", "cz-freecodecamp": "^1.0.1", "del": "^2.2.0", + "diff": "^3.5.0", "eslint": "^4.10.0", "eslint-config-freecodecamp": "^1.1.1", "gulp": "^3.9.1", diff --git a/seed/README.md b/seed/README.md index 0a6aadc9c7..f038a5ab64 100644 --- a/seed/README.md +++ b/seed/README.md @@ -10,8 +10,17 @@ For each challenge section, there is a JSON file (fields documented below) conta |---|---| | `npm run test-challenges` | run all challenge tests (for each challenge JSON file, run all `tests` against all `solutions`) | | `npm run test` | run all JS tests in the system, including client, server, lint and challenge tests | -| `node seed` | parses all the challenge JSON files and saves them into MongoDB (code is inside [index.js](index.js)) | +| `npm run seed`
  (or `node seed`) | parses all the challenge JSON files and saves them into MongoDB (code is inside [index.js](index.js)) | | `npm run commit` | interactive tool to help you build a good commit message | +| `npm run unpack` | extract challenges from `seed/challenges` into `unpacked` subdirectory, one HTML page per challenge | +| `npm run repack` | repack challenges from `unpacked` subdirectory into `seed/challenges` | + +### unpack and repack + +`npm run unpack` extracts challenges into separate files for easier viewing and editing. The files are `.gitignore`d and will *not* be checked in, and all mongo seed importing will keep using the existing system; this is essentially a tool for editing `challenge.json` files. These HTML files are self-contained and run their own tests -- open a browser JS console to see the test results. + +`npm run repack` gathers up the unpacked/edited HTML files into challenge-block JSON files. Use `git diff` to see the changes + ## Links @@ -24,7 +33,7 @@ For each challenge section, there is a JSON file (fields documented below) conta ## Challenge Template -```json +``` { "id": "unique identifier (alphanumerical, mongodb id)", "title": "Challenge Title", diff --git a/seed/addAssertsToTapTest.js b/seed/addAssertsToTapTest.js new file mode 100644 index 0000000000..58fb62a3e1 --- /dev/null +++ b/seed/addAssertsToTapTest.js @@ -0,0 +1,52 @@ +let _ = require('lodash'); + +function createIsAssert(tapTest, isThing) { + const { assert } = tapTest; + return function() { + const args = [...arguments]; + args[0] = isThing(args[0]); + assert.apply(tapTest, args); + }; +} + +function addAssertsToTapTest(tapTest) { + const assert = tapTest.assert; + + assert.isArray = createIsAssert(tapTest, _.isArray); + assert.isBoolean = createIsAssert(tapTest, _.isBoolean); + assert.isString = createIsAssert(tapTest, _.isString); + assert.isNumber = createIsAssert(tapTest, _.isNumber); + assert.isUndefined = createIsAssert(tapTest, _.isUndefined); + + assert.deepEqual = tapTest.deepEqual; + assert.equal = tapTest.equal; + assert.strictEqual = tapTest.equal; + assert.sameMembers = function sameMembers() { + const [ first, second, ...args] = arguments; + assert.apply( + tapTest, + [ + _.difference(first, second).length === 0 && + _.difference(second, first).length === 0 + ].concat(args) + ); + }; + assert.includeMembers = function includeMembers() { + const [ first, second, ...args] = arguments; + assert.apply(tapTest, + [ + _.difference(second, first).length === 0 + ].concat(args)); + }; + assert.match = function match() { + const [value, regex, ...args] = arguments; + assert.apply(tapTest, + [ + regex.test(value) + ].concat(args)); + }; + + return assert; +} + +module.exports = addAssertsToTapTest; diff --git a/seed/getChallenges.js b/seed/getChallenges.js index 7ff5f6b6d5..a8a413fc99 100644 --- a/seed/getChallenges.js +++ b/seed/getChallenges.js @@ -4,64 +4,54 @@ const fs = require('fs'); const path = require('path'); const hiddenFile = /(^(\.|\/\.))|(.md$)/g; + function getFilesFor(dir) { - return fs.readdirSync(path.join(__dirname, '/' + dir)) + let targetDir = path.join(__dirname, dir); + return fs.readdirSync(targetDir) .filter(file => !hiddenFile.test(file)) .map(function(file) { let superBlock; - if (fs.statSync(path.join(__dirname, dir + '/' + file)).isFile()) { - return { file: file }; + if (fs.statSync(path.join(targetDir, file)).isFile()) { + return {file: file}; } superBlock = file; - return getFilesFor(dir + '/' + superBlock) + return getFilesFor(path.join(dir, superBlock)) .map(function(data) { return { - file: superBlock + '/' + data.file, + file: path.join(superBlock, data.file), superBlock: superBlock }; }); }) - .reduce(function(files, file) { - if (!Array.isArray(file)) { - files.push(file); - return files; - } - return files.concat(file); + .reduce(function(files, entry) { + return files.concat(entry); }, []); } -function getSupOrder(filePath) { - const order = parseInt((filePath || '').split('-')[0], 10); - // check for NaN - if (order !== order) { - return 0; +function superblockInfo(filePath) { + let parts = (filePath || '').split('-'); + let order = parseInt(parts[0], 10); + if (isNaN(order)) { + return {order: 0, name: filePath}; + } else { + return { + order: order, + name: parts.splice(1).join('-') + }; } - return order; } -function getSupName(filePath) { - const order = parseInt((filePath || '').split('-')[0], 10); - // check for NaN - if (order !== order) { - return filePath; - } - - return (filePath || '').split('-').splice(1).join('-'); -} - -module.exports = function getChallenges() { - try { - return getFilesFor('challenges') - .map(function(data) { - const challengeSpec = require('./challenges/' + data.file); - challengeSpec.fileName = data.file; - challengeSpec.superBlock = getSupName(data.superBlock); - challengeSpec.superOrder = getSupOrder(data.superBlock); - - return challengeSpec; - }); - } catch (e) { - console.error('error: ', e); - return []; +module.exports = function getChallenges(challengesDir) { + if (!challengesDir) { + challengesDir = 'challenges'; } + return getFilesFor(challengesDir) + .map(function(data) { + const challengeSpec = require('./' + challengesDir + '/' + data.file); + let superInfo = superblockInfo(data.superBlock); + challengeSpec.fileName = data.file; + challengeSpec.superBlock = superInfo.name; + challengeSpec.superOrder = superInfo.order; + return challengeSpec; + }); }; diff --git a/seed/mongoIds.js b/seed/mongoIds.js new file mode 100644 index 0000000000..034a0b48cf --- /dev/null +++ b/seed/mongoIds.js @@ -0,0 +1,24 @@ +import _ from 'lodash'; +import { isMongoId } from 'validator'; + +class MongoIds { + constructor() { + this.knownIds = []; + } + check(id, title) { + if (!isMongoId(id)) { + throw new Error(`Expected a valid ObjectId for ${title}, but got ${id}`); + } + const idIndex = _.findIndex(this.knownIds, existing => id === existing); + if (idIndex !== -1) { + throw new Error(` + All challenges must have a unique id. + + The id for ${title} is already assigned + `); + } + this.knownIds = [ ...this.knownIds, id ]; + } +} + +export default MongoIds; diff --git a/seed/normalize-seed-files.js b/seed/normalize-seed-files.js index 4be5fec3e6..a777f3ca72 100644 --- a/seed/normalize-seed-files.js +++ b/seed/normalize-seed-files.js @@ -52,16 +52,16 @@ function createNewTranslations(challenge) { newTranslation = {}; newTranslation[matches[1]] = challenge[oldKey]; translations[tag] = translations[tag] ? - Object.assign({}, translations[tag], newTranslation) : - Object.assign({}, newTranslation); + ({...translations[tag], ...newTranslation}) : + ({...newTranslation}); return translations; } matches = oldKey.match(oldNameRegex); tag = normalizeLangTag(matches[1]); newTranslation = { title: challenge[oldKey] }; translations[tag] = translations[tag] ? - Object.assign({}, translations[tag], newTranslation) : - Object.assign({}, newTranslation); + ({...translations[tag], ...newTranslation}) : + ({...newTranslation}); return translations; }, {}); } @@ -71,11 +71,10 @@ function normalizeChallenge(challenge) { challenge.translations = challenge.translations || {}; var hasOldTranslations = keys.some(hasOldTranslation); if (hasOldTranslations) { - challenge.translations = Object.assign( - {}, - challenge.translations, - createNewTranslations(challenge) - ); + challenge.translations = ({ + ...challenge.translations, + ...createNewTranslations(challenge) + }); } challenge.translations = sortTranslationsKeys(challenge.translations); // remove old translations from the top level diff --git a/seed/repack.js b/seed/repack.js new file mode 100644 index 0000000000..9328b3e611 --- /dev/null +++ b/seed/repack.js @@ -0,0 +1,76 @@ +/* eslint-disable no-eval, no-process-exit */ +import fs from 'fs-extra'; +import path from 'path'; +import {UnpackedChallenge, ChallengeFile} from './unpackedChallenge'; + +const jsdiff = require('diff'); + +// Repack all challenges from all +// seed/unpacked/00-foo/bar/000-id.html files +// into +// seed/challenges/00-foo/bar.json files + +let unpackedRoot = path.join(__dirname, 'unpacked'); +let seedChallengesRoot = path.join(__dirname, 'challenges'); + +function directoriesIn(parentDir) { + return fs.readdirSync(parentDir) + .filter(entry => fs.statSync(path.join(parentDir, entry)).isDirectory()); +} + +let superBlocks = directoriesIn(unpackedRoot); +console.log(superBlocks); + +function diffFiles(originalFilePath, changedFilePath) { + // todo: async + console.log(`diffing ${originalFilePath} and ${changedFilePath}`); + let original = fs.readFileSync(originalFilePath).toString(); + let repacked = fs.readFileSync(changedFilePath).toString(); + + let changes = jsdiff.diffLines(original, repacked, { newlineIsToken: true }); + changes.forEach((change) => { + if (change.added || change.removed) { + console.log(JSON.stringify(change, null, 2)); + } + }); + console.log(''); +} + +superBlocks.forEach(superBlock => { + let superBlockPath = path.join(unpackedRoot, superBlock); + let blocks = directoriesIn(superBlockPath); + blocks.forEach(blockName => { + let blockPath = path.join(superBlockPath, blockName); + let blockFilePath = path.join(blockPath, blockName + '.json'); + let block = require(blockFilePath); + let index = 0; + block.challenges.forEach(challengeJson => { + let unpackedChallenge = + new UnpackedChallenge(blockPath, challengeJson, index); + let unpackedFile = unpackedChallenge.challengeFile(); + let chunks = unpackedFile.readChunks(); + + Object.assign(block.challenges[ index ], chunks); + + index += 1; + }); + + let outputFilePath = + path.join(seedChallengesRoot, superBlock, blockName + '.json'); + // todo: async + fs.writeFileSync(outputFilePath, JSON.stringify(block, null, 2)); + + // todo: make this a command-line option instead + let doDiff = false; + if (doDiff) { + diffFiles(blockFilePath, outputFilePath); + } + + }); + +}); + +// let challenges = getChallenges(); +// challenges.forEach(challengeBlock => { +// console.log() +// }); diff --git a/seed/test-challenges.js b/seed/test-challenges.js index 1f17a2f768..a7cdf6b11b 100644 --- a/seed/test-challenges.js +++ b/seed/test-challenges.js @@ -1,76 +1,106 @@ -/* eslint-disable no-eval, no-process-exit */ -import _ from 'lodash'; -import { Observable } from 'rx'; +/* eslint-disable no-eval, no-process-exit, no-unused-vars */ + +import {Observable} from 'rx'; import tape from 'tape'; -import { isMongoId } from 'validator'; import getChallenges from './getChallenges'; import { modern } from '../common/app/utils/challengeTypes'; +import MongoIds from './mongoIds'; +import addAssertsToTapTest from './addAssertsToTapTest'; -const notMongoId = id => !isMongoId(id); +let mongoIds = new MongoIds(); -let existingIds = []; +function evaluateTest(solution, assert, + react, redux, reactRedux, + head, tail, + test, tapTest) { + + let code = solution; + + /* NOTE: Provide dependencies for React/Redux challenges + * and configure testing environment + */ + let React, + ReactDOM, + Redux, + ReduxThunk, + ReactRedux, + Enzyme, + document; + + // Fake Deep Equal dependency + const DeepEqual = (a, b) => + JSON.stringify(a) === JSON.stringify(b); + + // Hardcode Deep Freeze dependency + const DeepFreeze = (o) => { + Object.freeze(o); + Object.getOwnPropertyNames(o).forEach(function(prop) { + if (o.hasOwnProperty(prop) + && o[ prop ] !== null + && ( + typeof o[ prop ] === 'object' || + typeof o[ prop ] === 'function' + ) + && !Object.isFrozen(o[ prop ])) { + DeepFreeze(o[ prop ]); + } + }); + return o; + }; + + if (react || redux || reactRedux) { + // Provide dependencies, just provide all of them + React = require('react'); + ReactDOM = require('react-dom'); + Redux = require('redux'); + ReduxThunk = require('redux-thunk'); + ReactRedux = require('react-redux'); + Enzyme = require('enzyme'); + const Adapter15 = require('enzyme-adapter-react-15'); + Enzyme.configure({ adapter: new Adapter15() }); + + /* Transpile ALL the code + * (we may use JSX in head or tail or tests, too): */ + const transform = require('babel-standalone').transform; + const options = { presets: [ 'es2015', 'react' ] }; + + head = transform(head, options).code; + solution = transform(solution, options).code; + tail = transform(tail, options).code; + test = transform(test, options).code; + + const { JSDOM } = require('jsdom'); + // Mock DOM document for ReactDOM.render method + const jsdom = new JSDOM(` + + +
+ + + `); + const { window } = jsdom; + + // Mock DOM for ReactDOM tests + document = window.document; + global.window = window; + global.document = window.document; -function validateObjectId(id, title) { - if (notMongoId(id)) { - throw new Error(`Expected a vaild ObjectId for ${title}, got ${id}`); } - const idIndex = _.findIndex(existingIds, existing => id === existing); - if (idIndex !== -1) { - throw new Error(` - All challenges must have a unique id. - The id for ${title} is already assigned - `); + /* eslint-enable no-unused-vars */ + try { + (() => { + return eval( + head + '\n;;' + + solution + '\n;;' + + tail + '\n;;' + + test + ); + })(); + } catch (e) { + tapTest.fail(e); } - existingIds = [ ...existingIds, id ]; - return; -} - -function createIsAssert(t, isThing) { - const { assert } = t; - return function() { - const args = [...arguments]; - args[0] = isThing(args[0]); - assert.apply(t, args); - }; -} - -function fillAssert(t) { - const assert = t.assert; - - assert.isArray = createIsAssert(t, _.isArray); - assert.isBoolean = createIsAssert(t, _.isBoolean); - assert.isString = createIsAssert(t, _.isString); - assert.isNumber = createIsAssert(t, _.isNumber); - assert.isUndefined = createIsAssert(t, _.isUndefined); - - assert.deepEqual = t.deepEqual; - assert.equal = t.equal; - assert.strictEqual = t.equal; - - assert.sameMembers = function sameMembers() { - const [ first, second, ...args] = arguments; - assert.apply( - t, - [ - _.difference(first, second).length === 0 && - _.difference(second, first).length === 0 - ].concat(args) - ); - }; - - assert.includeMembers = function includeMembers() { - const [ first, second, ...args] = arguments; - assert.apply(t, [_.difference(second, first).length === 0].concat(args)); - }; - - assert.match = function match() { - const [value, regex, ...args] = arguments; - assert.apply(t, [regex.test(value)].concat(args)); - }; - - return assert; } function createTest({ @@ -84,7 +114,8 @@ function createTest({ redux = false, reactRedux = false }) { - validateObjectId(id, title); + mongoIds.check(id, title); + solutions = solutions.filter(solution => !!solution); tests = tests.filter(test => !!test); @@ -92,7 +123,10 @@ function createTest({ const isAsync = s => s.includes('(async () => '); if (isAsync(tests.join(''))) { console.log(`Replacing Async Tests for Challenge ${title}`); - tests = tests.map(t => isAsync(t) ? "assert(true, 'message: great');" : t); + tests = tests.map(challengeTestSource => + isAsync(challengeTestSource) ? + "assert(true, 'message: great');" : + challengeTestSource); } head = head.join('\n'); @@ -106,116 +140,26 @@ function createTest({ } return Observable.fromCallback(tape)(title) - .doOnNext(t => solutions.length ? t.plan(plan) : t.end()) - .flatMap(t => { + .doOnNext(tapTest => + solutions.length ? tapTest.plan(plan) : tapTest.end()) + .flatMap(tapTest => { if (solutions.length <= 0) { - t.comment('No solutions for ' + title); + tapTest.comment('No solutions for ' + title); return Observable.just({ title, type: 'missing' }); } - return Observable.just(t) - .map(fillAssert) + return Observable.just(tapTest) + .map(addAssertsToTapTest) /* eslint-disable no-unused-vars */ // assert and code used within the eval .doOnNext(assert => { solutions.forEach(solution => { - // Original code string - const originalCode = solution; tests.forEach(test => { - let code = solution; - - /* NOTE: Provide dependencies for React/Redux challenges - * and configure testing environment - */ - - let React, - ReactDOM, - Redux, - ReduxThunk, - ReactRedux, - Enzyme, - document; - - // Fake Deep Equal dependency - const DeepEqual = (a, b) => - JSON.stringify(a) === JSON.stringify(b); - - // Hardcode Deep Freeze dependency - const DeepFreeze = (o) => { - Object.freeze(o); - Object.getOwnPropertyNames(o).forEach(function(prop) { - if (o.hasOwnProperty(prop) - && o[prop] !== null - && ( - typeof o[prop] === 'object' || - typeof o[prop] === 'function' - ) - && !Object.isFrozen(o[prop])) { - DeepFreeze(o[prop]); - } - }); - return o; - }; - - if (react || redux || reactRedux) { - // Provide dependencies, just provide all of them - React = require('react'); - ReactDOM = require('react-dom'); - Redux = require('redux'); - ReduxThunk = require('redux-thunk'); - ReactRedux = require('react-redux'); - Enzyme = require('enzyme'); - const Adapter15 = require('enzyme-adapter-react-15'); - Enzyme.configure({ adapter: new Adapter15() }); - - /* Transpile ALL the code - * (we may use JSX in head or tail or tests, too): */ - const transform = require('babel-standalone').transform; - const options = { presets: [ 'es2015', 'react' ] }; - - head = transform(head, options).code; - solution = transform(solution, options).code; - tail = transform(tail, options).code; - test = transform(test, options).code; - - const { JSDOM } = require('jsdom'); - // Mock DOM document for ReactDOM.render method - const jsdom = new JSDOM(` - - -
- - - `); - const { window } = jsdom; - - // Mock DOM for ReactDOM tests - document = window.document; - global.window = window; - global.document = window.document; - - } - - const editor = { - getValue() { return code; }, - getOriginalCode() { return originalCode; } - }; - /* eslint-enable no-unused-vars */ - try { - (() => { - return eval( - head + '\n;;' + - solution + '\n;;' + - tail + '\n;;' + - test - ); - })(); - } catch (e) { - t.fail(e); - } + evaluateTest(solution, assert, react, redux, reactRedux, + head, tail, test, tapTest); }); }); }) @@ -244,10 +188,12 @@ Observable.from(getChallenges()) if (noSolutions) { console.log( '# These challenges have no solutions\n- [ ] ' + - noSolutions.join('\n- [ ] ') + noSolutions.join('\n- [ ] ') ); } }, - err => { throw err; }, + err => { + throw err; + }, () => process.exit(0) ); diff --git a/seed/unpack.js b/seed/unpack.js new file mode 100644 index 0000000000..36410e4653 --- /dev/null +++ b/seed/unpack.js @@ -0,0 +1,87 @@ +/* eslint-disable no-eval, no-process-exit */ +import fs from 'fs-extra'; +import path from 'path'; +import browserify from 'browserify'; +import getChallenges from './getChallenges'; +import {UnpackedChallenge, ChallengeFile} from './unpackedChallenge'; + +// Unpack all challenges +// from all seed/challenges/00-foo/bar.json files +// into seed/unpacked/00-foo/bar/000-id.html files +// +// todo: unpack translations too +// todo: use common/app/routes/Challenges/utils/index.js:15 maps +// to determine format/style for non-JS tests +// todo: figure out embedded images etc. served from elsewhere in the project +// todo: prettier/clearer CSS + +// bundle up the test-running JS +function createUnpackedBundle() { + let unpackedFile = path.join(__dirname, 'unpacked.js'); + let b = browserify(unpackedFile).bundle(); + b.on('error', console.error); + let unpackedBundleFile = + path.join(__dirname, 'unpacked', 'unpacked-bundle.js'); + const bundleFileStream = fs.createWriteStream(unpackedBundleFile); + bundleFileStream.on('finish', () => { + console.log('Wrote bundled JS into ' + unpackedBundleFile); + }); + bundleFileStream.on('pipe', () => { + console.log('Writing bundled JS...'); + }); + bundleFileStream.on('error', console.error); + b.pipe(bundleFileStream); + // bundleFileStream.end(); // do not do this prematurely! +} + +let currentlyUnpackingDir = null; + +function unpackChallengeBlock(challengeBlock) { + let challengeBlockPath = path.parse(challengeBlock.fileName); + let unpackedChallengeBlockDir = path.join( + __dirname, + 'unpacked', + challengeBlockPath.dir, + challengeBlockPath.name + ); + fs.mkdirp(unpackedChallengeBlockDir, (err) => { + if (err && err.code !== 'EEXIST') { + console.log(err); + throw err; + } + + if (currentlyUnpackingDir !== challengeBlockPath.dir) { + currentlyUnpackingDir = challengeBlockPath.dir; + console.log(`Unpacking into ${currentlyUnpackingDir}:`); + } + console.log(` ${challengeBlock.name}`); + + // write a copy of the challenge block into unpacked dir + delete challengeBlock.fileName; + delete challengeBlock.superBlock; + delete challengeBlock.superOrder; + let challengeBlockCopy = + new ChallengeFile( + unpackedChallengeBlockDir, + challengeBlockPath.name, + '.json'); + challengeBlockCopy.write(JSON.stringify(challengeBlock, null, 2)); + + // unpack each challenge into an HTML file + let index = 0; + challengeBlock.challenges.forEach(challenge => { + new UnpackedChallenge( + unpackedChallengeBlockDir, + challenge, + index + ).unpack(); + index += 1; + }); + }); +} + +createUnpackedBundle(); +let challenges = getChallenges(); +challenges.forEach(challengeBlock => { + unpackChallengeBlock(challengeBlock); +}); diff --git a/seed/unpacked.css b/seed/unpacked.css new file mode 100644 index 0000000000..807168aac7 --- /dev/null +++ b/seed/unpacked.css @@ -0,0 +1,22 @@ +body { + font-family: sans-serif; +} + +script.unpacked, pre.unpacked { + display: block; + font-family: monospace; + font-size: 14px; + white-space: pre; + border: 1px solid blue; + background: #EFEFEF; + padding: .5em 1em; + margin: 1em; + overflow: auto; +} + +div.unpacked { + border: 1px solid black; + padding: .5em 1em; + margin: 1em; + overflow: auto; +} diff --git a/seed/unpacked.js b/seed/unpacked.js new file mode 100644 index 0000000000..4c9d2ad829 --- /dev/null +++ b/seed/unpacked.js @@ -0,0 +1,18 @@ +/* eslint-disable no-unused-vars,max-len */ +window._ = require('lodash'); +window.test = require('tape').test; + +// check for Browser TAP chrome extension, available here: +// https://chrome.google.com/webstore/detail/browser-tap/ncfblaiipckncgeipgmpdioedcdmofei?hl=en +if (window.tapExtension) { + window.test = window.tapExtension(window.test); +} + +window.addAssertsToTapTest = require('./addAssertsToTapTest'); +window.$ = require('jquery'); + +test('framework', function(t) { + t.plan(1); + t.equal(1, 1, 'one equals one'); +}); + diff --git a/seed/unpackedChallenge.js b/seed/unpackedChallenge.js new file mode 100644 index 0000000000..f419c2eb26 --- /dev/null +++ b/seed/unpackedChallenge.js @@ -0,0 +1,258 @@ +/* eslint-disable no-inline-comments */ +import fs from 'fs-extra'; +import path from 'path'; +import _ from 'lodash'; + +const jsonLinePrefix = '//--JSON:'; + +class ChallengeFile { + constructor(dir, name, suffix) { + this.dir = dir; + this.name = name; + this.suffix = suffix; + } + + filePath() { + return path.join(this.dir, this.name + this.suffix); + } + + write(contents) { + if (_.isArray(contents)) { + contents = contents.join('\n'); + } + fs.writeFile(this.filePath(), contents, err => { + if (err) { + throw err; + } + }); + } + + readChunks() { + // todo: make this work async + // todo: make sure it works with encodings + let data = fs.readFileSync(this.filePath()); + let lines = data.toString().split(/(?:\r\n|\r|\n)/g); + let chunks = {}; + let readingChunk = null; + lines.forEach(line => { + let chunkEnd = /( { + if (_.isString(part)) { + out.push(part.toString()); + } else { + // Descriptions are weird since sometimes they're text and sometimes + // they're "steps" which appear one at a time with optional pix and + // captions and links, or "questions" with choices and expanations... + // For now we preserve non-string descriptions via JSON but this is + // not a great solution. + // It would be better if "steps" and "description" were separate fields. + // For the record, the (unnamed) fields in step are: + // 0: image URL + // 1: caption + // 2: text + // 3: link URL + out.push(jsonLinePrefix + JSON.stringify(part)); + } + }); + // indent by 2 + return out; + } + + expandedTests(tests) { + if (!tests) { + return []; + } + let out = []; + tests.forEach(test => { + if (_.isString(test)) { + out.push(test); + } else { + // todo: figure out what to do about these id-title challenge links + out.push(jsonLinePrefix + JSON.stringify(test)); + } + }); + return out; + } + + unpackedHTML() { + let text = []; + text.push(''); + text.push(''); + text.push(''); + text.push(''); + text.push(''); + text.push(''); + text.push(''); + text.push(`

${this.challenge.title}

`); + text.push(`

This is the unpacked version of + ${this.superBlockName}/${this.challengeBlockName} + (challenge id ${this.challenge.id}).

`); + text.push('

Open the JavaScript console to see test results.

'); + + // text.push(`

Edit this HTML file (between <!--s only!) + // and run npm repack ??? + // to incorporate your changes into the challenge database.

`); + + text.push(''); + text.push('

Description

'); + text.push('
'); + text.push(''); + text.push(this.expandedDescription(this.challenge.description).join('\n')); + text.push(''); + text.push('
'); + + text.push(''); + text.push('

Seed

'); + text.push('
');
+    if (this.challenge.seed) {
+      text.push(text, this.challenge.seed.join('\n'));
+    }
+    text.push('');
+    text.push('
'); + + // Q: What is the difference between 'seed' and 'challengeSeed' ? + text.push(''); + text.push('

Challenge Seed

'); + text.push('
');
+    if (this.challenge.challengeSeed) {
+      text.push(text, this.challenge.challengeSeed.join('\n'));
+    }
+    text.push('');
+    text.push('
'); + + text.push(''); + text.push('

Head

'); + text.push(''); + + text.push(''); + text.push('

Solution

'); + text.push( + ''); + + text.push(''); + text.push('

Tail

'); + text.push(''); + + text.push(''); + text.push('

Tests

'); + text.push(''); + + text.push(''); + text.push(''); + text.push(''); + return text; + } +} + +export {UnpackedChallenge}; +