feat(seed): "unpack" and "repack" scripts

add "npm run seed" as alias to "node seed"

unpack tests and solution into HTML file; add titles and help text; style unpacked file

enable running unpacked assert tests in browser

Using browserify, compile "tape", "lodash", jQuery into "unpacked-bundle.js" for use during in-browser unpacked tests

feat(seed): diff after repacking

feat(seed): unpacked tests use Browser TAP chrome dev tool if available
This commit is contained in:
Alex Chaffee 2018-01-19 14:03:17 -05:00 committed by Mrugesh Mohapatra
parent 0c6141289b
commit a8484873d8
16 changed files with 1237 additions and 222 deletions

View File

@ -1 +1,2 @@
public/**/*.js
seed/unpacked/**/*.js

1
.gitignore vendored
View File

@ -35,6 +35,7 @@ coverage
server/*.bundle.js
public/js/bundle*
seed/unpacked
*.map

View File

@ -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.

533
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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` <br>&nbsp;&nbsp;(<small>or</small> `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",

View File

@ -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;

View File

@ -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;
});
};

24
seed/mongoIds.js Normal file
View File

@ -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;

View File

@ -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

76
seed/repack.js Normal file
View File

@ -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()
// });

View File

@ -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(`<!doctype html>
<html>
<body>
<div id="challenge-node"></div>
</body>
</html>
`);
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(`<!doctype html>
<html>
<body>
<div id="challenge-node"></div>
</body>
</html>
`);
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)
);

87
seed/unpack.js Normal file
View File

@ -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);
});

22
seed/unpacked.css Normal file
View File

@ -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;
}

18
seed/unpacked.js Normal file
View File

@ -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');
});

258
seed/unpackedChallenge.js Normal file
View File

@ -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 = /(<!|\/\*)--end--/;
let chunkStart = /(<!|\/\*)--(\w+)--/;
line = line.toString();
if (chunkEnd.test(line)) {
if (!readingChunk) {
throw 'Encountered --end-- without being in a chunk';
}
readingChunk = null;
} else if (chunkStart.test(line)) {
let chunkName = line.match(chunkStart)[ 2 ];
if (readingChunk) {
throw `Encountered chunk ${chunkName} start `
+ `while already reading ${readingChunk}:
${line}`;
}
readingChunk = chunkName;
} else if (readingChunk) {
if (line.startsWith(jsonLinePrefix)) {
line = JSON.parse(line.slice(jsonLinePrefix.length));
}
if (!chunks[readingChunk]) {
chunks[readingChunk] = [];
}
// don't push empty top lines
if (!(!line && chunks[readingChunk].length === 0)) {
chunks[ readingChunk ].push(line);
}
}
});
// hack to deal with solutions field being an array of a single string
// instead of an array of lines like other fields
if (chunks.solutions) {
chunks.solutions = [chunks.solutions.join('\n')];
}
// console.log(JSON.stringify(chunks, null, 2));
return chunks;
}
}
export {ChallengeFile};
class UnpackedChallenge {
constructor(targetDir, challengeJson, index) {
this.targetDir = targetDir;
this.index = index;
// todo: merge challengeJson properties into this object?
this.challenge = challengeJson;
// extract names of block and superblock from path
// note: this is a bit redundant with the
// fileName,superBlock,superOrder fields
// that getChallenges() adds to the challenge JSON
let targetDirPath = path.parse(targetDir);
let parentDirPath = path.parse(targetDirPath.dir);
// superBlockName e.g. "03-front-end-libraries"
this.superBlockName = parentDirPath.base;
// challengeBlockName e.g. "bootstrap"
this.challengeBlockName = targetDirPath.base;
}
unpack() {
this.challengeFile()
.write(this.unpackedHTML());
}
challengeFile() {
return new ChallengeFile(this.targetDir, this.baseName(), '.html');
}
baseName() {
// eslint-disable-next-line no-nested-ternary
let prefix = ((this.index < 10) ? '00' : (this.index < 100) ? '0' : '')
+ this.index;
return `${prefix}-${this.challenge.id}`;
}
expandedDescription(description) {
let out = [];
description.forEach(part => {
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('<html>');
text.push('<head>');
text.push('<link rel="stylesheet" href="../../../unpacked.css">');
text.push('<!-- shim to enable running the tests in-browser -->');
text.push('<script src="../../unpacked-bundle.js"></script>');
text.push('</head>');
text.push('<body>');
text.push(`<h1>${this.challenge.title}</h1>`);
text.push(`<p>This is the <b>unpacked</b> version of
<code>${this.superBlockName}/${this.challengeBlockName}</code>
(challenge id <code>${this.challenge.id}</code>).</p>`);
text.push('<p>Open the JavaScript console to see test results.</p>');
// text.push(`<p>Edit this HTML file (between &lt;!--s only!)
// and run <code>npm repack ???</code>
// to incorporate your changes into the challenge database.</p>`);
text.push('');
text.push('<h2>Description</h2>');
text.push('<div class="unpacked description">');
text.push('<!--description-->');
text.push(this.expandedDescription(this.challenge.description).join('\n'));
text.push('<!--end-->');
text.push('</div>');
text.push('');
text.push('<h2>Seed</h2>');
text.push('<!--seed--><pre class="unpacked">');
if (this.challenge.seed) {
text.push(text, this.challenge.seed.join('\n'));
}
text.push('<!--end-->');
text.push('</pre>');
// Q: What is the difference between 'seed' and 'challengeSeed' ?
text.push('');
text.push('<h2>Challenge Seed</h2>');
text.push('<!--challengeSeed--><pre class="unpacked">');
if (this.challenge.challengeSeed) {
text.push(text, this.challenge.challengeSeed.join('\n'));
}
text.push('<!--end-->');
text.push('</pre>');
text.push('');
text.push('<h2>Head</h2>');
text.push('<!--head--><script class="unpacked head">');
if (this.challenge.head) {
text.push(text, this.challenge.head.join('\n'));
}
text.push('</script><!--end-->');
text.push('');
text.push('<h2>Solution</h2>');
text.push(
'<!--solutions--><script class="unpacked solution" id="solution">'
);
// Note: none of the challenges have more than one solution
// todo: should we deal with multiple solutions or not?
if (this.challenge.solutions && this.challenge.solutions.length > 0) {
let solution = this.challenge.solutions[0];
text.push(solution);
}
text.push('</script><!--end-->');
text.push('');
text.push('<h2>Tail</h2>');
text.push('<!--tail--><script class="unpacked tail">');
if (this.challenge.tail) {
text.push(text, this.challenge.tail.join('\n'));
}
text.push('</script><!--end-->');
text.push('');
text.push('<h2>Tests</h2>');
text.push('<script class="unpacked tests">');
text.push(`test(\'${this.challenge.title} challenge tests\', ` +
'function(t) {');
text.push('let assert = addAssertsToTapTest(t);');
text.push('let code = document.getElementById(\'solution\').innerText;');
text.push('t.plan(' +
(this.challenge.tests ? this.challenge.tests.length : 0) +
');');
text.push('/*--tests--*/');
text.push(this.expandedTests(this.challenge.tests).join('\n'));
text.push('/*--end--*/');
text.push('});')
text.push('</script>');
text.push('');
text.push('</body>');
text.push('</html>');
return text;
}
}
export {UnpackedChallenge};