diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..c58216c2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,20 @@ +module.exports = { + parser: '@typescript-eslint/parser', // Specifies the ESLint parser + extends: [ + 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin + 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier + 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array + ], + parserOptions: { + ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features + sourceType: 'module', // Allows for the use of imports + }, + rules: { + // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-ignore': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/camelcase': 'off', + }, +}; diff --git a/.vscode/settings.json b/.vscode/settings.json index 58497395..7d44df88 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,41 +1,27 @@ { - // autofix - // "eslint.autoFixOnSave": true, - // "eslint.validate": [ - // "javascript", - // { - // "language": "typescript", - // "autoFix": true - // }, - // { - // "language": "typescriptreact", - // "autoFix": true - // } - // ], - "editor.codeActionsOnSave": { - "source.fixAll": true - }, - "tslint.enable": true, - "[javascript]": { - "editor.formatOnSave": true - }, - "[typescript]": { - "editor.formatOnSave": true - }, - // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - // "typescript.tsc.autoDetect": "off", - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results + "editor.formatOnSave": true, + "eslint.autoFixOnSave": true, + "eslint.validate": ["javascript", { "language": "typescript", "autoFix": true }], + "[javascript]": { + "editor.formatOnSave": false }, - - // styles - "editor.formatOnSave": true, - "workbench.colorCustomizations": { - "activityBar.background": "#000000", - "titleBar.activeBackground": "#000000", - "titleBar.activeForeground": "#FFFFFF" - } + "[typescript]": { + "editor.formatOnSave": false + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "files.exclude": { + "build": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "build": true // set this to false to include "out" folder in search results + }, + // styles + "workbench.colorCustomizations": { + "activityBar.background": "#000000", + "titleBar.activeBackground": "#000000", + "titleBar.activeForeground": "#FFFFFF" + }, + "cSpell.ignoreWords": [ + "camelcase" + ] } diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..f793d69c --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + preset: 'ts-jest', + verbose: true, + testPathIgnorePatterns: ['build'], +} diff --git a/package-lock.json b/package-lock.json index d7f71e03..5bfc2f91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "coderoad", - "version": "0.0.1", + "version": "0.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -24,6 +24,16 @@ "js-tokens": "^4.0.0" } }, + "@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + } + }, "@types/assert": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@types/assert/-/assert-1.4.3.tgz", @@ -39,6 +49,12 @@ "dotenv": "*" } }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", @@ -56,6 +72,36 @@ "@types/node": "*" } }, + "@types/istanbul-lib-coverage": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", + "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==" + }, + "@types/istanbul-lib-report": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz", + "integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==", + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", + "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "24.0.23", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.23.tgz", + "integrity": "sha512-L7MBvwfNpe7yVPTXLn32df/EK+AMBFAFvZrRuArGs7npEWnlziUXK+5GMIUTI4NIuwok3XibsjXCs5HxviYXjg==", + "requires": { + "jest-diff": "^24.3.0" + } + }, "@types/jsdom": { "version": "12.2.4", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-12.2.4.tgz", @@ -75,22 +121,22 @@ } } }, + "@types/json-schema": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", + "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "dev": true }, - "@types/mocha": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", - "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", - "dev": true - }, "@types/node": { - "version": "12.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.7.tgz", - "integrity": "sha512-E6Zn0rffhgd130zbCbAr/JdXfXkoOUFAKNs/rF8qnafSJ8KYaA/j3oz7dcwal+lYjLA7xvdd5J4wdYpCTlP8+w==", + "version": "12.12.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.8.tgz", + "integrity": "sha512-XLla8N+iyfjvsa0KKV+BP/iGSoTmwxsu5Ci5sM33z9TjohF72DEz95iNvD6pPmemvbQgxAv/909G73gUn8QR7w==", "dev": true }, "@types/tough-cookie": { @@ -99,6 +145,112 @@ "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==", "dev": true }, + "@types/yargs": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.3.tgz", + "integrity": "sha512-K8/LfZq2duW33XW/tFwEAfnZlqIfVsoyRB3kfXdPXYhl0nfM8mmh7GS0jg7WrX2Dgq/0Ha/pR1PaR+BvmWwjiQ==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-13.1.0.tgz", + "integrity": "sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg==" + }, + "@typescript-eslint/eslint-plugin": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.7.0.tgz", + "integrity": "sha512-H5G7yi0b0FgmqaEUpzyBlVh0d9lq4cWG2ap0RKa6BkF3rpBb6IrAoubt1NWh9R2kRs/f0k6XwRDiDz3X/FqXhQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.7.0", + "eslint-utils": "^1.4.2", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^2.0.1", + "tsutils": "^3.17.1" + }, + "dependencies": { + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.7.0.tgz", + "integrity": "sha512-9/L/OJh2a5G2ltgBWJpHRfGnt61AgDeH6rsdg59BH0naQseSwR7abwHq3D5/op0KYD/zFT4LS5gGvWcMmegTEg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.7.0", + "eslint-scope": "^5.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.7.0.tgz", + "integrity": "sha512-ctC0g0ZvYclxMh/xI+tyqP0EC2fAo6KicN9Wm2EIao+8OppLfxji7KAGJosQHSGBj3TcqUrA96AjgXuKa5ob2g==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.7.0", + "@typescript-eslint/typescript-estree": "2.7.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.7.0.tgz", + "integrity": "sha512-vVCE/DY72N4RiJ/2f10PTyYekX2OLaltuSIBqeHYI44GQ940VCYioInIb8jKMrK9u855OEJdFC+HmWAZTnC+Ag==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "glob": "^7.1.4", + "is-glob": "^4.0.1", + "lodash.unescape": "4.0.1", + "semver": "^6.3.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, "abab": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.2.tgz", @@ -125,6 +277,12 @@ } } }, + "acorn-jsx": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "dev": true + }, "acorn-walk": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", @@ -156,6 +314,15 @@ "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", "dev": true }, + "ansi-escapes": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz", + "integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==", + "dev": true, + "requires": { + "type-fest": "^0.5.2" + } + }, "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", @@ -166,7 +333,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -210,6 +376,12 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -265,16 +437,25 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, "camelcase": { @@ -292,13 +473,33 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, "cliui": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", @@ -320,7 +521,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -328,8 +528,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "combined-stream": { "version": "1.0.8", @@ -487,6 +686,20 @@ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, + "diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==" + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -587,8 +800,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "1.12.0", @@ -602,11 +814,172 @@ "source-map": "~0.6.1" } }, + "eslint": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.6.0.tgz", + "integrity": "sha512-PpEBq7b6qY/qrOmpYQ/jTMDYfuQMELR4g4WI1M/NaSDDD/bdcMb+dj4Hgks7p41kW2caXsPsEZAEAyAgjVVC0g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.5.0.tgz", + "integrity": "sha512-cjXp8SbO9VFGW/Z7mbTydqS9to8Z58E5aYhj3e1+Hx7lS9s6gL5ILKNpCqZAFOVYRcSkWPFYljHrEh8QFEK5EQ==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, + "eslint-plugin-prettier": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz", + "integrity": "sha512-A+TZuHZ0KU0cnn56/9mfR7/KjUJ9QNVXUhwvRFSR7PGPe0zQR6PTkmyqg1AtUUEOzTqeRsUwyKFh0oVZKVCrtA==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true + }, + "espree": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", + "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "dev": true, + "requires": { + "acorn": "^7.1.0", + "acorn-jsx": "^5.1.0", + "eslint-visitor-keys": "^1.1.0" + } + }, "esprima": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", @@ -637,6 +1010,17 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -647,6 +1031,12 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -657,6 +1047,24 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, + "figures": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", + "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -675,6 +1083,34 @@ "is-buffer": "~2.0.3" } }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "dev": true + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -702,12 +1138,24 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -739,6 +1187,21 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, "graphql": { "version": "14.5.8", "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.5.8.tgz", @@ -780,8 +1243,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.0", @@ -847,6 +1309,28 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -863,6 +1347,86 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "inquirer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz", + "integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^2.4.2", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^4.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + } + } + }, "invert-kv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", @@ -904,6 +1468,12 @@ "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", "dev": true }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -916,6 +1486,15 @@ "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==", "dev": true }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, "is-nan": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.2.1.tgz", @@ -925,6 +1504,12 @@ "define-properties": "^1.1.1" } }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -971,6 +1556,22 @@ "integrity": "sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==", "dev": true }, + "jest-diff": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", + "requires": { + "chalk": "^2.0.1", + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1049,11 +1650,34 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "json5": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", + "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "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 + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -1098,11 +1722,23 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", + "dev": true + }, "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -1112,6 +1748,12 @@ "chalk": "^2.0.1" } }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -1346,6 +1988,18 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -1460,6 +2114,15 @@ "wrappy": "1" } }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, "optionator": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", @@ -1484,6 +2147,12 @@ "mem": "^4.0.0" } }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -1526,6 +2195,15 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -1592,6 +2270,39 @@ "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "dev": true }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "requires": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + } + } + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "psl": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", @@ -1623,6 +2334,11 @@ "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", "dev": true }, + "react-is": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", + "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==" + }, "read-pkg": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", @@ -1634,6 +2350,12 @@ "pify": "^3.0.0" } }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, "request": { "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", @@ -1733,6 +2455,22 @@ "path-parse": "^1.0.6" } }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -1742,6 +2480,15 @@ "glob": "^7.1.3" } }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, "rxjs": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", @@ -1802,6 +2549,17 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -1937,7 +2695,6 @@ "version": "5.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -1947,6 +2704,67 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "tough-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", @@ -1971,56 +2789,47 @@ "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==", "dev": true }, - "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true - }, - "tslint": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", - "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", + "ts-jest": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-24.1.0.tgz", + "integrity": "sha512-HEGfrIEAZKfu1pkaxB9au17b1d9b56YZSqz5eCVE8mX68+5reOvlM93xGOzzCREIov9mdH7JBG+s0UyNAqr0tQ==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^4.0.1", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.8.0", - "tsutils": "^2.29.0" + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "mkdirp": "0.x", + "resolve": "1.x", + "semver": "^5.5", + "yargs-parser": "10.x" }, "dependencies": { - "diff": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", - "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "dev": true + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } } } }, - "tslint-config-prettier": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz", - "integrity": "sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==", + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, - "tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2042,6 +2851,12 @@ "prelude-ls": "~1.1.2" } }, + "type-fest": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", + "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==", + "dev": true + }, "typescript": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", @@ -2084,6 +2899,12 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" }, + "v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -2317,6 +3138,15 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, "ws": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", diff --git a/package.json b/package.json index e4a268f1..bcefaa67 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "build:web": "cd web-app && npm run build", "compile": "tsc -p ./", "postinstall": "node ./node_modules/vscode/bin/install", + "lint": "eslint src/**/*ts", "machine": "node ./out/state/index.js", "storybook": "cd web-app && npm run storybook", "test": "jest", @@ -30,6 +31,7 @@ "watch": "tsc -watch -p ./" }, "dependencies": { + "@types/jest": "^24.0.23", "jsdom": "^15.2.1" }, "devDependencies": { @@ -37,17 +39,20 @@ "@types/dotenv": "^8.2.0", "@types/glob": "^7.1.1", "@types/jsdom": "^12.2.4", - "@types/mocha": "^5.2.7", - "@types/node": "^12.12.7", + "@types/node": "^12.12.8", + "@typescript-eslint/eslint-plugin": "^2.7.0", + "@typescript-eslint/parser": "^2.7.0", "assert": "^2.0.0", "concurrently": "^5.0.0", "dotenv": "^8.2.0", + "eslint": "^6.6.0", + "eslint-config-prettier": "^6.5.0", + "eslint-plugin-prettier": "^3.1.1", "glob": "^7.1.6", "graphql": "^14.5.8", "mocha": "^6.2.2", "prettier": "^1.19.1", - "tslint": "^5.20.1", - "tslint-config-prettier": "^1.18.0", + "ts-jest": "^24.1.0", "typescript": "^3.7.2", "vscode": "^1.1.36", "vscode-test": "^1.2.3" diff --git a/src/actions/saveCommit.ts b/src/actions/saveCommit.ts index e442c703..146242e3 100644 --- a/src/actions/saveCommit.ts +++ b/src/actions/saveCommit.ts @@ -1,7 +1,7 @@ import * as git from '../services/git' async function saveCommit() { - git.saveCommit('Save progress') + git.saveCommit('Save progress') } export default saveCommit diff --git a/src/actions/setupActions.ts b/src/actions/setupActions.ts index 33aab5fa..760d6d7c 100644 --- a/src/actions/setupActions.ts +++ b/src/actions/setupActions.ts @@ -1,5 +1,5 @@ import * as G from 'typings/graphql' -import {join} from 'path' +import { join } from 'path' import * as vscode from 'vscode' import * as git from '../services/git' import node from '../services/node' @@ -17,127 +17,120 @@ import node from '../services/node' // } // } - // TODO: pass command and command name down for filtering. Eg. JAVASCRIPT, 'npm install' const runCommands = async (commands: string[]) => { - for (const command of commands) { - const {stdout, stderr} = await node.exec(command) - if (stderr) { - console.error(stderr) - // language specific error messages from running commands - // const filteredMessages = Object.keys(commandErrorMessageFilter[language]) - // for (const message of filteredMessages) { - // if (stderr.match(message)) { - // // ignored error - // throw new Error('Error running setup command') - // } - // } - } - console.log(`run command: ${command}`, stdout) - } + for (const command of commands) { + const { stdout, stderr } = await node.exec(command) + if (stderr) { + console.error(stderr) + // language specific error messages from running commands + // const filteredMessages = Object.keys(commandErrorMessageFilter[language]) + // for (const message of filteredMessages) { + // if (stderr.match(message)) { + // // ignored error + // throw new Error('Error running setup command') + // } + // } + } + console.log(`run command: ${command}`, stdout) + } } // collect active file watchers (listeners) -const watchers: {[key: string]: vscode.FileSystemWatcher} = {} +const watchers: { [key: string]: vscode.FileSystemWatcher } = {} const disposeWatcher = (listener: string) => { - watchers[listener].dispose() - delete watchers[listener] + watchers[listener].dispose() + delete watchers[listener] } const setupActions = async (workspaceRoot: vscode.WorkspaceFolder, actions: G.StepActions): Promise => { - const {commands, commits, files, listeners} = actions - // run commits - if (commits) { - for (const commit of commits) { - await git.loadCommit(commit) - } - } + const { commands, commits, files, listeners } = actions + // run commits + if (commits) { + for (const commit of commits) { + await git.loadCommit(commit) + } + } - // run file watchers (listeners) - if (listeners) { - console.log('listeners') - for (const listener of listeners) { - if (!watchers[listener]) { - const pattern = new vscode.RelativePattern( - vscode.workspace.getWorkspaceFolder(workspaceRoot.uri)!, - listener - ) - console.log(pattern) - const listen = vscode.workspace.createFileSystemWatcher( - pattern - ) - watchers[listener] = listen - watchers[listener].onDidChange(() => { - console.log('onDidChange') - // trigger save - vscode.commands.executeCommand('coderoad.run_test', null, () => { - // cleanup watcher on success - disposeWatcher(listener) - }) - }) - watchers[listener].onDidCreate(() => { - console.log('onDidCreate') - // trigger save - vscode.commands.executeCommand('coderoad.run_test', null, () => { - // cleanup watcher on success - disposeWatcher(listener) - }) - }) - watchers[listener].onDidDelete(() => { - console.log('onDidDelete') - // trigger save - vscode.commands.executeCommand('coderoad.run_test', null, () => { - // cleanup watcher on success - disposeWatcher(listener) - }) - }) - } - } - } else { - // remove all watchers - for (const listener of Object.keys(watchers)) { - disposeWatcher(listener) - } - } + // run file watchers (listeners) + if (listeners) { + console.log('listeners') + for (const listener of listeners) { + if (!watchers[listener]) { + const rootUri = vscode.workspace.getWorkspaceFolder(workspaceRoot.uri) + const pattern = new vscode.RelativePattern(rootUri!, listener) // eslint-disable-line + console.log(pattern) + const listen = vscode.workspace.createFileSystemWatcher(pattern) + watchers[listener] = listen + watchers[listener].onDidChange(() => { + console.log('onDidChange') + // trigger save + vscode.commands.executeCommand('coderoad.run_test', null, () => { + // cleanup watcher on success + disposeWatcher(listener) + }) + }) + watchers[listener].onDidCreate(() => { + console.log('onDidCreate') + // trigger save + vscode.commands.executeCommand('coderoad.run_test', null, () => { + // cleanup watcher on success + disposeWatcher(listener) + }) + }) + watchers[listener].onDidDelete(() => { + console.log('onDidDelete') + // trigger save + vscode.commands.executeCommand('coderoad.run_test', null, () => { + // cleanup watcher on success + disposeWatcher(listener) + }) + }) + } + } + } else { + // remove all watchers + for (const listener of Object.keys(watchers)) { + disposeWatcher(listener) + } + } - // run command - if (commands) { - await runCommands(commands) - } + // run command + if (commands) { + await runCommands(commands) + } - // open files - if (files) { - for (const filePath of files) { - try { - // TODO: figure out why this does not work - // try { - // const absoluteFilePath = join(workspaceRoot.uri.path, filePath) - // const doc = await vscode.workspace.openTextDocument(absoluteFilePath) - // await vscode.window.showTextDocument(doc, vscode.ViewColumn.One) - // // there are times when initialization leave the panel behind any files opened - // // ensure the panel is redrawn on the right side first - // // webview.createOrShow() - // } catch (error) { - // console.log(`Failed to open file ${filePath}`, error) - // } - const wr = vscode.workspace.rootPath - if (!wr) { - throw new Error('No workspace root path') - } - const absoluteFilePath = join(wr, filePath) - const doc = await vscode.workspace.openTextDocument(absoluteFilePath) - await vscode.window.showTextDocument(doc, vscode.ViewColumn.One) - // there are times when initialization leave the panel behind any files opened - // ensure the panel is redrawn on the right side first - vscode.commands.executeCommand('coderoad.open_webview') - } catch (error) { - console.log(`Failed to open file ${filePath}`, error) - } - } - } + // open files + if (files) { + for (const filePath of files) { + try { + // TODO: figure out why this does not work + // try { + // const absoluteFilePath = join(workspaceRoot.uri.path, filePath) + // const doc = await vscode.workspace.openTextDocument(absoluteFilePath) + // await vscode.window.showTextDocument(doc, vscode.ViewColumn.One) + // // there are times when initialization leave the panel behind any files opened + // // ensure the panel is redrawn on the right side first + // // webview.createOrShow() + // } catch (error) { + // console.log(`Failed to open file ${filePath}`, error) + // } + const wr = vscode.workspace.rootPath + if (!wr) { + throw new Error('No workspace root path') + } + const absoluteFilePath = join(wr, filePath) + const doc = await vscode.workspace.openTextDocument(absoluteFilePath) + await vscode.window.showTextDocument(doc, vscode.ViewColumn.One) + // there are times when initialization leave the panel behind any files opened + // ensure the panel is redrawn on the right side first + vscode.commands.executeCommand('coderoad.open_webview') + } catch (error) { + console.log(`Failed to open file ${filePath}`, error) + } + } + } } export default setupActions - - diff --git a/src/actions/solutionActions.ts b/src/actions/solutionActions.ts index 27f9e27a..28259873 100644 --- a/src/actions/solutionActions.ts +++ b/src/actions/solutionActions.ts @@ -4,8 +4,8 @@ import * as git from '../services/git' import setupActions from './setupActions' const solutionActions = async (workspaceRoot: vscode.WorkspaceFolder, stepActions: G.StepActions): Promise => { - await git.clear() - return setupActions(workspaceRoot, stepActions) + await git.clear() + return setupActions(workspaceRoot, stepActions) } export default solutionActions diff --git a/src/actions/tutorialConfig.ts b/src/actions/tutorialConfig.ts index 058cf93a..c0040d70 100644 --- a/src/actions/tutorialConfig.ts +++ b/src/actions/tutorialConfig.ts @@ -2,50 +2,49 @@ import * as G from 'typings/graphql' import * as vscode from 'vscode' import * as git from '../services/git' import languageMap from '../editor/languageMap' -import {COMMANDS} from '../editor/commands' +import { COMMANDS } from '../editor/commands' interface TutorialConfigParams { - config: G.TutorialConfig, - alreadyConfigured?: boolean - onComplete?(): void + config: G.TutorialConfig + alreadyConfigured?: boolean + onComplete?(): void } - -const tutorialConfig = async ({config, alreadyConfigured, }: TutorialConfigParams) => { - if (!alreadyConfigured) { - // setup git, add remote - await git.initIfNotExists() - - // TODO: if remote not already set - await git.setupRemote(config.repo.uri) - } - - vscode.commands.executeCommand(COMMANDS.CONFIG_TEST_RUNNER, config.testRunner) - - const fileFormats = config.testRunner.fileFormats - - // verify if file test should run based on document saved - const shouldRunTest = (document: vscode.TextDocument): boolean => { - // must be a file - if (document.uri.scheme !== 'file') { - return false - } - // must configure with file formatss - if (fileFormats && fileFormats.length) { - const fileFormat: G.FileFormat = languageMap[document.languageId] - if (!fileFormats.includes(fileFormat)) { - return false - } - } - return true - } - - // setup onSave hook - vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => { - if (shouldRunTest(document)) { - vscode.commands.executeCommand('coderoad.run_test') - } - }) +const tutorialConfig = async ({ config, alreadyConfigured }: TutorialConfigParams) => { + if (!alreadyConfigured) { + // setup git, add remote + await git.initIfNotExists() + + // TODO: if remote not already set + await git.setupRemote(config.repo.uri) + } + + vscode.commands.executeCommand(COMMANDS.CONFIG_TEST_RUNNER, config.testRunner) + + const fileFormats = config.testRunner.fileFormats + + // verify if file test should run based on document saved + const shouldRunTest = (document: vscode.TextDocument): boolean => { + // must be a file + if (document.uri.scheme !== 'file') { + return false + } + // must configure with file formatss + if (fileFormats && fileFormats.length) { + const fileFormat: G.FileFormat = languageMap[document.languageId] + if (!fileFormats.includes(fileFormat)) { + return false + } + } + return true + } + + // setup onSave hook + vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => { + if (shouldRunTest(document)) { + vscode.commands.executeCommand('coderoad.run_test') + } + }) } -export default tutorialConfig \ No newline at end of file +export default tutorialConfig diff --git a/src/channel/context.ts b/src/channel/context.ts index b18f4a26..36a7c2b7 100644 --- a/src/channel/context.ts +++ b/src/channel/context.ts @@ -7,27 +7,29 @@ import Progress from './state/Progress' import Tutorial from './state/Tutorial' class Context { - public tutorial: Tutorial - public position: Position - public progress: Progress - constructor(workspaceState: vscode.Memento) { - - // state held in one place - this.tutorial = new Tutorial(workspaceState) - this.position = new Position() - this.progress = new Progress() - } - public setTutorial = async (workspaceState: vscode.Memento, tutorial: G.Tutorial): Promise<{progress: CR.Progress, position: CR.Position}> => { - this.tutorial.set(tutorial) - const progress: CR.Progress = await this.progress.setTutorial(workspaceState, tutorial) - const position: CR.Position = this.position.setPositionFromProgress(tutorial, progress) - return {progress, position} - } - public reset = () => { - this.tutorial.reset() - this.progress.reset() - this.position.reset() - } + public tutorial: Tutorial + public position: Position + public progress: Progress + constructor(workspaceState: vscode.Memento) { + // state held in one place + this.tutorial = new Tutorial(workspaceState) + this.position = new Position() + this.progress = new Progress() + } + public setTutorial = async ( + workspaceState: vscode.Memento, + tutorial: G.Tutorial, + ): Promise<{ progress: CR.Progress; position: CR.Position }> => { + this.tutorial.set(tutorial) + const progress: CR.Progress = await this.progress.setTutorial(workspaceState, tutorial) + const position: CR.Position = this.position.setPositionFromProgress(tutorial, progress) + return { progress, position } + } + public reset = () => { + this.tutorial.reset() + this.progress.reset() + this.position.reset() + } } -export default Context \ No newline at end of file +export default Context diff --git a/src/channel/index.ts b/src/channel/index.ts index ecc33de6..c4addb03 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -8,156 +8,152 @@ import setupActions from '../actions/setupActions' import solutionActions from '../actions/solutionActions' import saveCommit from '../actions/saveCommit' - interface Channel { - receive(action: CR.Action): Promise - send(action: CR.Action): Promise + receive(action: CR.Action): Promise + send(action: CR.Action): Promise } interface ChannelProps { - postMessage: (action: CR.Action) => Thenable - workspaceState: vscode.Memento - workspaceRoot: vscode.WorkspaceFolder + postMessage: (action: CR.Action) => Thenable + workspaceState: vscode.Memento + workspaceRoot: vscode.WorkspaceFolder } - class Channel implements Channel { - private postMessage: (action: CR.Action) => Thenable - private workspaceState: vscode.Memento - private workspaceRoot: vscode.WorkspaceFolder - private context: Context - constructor({postMessage, workspaceState, workspaceRoot}: ChannelProps) { - // workspaceState used for local storage - this.workspaceState = workspaceState - this.workspaceRoot = workspaceRoot - this.postMessage = postMessage - this.context = new Context(workspaceState) - } - - // receive from webview - public receive = async (action: CR.Action) => { - // action may be an object.type or plain string - const actionType: string = typeof action === 'string' ? action : action.type - - // console.log('EDITOR RECEIVED:', actionType) - switch (actionType) { - case 'ENV_GET': - this.send({ - type: 'ENV_LOAD', - payload: { - env: { - machineId: vscode.env.machineId, - sessionId: vscode.env.sessionId, - } - } - }) - return - // continue from tutorial from local storage - case 'EDITOR_TUTORIAL_LOAD': - const tutorial: G.Tutorial | null = this.context.tutorial.get() - - // new tutorial - if (!tutorial || !tutorial.id || !tutorial.version) { - this.send({type: 'NEW_TUTORIAL'}) - return - } - - // set tutorial - const {position, progress} = await this.context.setTutorial(this.workspaceState, tutorial) - - if (progress.complete) { - // tutorial is already complete - this.send({type: 'NEW_TUTORIAL'}) - return - } - - // communicate to client the tutorial & stepProgress state - this.send({type: 'CONTINUE_TUTORIAL', payload: {tutorial, progress, position}}) - - return - // clear tutorial local storage - case 'TUTORIAL_CLEAR': - // clear current progress/position/tutorial - this.context.reset() - return - // configure test runner, language, git - case 'EDITOR_TUTORIAL_CONFIG': - const tutorialData: G.Tutorial = action.payload.tutorial - // setup tutorial config (save listener, test runner, etc) - this.context.setTutorial(this.workspaceState, tutorialData) - - const data: G.TutorialData = tutorialData.version.data - - await tutorialConfig({config: data.config}) - - // run init setup actions - if (data.init) { - const setup: G.StepActions | null | undefined = data.init.setup - if (setup) { - setupActions(this.workspaceRoot, setup) - } - } - - // report back to the webview that setup is complete - this.send({type: 'TUTORIAL_CONFIGURED'}) - return - case 'EDITOR_TUTORIAL_CONTINUE_CONFIG': - const tutorialContinue: G.Tutorial | null = this.context.tutorial.get() - if (!tutorialContinue) { - throw new Error('Invalid tutorial to continue') - } - const continueConfig: G.TutorialConfig = tutorialContinue.version.data.config - tutorialConfig({ - config: continueConfig, - alreadyConfigured: true - }) - return - case 'EDITOR_SYNC_PROGRESS': - // sync client progress on server - this.context.position.set(action.payload.position) - this.context.progress.set(action.payload.progress) - return - // load step actions (git commits, commands, open files) - case 'SETUP_ACTIONS': - vscode.commands.executeCommand('coderoad.set_current_step', action.payload) - setupActions(this.workspaceRoot, action.payload) - return - // load solution step actions (git commits, commands, open files) - case 'SOLUTION_ACTIONS': - await solutionActions(this.workspaceRoot, action.payload) - // run test following solution to update position - vscode.commands.executeCommand('coderoad.run_test', action.payload) - return - - default: - console.log(`No match for action type: ${actionType}`) - return - } - } - // send to webview - public send = async (action: CR.Action) => { - console.log(`EDITOR SEND ${action.type}`) - // action may be an object.type or plain string - const actionType: string = typeof action === 'string' ? action : action.type - switch (actionType) { - case 'TEST_PASS': - // update local storage stepProgress - const progress = this.context.progress.setStepComplete(action.payload.stepId) - const tutorial = this.context.tutorial.get() - if (!tutorial) { - throw new Error('Error with current tutorial') - } - this.context.position.setPositionFromProgress(tutorial, progress) - saveCommit() - } - - const success = await this.postMessage(action) - if (!success) { - throw new Error(`Message post failure: ${JSON.stringify(action)}`) - } - } - + private postMessage: (action: CR.Action) => Thenable + private workspaceState: vscode.Memento + private workspaceRoot: vscode.WorkspaceFolder + private context: Context + constructor({ postMessage, workspaceState, workspaceRoot }: ChannelProps) { + // workspaceState used for local storage + this.workspaceState = workspaceState + this.workspaceRoot = workspaceRoot + this.postMessage = postMessage + this.context = new Context(workspaceState) + } + + // receive from webview + public receive = async (action: CR.Action) => { + // action may be an object.type or plain string + const actionType: string = typeof action === 'string' ? action : action.type + + // console.log('EDITOR RECEIVED:', actionType) + switch (actionType) { + case 'ENV_GET': + this.send({ + type: 'ENV_LOAD', + payload: { + env: { + machineId: vscode.env.machineId, + sessionId: vscode.env.sessionId, + }, + }, + }) + return + // continue from tutorial from local storage + case 'EDITOR_TUTORIAL_LOAD': + const tutorial: G.Tutorial | null = this.context.tutorial.get() + + // new tutorial + if (!tutorial || !tutorial.id || !tutorial.version) { + this.send({ type: 'NEW_TUTORIAL' }) + return + } + + // set tutorial + const { position, progress } = await this.context.setTutorial(this.workspaceState, tutorial) + + if (progress.complete) { + // tutorial is already complete + this.send({ type: 'NEW_TUTORIAL' }) + return + } + + // communicate to client the tutorial & stepProgress state + this.send({ type: 'CONTINUE_TUTORIAL', payload: { tutorial, progress, position } }) + + return + // clear tutorial local storage + case 'TUTORIAL_CLEAR': + // clear current progress/position/tutorial + this.context.reset() + return + // configure test runner, language, git + case 'EDITOR_TUTORIAL_CONFIG': + const tutorialData: G.Tutorial = action.payload.tutorial + // setup tutorial config (save listener, test runner, etc) + this.context.setTutorial(this.workspaceState, tutorialData) + + const data: G.TutorialData = tutorialData.version.data + + await tutorialConfig({ config: data.config }) + + // run init setup actions + if (data.init) { + const setup: G.StepActions | null | undefined = data.init.setup + if (setup) { + setupActions(this.workspaceRoot, setup) + } + } + + // report back to the webview that setup is complete + this.send({ type: 'TUTORIAL_CONFIGURED' }) + return + case 'EDITOR_TUTORIAL_CONTINUE_CONFIG': + const tutorialContinue: G.Tutorial | null = this.context.tutorial.get() + if (!tutorialContinue) { + throw new Error('Invalid tutorial to continue') + } + const continueConfig: G.TutorialConfig = tutorialContinue.version.data.config + tutorialConfig({ + config: continueConfig, + alreadyConfigured: true, + }) + return + case 'EDITOR_SYNC_PROGRESS': + // sync client progress on server + this.context.position.set(action.payload.position) + this.context.progress.set(action.payload.progress) + return + // load step actions (git commits, commands, open files) + case 'SETUP_ACTIONS': + vscode.commands.executeCommand('coderoad.set_current_step', action.payload) + setupActions(this.workspaceRoot, action.payload) + return + // load solution step actions (git commits, commands, open files) + case 'SOLUTION_ACTIONS': + await solutionActions(this.workspaceRoot, action.payload) + // run test following solution to update position + vscode.commands.executeCommand('coderoad.run_test', action.payload) + return + + default: + console.log(`No match for action type: ${actionType}`) + return + } + } + // send to webview + public send = async (action: CR.Action) => { + console.log(`EDITOR SEND ${action.type}`) + // action may be an object.type or plain string + const actionType: string = typeof action === 'string' ? action : action.type + switch (actionType) { + case 'TEST_PASS': + // update local storage stepProgress + const progress = this.context.progress.setStepComplete(action.payload.stepId) + const tutorial = this.context.tutorial.get() + if (!tutorial) { + throw new Error('Error with current tutorial') + } + this.context.position.setPositionFromProgress(tutorial, progress) + saveCommit() + } + + const success = await this.postMessage(action) + if (!success) { + throw new Error(`Message post failure: ${JSON.stringify(action)}`) + } + } } export default Channel - diff --git a/src/channel/state/Position.ts b/src/channel/state/Position.ts index 6c774432..988241ae 100644 --- a/src/channel/state/Position.ts +++ b/src/channel/state/Position.ts @@ -2,63 +2,62 @@ import * as CR from 'typings' import * as G from 'typings/graphql' const defaultValue: CR.Position = { - levelId: '', - stepId: '', + levelId: '', + stepId: '', } // position class Position { - private value: CR.Position - constructor() { - this.value = defaultValue - } - public get = () => { - return this.value - } - public set = (value: CR.Position) => { - this.value = value - } - public reset = () => { - this.value = defaultValue - } - // calculate the current position based on the saved progress - public setPositionFromProgress = (tutorial: G.Tutorial, progress: CR.Progress): CR.Position => { - - // tutorial already completed - // TODO: handle start again? - if (progress.complete) { - return this.value - } - - if (!tutorial || !tutorial.version || !tutorial.version.data || !tutorial.version.data.levels) { - throw new Error('Error setting position from progress') - } - - const {levels} = tutorial.version.data - - const lastLevelIndex: number | undefined = levels.findIndex((l: G.Level) => !progress.levels[l.id]) - // TODO: consider all levels complete as progress.complete - if (lastLevelIndex >= levels.length) { - throw new Error('Error setting progress level') - } - const currentLevel: G.Level = levels[lastLevelIndex] - - const {steps} = currentLevel - - const lastStepIndex: number | undefined = steps.findIndex((s: G.Step) => !progress.steps[s.id]) - if (lastStepIndex >= steps.length) { - throw new Error('Error setting progress step') - } - // handle position when last step is complete but "continue" not yet selected - const adjustedLastStepIndex = lastStepIndex === -1 ? steps.length - 1 : lastStepIndex - const currentStep: G.Step = steps[adjustedLastStepIndex] - - this.value = { - levelId: currentLevel.id, - stepId: currentStep.id, - } - return this.value - } + private value: CR.Position + constructor() { + this.value = defaultValue + } + public get = () => { + return this.value + } + public set = (value: CR.Position) => { + this.value = value + } + public reset = () => { + this.value = defaultValue + } + // calculate the current position based on the saved progress + public setPositionFromProgress = (tutorial: G.Tutorial, progress: CR.Progress): CR.Position => { + // tutorial already completed + // TODO: handle start again? + if (progress.complete) { + return this.value + } + + if (!tutorial || !tutorial.version || !tutorial.version.data || !tutorial.version.data.levels) { + throw new Error('Error setting position from progress') + } + + const { levels } = tutorial.version.data + + const lastLevelIndex: number | undefined = levels.findIndex((l: G.Level) => !progress.levels[l.id]) + // TODO: consider all levels complete as progress.complete + if (lastLevelIndex >= levels.length) { + throw new Error('Error setting progress level') + } + const currentLevel: G.Level = levels[lastLevelIndex] + + const { steps } = currentLevel + + const lastStepIndex: number | undefined = steps.findIndex((s: G.Step) => !progress.steps[s.id]) + if (lastStepIndex >= steps.length) { + throw new Error('Error setting progress step') + } + // handle position when last step is complete but "continue" not yet selected + const adjustedLastStepIndex = lastStepIndex === -1 ? steps.length - 1 : lastStepIndex + const currentStep: G.Step = steps[adjustedLastStepIndex] + + this.value = { + levelId: currentLevel.id, + stepId: currentStep.id, + } + return this.value + } } -export default Position \ No newline at end of file +export default Position diff --git a/src/channel/state/Progress.ts b/src/channel/state/Progress.ts index 99799f8f..311e8a60 100644 --- a/src/channel/state/Progress.ts +++ b/src/channel/state/Progress.ts @@ -5,46 +5,46 @@ import * as vscode from 'vscode' import Storage from '../../services/storage' const defaultValue: CR.Progress = { - levels: {}, - steps: {}, - complete: false + levels: {}, + steps: {}, + complete: false, } // hold current progress and sync to storage based on tutorial.id/version class Progress { - private value: CR.Progress - private storage: Storage | undefined - constructor() { - this.value = defaultValue - } - public setTutorial = async (workspaceState: vscode.Memento, tutorial: G.Tutorial): Promise => { - this.storage = new Storage({ - key: `coderoad:progress:${tutorial.id}:${tutorial.version}`, - storage: workspaceState, - defaultValue, - })// set value from storage - this.value = await this.storage.get() || defaultValue - return this.value - } - public get = () => { - return this.value - } - public set = (value: CR.Progress) => { - this.value = value - if (!this.storage) { - return defaultValue - } - this.storage.set(value) - return this.value - } - public reset = () => { - this.set(defaultValue) - } - public setStepComplete = (stepId: string): CR.Progress => { - const next = this.value - next.steps[stepId] = true - return this.set(next) - } + private value: CR.Progress + private storage: Storage | undefined + constructor() { + this.value = defaultValue + } + public setTutorial = async (workspaceState: vscode.Memento, tutorial: G.Tutorial): Promise => { + this.storage = new Storage({ + key: `coderoad:progress:${tutorial.id}:${tutorial.version}`, + storage: workspaceState, + defaultValue, + }) // set value from storage + this.value = (await this.storage.get()) || defaultValue + return this.value + } + public get = () => { + return this.value + } + public set = (value: CR.Progress) => { + this.value = value + if (!this.storage) { + return defaultValue + } + this.storage.set(value) + return this.value + } + public reset = () => { + this.set(defaultValue) + } + public setStepComplete = (stepId: string): CR.Progress => { + const next = this.value + next.steps[stepId] = true + return this.set(next) + } } -export default Progress \ No newline at end of file +export default Progress diff --git a/src/channel/state/Tutorial.ts b/src/channel/state/Tutorial.ts index 4d54ab97..d6efac1d 100644 --- a/src/channel/state/Tutorial.ts +++ b/src/channel/state/Tutorial.ts @@ -5,28 +5,28 @@ import Storage from '../../services/storage' // Tutorial class Tutorial { - private storage: Storage - private value: G.Tutorial | null - constructor(workspaceState: vscode.Memento) { - this.storage = new Storage({ - key: 'coderoad:currentTutorial', - storage: workspaceState, - defaultValue: null - }) - this.value = null - // set value from storage - this.storage.get().then((value: G.Tutorial | null) => { - this.value = value - }) - } - public get = () => this.value - public set = (value: G.Tutorial | null) => { - this.value = value - this.storage.set(value) - } - public reset = () => { - this.set(null) - } + private storage: Storage + private value: G.Tutorial | null + constructor(workspaceState: vscode.Memento) { + this.storage = new Storage({ + key: 'coderoad:currentTutorial', + storage: workspaceState, + defaultValue: null, + }) + this.value = null + // set value from storage + this.storage.get().then((value: G.Tutorial | null) => { + this.value = value + }) + } + public get = () => this.value + public set = (value: G.Tutorial | null) => { + this.value = value + this.storage.set(value) + } + public reset = () => { + this.set(null) + } } -export default Tutorial \ No newline at end of file +export default Tutorial diff --git a/src/editor/ReactWebView.ts b/src/editor/ReactWebView.ts index ae49424f..f54cdb8e 100644 --- a/src/editor/ReactWebView.ts +++ b/src/editor/ReactWebView.ts @@ -1,184 +1,189 @@ -import {Action} from 'typings' +import { Action } from 'typings' import * as path from 'path' import * as vscode from 'vscode' -import {JSDOM} from 'jsdom' +import { JSDOM } from 'jsdom' import Channel from '../channel' const getNonce = (): string => { - let text = '' - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)) - } - return text + let text = '' + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)) + } + return text } interface ReactWebViewProps { - extensionPath: string - workspaceState: vscode.Memento - workspaceRoot: vscode.WorkspaceFolder + extensionPath: string + workspaceState: vscode.Memento + workspaceRoot: vscode.WorkspaceFolder } - // Manages webview panel class ReactWebView { - // @ts-ignore - public loaded: boolean - - public send: Channel['send'] - private panel: vscode.WebviewPanel - private extensionPath: string - private disposables: vscode.Disposable[] = [] - private channel: Channel - - public constructor({extensionPath, workspaceState, workspaceRoot}: ReactWebViewProps) { - this.extensionPath = extensionPath - - // Create and show a new webview panel - this.panel = this.createWebviewPanel() - - // Set the webview initial html content - this.render() - - // Listen for when the panel is disposed - // This happens when the user closes the panel or when the panel is closed programmatically - this.panel.onDidDispose(this.dispose, this, this.disposables) - - - // update panel on changes - // const updateWindows = () => { - // vscode.commands.executeCommand('coderoad.open_webview') - // } - - // // // prevents moving coderoad panel on top of left panel - // vscode.window.onDidChangeVisibleTextEditors((textEditors: vscode.TextEditor[]) => { - // // updateWindows() - // }) - - // TODO: prevent window from moving to the left when no windows remain on rights - - // channel connects webview to the editor - this.channel = new Channel({ - workspaceState, - workspaceRoot, - postMessage: (action: Action): Thenable => { - // console.log(`postMessage ${JSON.stringify(action)}`) - return this.panel.webview.postMessage(action) - } - }) - // Handle messages from the webview - const receive = this.channel.receive - this.panel.webview.onDidReceiveMessage(receive, null, this.disposables) - this.send = this.channel.send - } - - public createOrShow(): void { - vscode.commands.executeCommand('vscode.setEditorLayout', { - orientation: 0, - groups: [{groups: [{}], size: 0.6}, {groups: [{}], size: 0.4}], - }) - // If we already have a panel, show it. - // Otherwise, create a new panel. - - if (this.panel && this.panel.webview) { - if (!this.loaded) { - this.panel.reveal(vscode.ViewColumn.Two) - this.loaded = true - } - } else { - this.panel = this.createWebviewPanel() - } - } - - private async dispose(): Promise { - // Clean up our resources - this.loaded = false - this.panel.dispose() - Promise.all(this.disposables.map((x) => x.dispose())) - } - - private createWebviewPanel = (): vscode.WebviewPanel => { - const viewType = 'CodeRoad' - const title = 'CodeRoad' - const config = { - // Enable javascript in the webview - enableScripts: true, - // And restrict the webview to only loading content from our extension's `media` directory. - localResourceRoots: [vscode.Uri.file(path.join(this.extensionPath, 'build'))], - // prevents destroying the window when it is in the background - retainContextWhenHidden: true, - } - this.loaded = true - return vscode.window.createWebviewPanel(viewType, title, vscode.ViewColumn.Two, config) - } - - private render = async (): Promise => { - // path to build directory - const rootPath = path.join(this.extensionPath, 'build') - - // load copied index.html from web app build - const dom = await JSDOM.fromFile(path.join(rootPath, 'index.html')) - const {document} = dom.window - - // set base href - const base: HTMLBaseElement = document.createElement('base') - base.href = vscode.Uri.file(rootPath).with({scheme: 'vscode-resource'}).toString() + '/' - document.head.appendChild(base) - - // used for CSP - const nonces: string[] = [] - - // generate vscode-resource build path uri - const createUri = (filePath: string): string => - vscode.Uri.file(filePath).with({scheme: 'vscode-resource'}).toString() - .replace(/^\/+/g, '') // remove leading '/' - .replace('/vscode-resource%3A', rootPath) // replace mangled resource path with root - - // fix paths for scripts - const scripts: HTMLScriptElement[] = Array.from(document.getElementsByTagName('script')) - for (const script of scripts) { - if (script.src) { - const nonce: string = getNonce() - nonces.push(nonce) - script.nonce = nonce - script.src = createUri(script.src) - } - } - - // add run-time script from webpack - const runTimeScript = document.createElement('script') - runTimeScript.nonce = getNonce() - nonces.push(runTimeScript.nonce) - const manifest = require(path.join(rootPath, 'asset-manifest.json')) - runTimeScript.src = createUri(path.join(rootPath, manifest.files['runtime-main.js'])) - document.body.appendChild(runTimeScript) - - // fix paths for links - const styles: HTMLLinkElement[] = Array.from(document.getElementsByTagName('link')) - for (const style of styles) { - if (style.href) { - style.href = createUri(style.href) - } - } - - // set CSP (content security policy) to grant permission to local files - const cspMeta: HTMLMetaElement = document.createElement('meta') - cspMeta.httpEquiv = 'Content-Security-Policy' - cspMeta.content = [ - 'font-src vscode-resource://*;', - 'img-src vscode-resource: https:;', - `script-src ${nonces.map(nonce => `'nonce-${nonce}'`).join(' ')};`, - `style-src 'unsafe-inline' vscode-resource: http: https: data:;` - ].join(' ') - document.head.appendChild(cspMeta) - - // stringify dom - const html = dom.serialize() - - // set view - this.panel.webview.html = html - } - + // @ts-ignore + public loaded: boolean + + public send: Channel['send'] + private panel: vscode.WebviewPanel + private extensionPath: string + private disposables: vscode.Disposable[] = [] + private channel: Channel + + public constructor({ extensionPath, workspaceState, workspaceRoot }: ReactWebViewProps) { + this.extensionPath = extensionPath + + // Create and show a new webview panel + this.panel = this.createWebviewPanel() + + // Set the webview initial html content + this.render() + + // Listen for when the panel is disposed + // This happens when the user closes the panel or when the panel is closed programmatically + this.panel.onDidDispose(this.dispose, this, this.disposables) + + // update panel on changes + // const updateWindows = () => { + // vscode.commands.executeCommand('coderoad.open_webview') + // } + + // // // prevents moving coderoad panel on top of left panel + // vscode.window.onDidChangeVisibleTextEditors((textEditors: vscode.TextEditor[]) => { + // // updateWindows() + // }) + + // TODO: prevent window from moving to the left when no windows remain on rights + + // channel connects webview to the editor + this.channel = new Channel({ + workspaceState, + workspaceRoot, + postMessage: (action: Action): Thenable => { + // console.log(`postMessage ${JSON.stringify(action)}`) + return this.panel.webview.postMessage(action) + }, + }) + // Handle messages from the webview + const receive = this.channel.receive + this.panel.webview.onDidReceiveMessage(receive, null, this.disposables) + this.send = this.channel.send + } + + public createOrShow(): void { + vscode.commands.executeCommand('vscode.setEditorLayout', { + orientation: 0, + groups: [ + { groups: [{}], size: 0.6 }, + { groups: [{}], size: 0.4 }, + ], + }) + // If we already have a panel, show it. + // Otherwise, create a new panel. + + if (this.panel && this.panel.webview) { + if (!this.loaded) { + this.panel.reveal(vscode.ViewColumn.Two) + this.loaded = true + } + } else { + this.panel = this.createWebviewPanel() + } + } + + private async dispose(): Promise { + // Clean up our resources + this.loaded = false + this.panel.dispose() + Promise.all(this.disposables.map(x => x.dispose())) + } + + private createWebviewPanel = (): vscode.WebviewPanel => { + const viewType = 'CodeRoad' + const title = 'CodeRoad' + const config = { + // Enable javascript in the webview + enableScripts: true, + // And restrict the webview to only loading content from our extension's `media` directory. + localResourceRoots: [vscode.Uri.file(path.join(this.extensionPath, 'build'))], + // prevents destroying the window when it is in the background + retainContextWhenHidden: true, + } + this.loaded = true + return vscode.window.createWebviewPanel(viewType, title, vscode.ViewColumn.Two, config) + } + + private render = async (): Promise => { + // path to build directory + const rootPath = path.join(this.extensionPath, 'build') + + // load copied index.html from web app build + const dom = await JSDOM.fromFile(path.join(rootPath, 'index.html')) + const { document } = dom.window + + // set base href + const base: HTMLBaseElement = document.createElement('base') + base.href = + vscode.Uri.file(rootPath) + .with({ scheme: 'vscode-resource' }) + .toString() + '/' + document.head.appendChild(base) + + // used for CSP + const nonces: string[] = [] + + // generate vscode-resource build path uri + const createUri = (filePath: string): string => + vscode.Uri.file(filePath) + .with({ scheme: 'vscode-resource' }) + .toString() + .replace(/^\/+/g, '') // remove leading '/' + .replace('/vscode-resource%3A', rootPath) // replace mangled resource path with root + + // fix paths for scripts + const scripts: HTMLScriptElement[] = Array.from(document.getElementsByTagName('script')) + for (const script of scripts) { + if (script.src) { + const nonce: string = getNonce() + nonces.push(nonce) + script.nonce = nonce + script.src = createUri(script.src) + } + } + + // add run-time script from webpack + const runTimeScript = document.createElement('script') + runTimeScript.nonce = getNonce() + nonces.push(runTimeScript.nonce) + const manifest = await import(path.join(rootPath, 'asset-manifest.json')) + runTimeScript.src = createUri(path.join(rootPath, manifest.files['runtime-main.js'])) + document.body.appendChild(runTimeScript) + + // fix paths for links + const styles: HTMLLinkElement[] = Array.from(document.getElementsByTagName('link')) + for (const style of styles) { + if (style.href) { + style.href = createUri(style.href) + } + } + + // set CSP (content security policy) to grant permission to local files + const cspMeta: HTMLMetaElement = document.createElement('meta') + cspMeta.httpEquiv = 'Content-Security-Policy' + cspMeta.content = [ + 'font-src vscode-resource://*;', + 'img-src vscode-resource: https:;', + `script-src ${nonces.map(nonce => `'nonce-${nonce}'`).join(' ')};`, + `style-src 'unsafe-inline' vscode-resource: http: https: data:;`, + ].join(' ') + document.head.appendChild(cspMeta) + + // stringify dom + const html = dom.serialize() + + // set view + this.panel.webview.html = html + } } export default ReactWebView diff --git a/src/editor/commands.ts b/src/editor/commands.ts index 1b157364..23860741 100644 --- a/src/editor/commands.ts +++ b/src/editor/commands.ts @@ -1,88 +1,88 @@ import * as G from 'typings/graphql' import * as vscode from 'vscode' import ReactWebView from './ReactWebView' -import createTestRunner, {Payload} from '../services/testRunner' +import createTestRunner, { Payload } from '../services/testRunner' export const COMMANDS = { - START: 'coderoad.start', - OPEN_WEBVIEW: 'coderoad.open_webview', - CONFIG_TEST_RUNNER: 'coderoad.config_test_runner', - RUN_TEST: 'coderoad.run_test', - SET_CURRENT_STEP: 'coderoad.set_current_step', + START: 'coderoad.start', + OPEN_WEBVIEW: 'coderoad.open_webview', + CONFIG_TEST_RUNNER: 'coderoad.config_test_runner', + RUN_TEST: 'coderoad.run_test', + SET_CURRENT_STEP: 'coderoad.set_current_step', } interface CreateCommandProps { - extensionPath: string - workspaceState: vscode.Memento - workspaceRoot: vscode.WorkspaceFolder + extensionPath: string + workspaceState: vscode.Memento + workspaceRoot: vscode.WorkspaceFolder } -export const createCommands = ({extensionPath, workspaceState, workspaceRoot}: CreateCommandProps) => { - // React panel webview - let webview: any - let currentStepId = '' - let testRunner: any +export const createCommands = ({ extensionPath, workspaceState, workspaceRoot }: CreateCommandProps) => { + // React panel webview + let webview: any + let currentStepId = '' + let testRunner: any - return { - // initialize - [COMMANDS.START]: async () => { - // TODO: replace with a prompt to open a workspace - // await isEmptyWorkspace() + return { + // initialize + [COMMANDS.START]: async () => { + // TODO: replace with a prompt to open a workspace + // await isEmptyWorkspace() - let webviewState: 'INITIALIZING' | 'RESTARTING' - if (!webview) { - webviewState = 'INITIALIZING' - } else if (webview.loaded) { - // already loaded - vscode.window.showInformationMessage('CodeRoad already open') - return - } else { - webviewState = 'RESTARTING' - } + let webviewState: 'INITIALIZING' | 'RESTARTING' + if (!webview) { + webviewState = 'INITIALIZING' + } else if (webview.loaded) { + // already loaded + vscode.window.showInformationMessage('CodeRoad already open') + return + } else { + webviewState = 'RESTARTING' + } - // activate machine - webview = new ReactWebView({ - extensionPath, - workspaceState, - workspaceRoot, - }) - }, - // open React webview - [COMMANDS.OPEN_WEBVIEW]: () => { - // setup 1x1 horizontal layout - webview.createOrShow() - }, - [COMMANDS.CONFIG_TEST_RUNNER]: (config: G.TutorialTestRunner) => { - testRunner = createTestRunner(config, { - onSuccess: (payload: Payload) => { - // send test pass message back to client - vscode.window.showInformationMessage('PASS') - webview.send({type: 'TEST_PASS', payload}) - }, - onFail: (payload: Payload) => { - // send test fail message back to client - vscode.window.showWarningMessage('FAIL') - webview.send({type: 'TEST_FAIL', payload}) - }, - onError: (payload: Payload) => { - // send test error message back to client - webview.send({type: 'TEST_ERROR', payload}) - }, - onRun: (payload: Payload) => { - // send test run message back to client - webview.send({type: 'TEST_RUNNING', payload}) - } - }) - }, - [COMMANDS.SET_CURRENT_STEP]: ({stepId}: Payload) => { - // NOTE: as async, may sometimes be inaccurate - // set from last setup stepAction - currentStepId = stepId - }, - [COMMANDS.RUN_TEST]: (current: Payload | undefined, onSuccess: () => void) => { - // use stepId from client, or last set stepId - const payload: Payload = {stepId: current ? current.stepId : currentStepId} - testRunner(payload, onSuccess) - }, - } + // activate machine + webview = new ReactWebView({ + extensionPath, + workspaceState, + workspaceRoot, + }) + }, + // open React webview + [COMMANDS.OPEN_WEBVIEW]: () => { + // setup 1x1 horizontal layout + webview.createOrShow() + }, + [COMMANDS.CONFIG_TEST_RUNNER]: (config: G.TutorialTestRunner) => { + testRunner = createTestRunner(config, { + onSuccess: (payload: Payload) => { + // send test pass message back to client + vscode.window.showInformationMessage('PASS') + webview.send({ type: 'TEST_PASS', payload }) + }, + onFail: (payload: Payload, message: string) => { + // send test fail message back to client + vscode.window.showWarningMessage(`FAIL: ${message}`) + webview.send({ type: 'TEST_FAIL', payload }) + }, + onError: (payload: Payload) => { + // send test error message back to client + webview.send({ type: 'TEST_ERROR', payload }) + }, + onRun: (payload: Payload) => { + // send test run message back to client + webview.send({ type: 'TEST_RUNNING', payload }) + }, + }) + }, + [COMMANDS.SET_CURRENT_STEP]: ({ stepId }: Payload) => { + // NOTE: as async, may sometimes be inaccurate + // set from last setup stepAction + currentStepId = stepId + }, + [COMMANDS.RUN_TEST]: (current: Payload | undefined, onSuccess: () => void) => { + // use stepId from client, or last set stepId + const payload: Payload = { stepId: current ? current.stepId : currentStepId } + testRunner(payload, onSuccess) + }, + } } diff --git a/src/editor/index.ts b/src/editor/index.ts index 7d6a48b1..4a42f3e4 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -1,54 +1,56 @@ import * as vscode from 'vscode' -import {createCommands} from './commands' +import { createCommands } from './commands' class Editor { - // extension context set on activation - // @ts-ignore - private vscodeExt: vscode.ExtensionContext - - public activate = (vscodeExt: vscode.ExtensionContext): void => { - - this.vscodeExt = vscodeExt - - // set out 60/40 layout - vscode.commands.executeCommand('vscode.setEditorLayout', { - orientation: 0, - groups: [{groups: [{}], size: 0.6}, {groups: [{}], size: 0.4}], - }) - - // commands - this.activateCommands() - - // setup tasks or views here - } - public deactivate = (): void => { - // cleanup subscriptions/tasks - for (const disposable of this.vscodeExt.subscriptions) { - disposable.dispose() - } - } - - private activateCommands = (): void => { - // set workspace root for node executions - const workspaceRoots: vscode.WorkspaceFolder[] | undefined = vscode.workspace.workspaceFolders - if (!workspaceRoots || !workspaceRoots.length) { - throw new Error('No workspace root path') - } - const workspaceRoot: vscode.WorkspaceFolder = workspaceRoots[0] - - const commands = createCommands({ - extensionPath: this.vscodeExt.extensionPath, - // NOTE: local storage must be bound to the vscodeExt.workspaceState - workspaceState: this.vscodeExt.workspaceState, - workspaceRoot, - }) - - // register commands - for (const cmd in commands) { - const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd]) - this.vscodeExt.subscriptions.push(command) - } - } + // extension context set on activation + // @ts-ignore + private vscodeExt: vscode.ExtensionContext + + public activate = (vscodeExt: vscode.ExtensionContext): void => { + this.vscodeExt = vscodeExt + + // set out 60/40 layout + vscode.commands.executeCommand('vscode.setEditorLayout', { + orientation: 0, + groups: [ + { groups: [{}], size: 0.6 }, + { groups: [{}], size: 0.4 }, + ], + }) + + // commands + this.activateCommands() + + // setup tasks or views here + } + public deactivate = (): void => { + // cleanup subscriptions/tasks + for (const disposable of this.vscodeExt.subscriptions) { + disposable.dispose() + } + } + + private activateCommands = (): void => { + // set workspace root for node executions + const workspaceRoots: vscode.WorkspaceFolder[] | undefined = vscode.workspace.workspaceFolders + if (!workspaceRoots || !workspaceRoots.length) { + throw new Error('No workspace root path') + } + const workspaceRoot: vscode.WorkspaceFolder = workspaceRoots[0] + + const commands = createCommands({ + extensionPath: this.vscodeExt.extensionPath, + // NOTE: local storage must be bound to the vscodeExt.workspaceState + workspaceState: this.vscodeExt.workspaceState, + workspaceRoot, + }) + + // register commands + for (const cmd in commands) { + const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd]) + this.vscodeExt.subscriptions.push(command) + } + } } export default Editor diff --git a/src/editor/languageMap.ts b/src/editor/languageMap.ts index def7cc6f..40100b97 100644 --- a/src/editor/languageMap.ts +++ b/src/editor/languageMap.ts @@ -1,23 +1,23 @@ import * as G from 'typings/graphql' // sourced from https://code.visualstudio.com/docs/languages/identifiers const languageMap: { - [lang: string]: G.FileFormat + [lang: string]: G.FileFormat } = { - // go: 'GO', - javascript: 'JS', - javascriptreact: 'JSX', - json: 'JSON', - // less: 'LESS', - // lua: 'LUA', - // php: 'PHP', - // python: 'PY', - // ruby: 'RB', - // sass: 'SASS', - // scss: 'SCSS', - // sql: 'SQL', - typescript: 'TS', - typescriptreact: 'TSX', - // yaml: 'YAML' + // go: 'GO', + javascript: 'JS', + javascriptreact: 'JSX', + json: 'JSON', + // less: 'LESS', + // lua: 'LUA', + // php: 'PHP', + // python: 'PY', + // ruby: 'RB', + // sass: 'SASS', + // scss: 'SCSS', + // sql: 'SQL', + typescript: 'TS', + typescriptreact: 'TSX', + // yaml: 'YAML' } -export default languageMap \ No newline at end of file +export default languageMap diff --git a/src/editor/outputChannel.ts b/src/editor/outputChannel.ts index a2eeb224..e43dbb25 100644 --- a/src/editor/outputChannel.ts +++ b/src/editor/outputChannel.ts @@ -3,8 +3,8 @@ import * as vscode from 'vscode' let channel: vscode.OutputChannel export const getOutputChannel = (name: string): vscode.OutputChannel => { - if (!channel) { - channel = vscode.window.createOutputChannel(name) - } - return channel -} \ No newline at end of file + if (!channel) { + channel = vscode.window.createOutputChannel(name) + } + return channel +} diff --git a/src/services/git/gitAction.ts b/src/services/git/gitAction.ts index f6bd6ce9..f33b459c 100644 --- a/src/services/git/gitAction.ts +++ b/src/services/git/gitAction.ts @@ -1,17 +1,17 @@ import node from '../node' interface GitAction { - command: string - onError?(stderr: string): any - onSuccess?(stdout: string): any + command: string + onError?(stderr: string): any + onSuccess?(stdout: string): any } // handle common node.exec logic -export const gitAction = async ({command, onError, onSuccess}: GitAction) => { - const {stdout, stderr} = await node.exec(command) - if (onError && stderr) { - return onError(stderr) - } else if (onSuccess && stdout) { - return onSuccess(stdout) - } -} \ No newline at end of file +export const gitAction = async ({ command, onError, onSuccess }: GitAction) => { + const { stdout, stderr } = await node.exec(command) + if (onError && stderr) { + return onError(stderr) + } else if (onSuccess && stdout) { + return onSuccess(stdout) + } +} diff --git a/src/services/git/index.ts b/src/services/git/index.ts index fc05ee0e..d5106a17 100644 --- a/src/services/git/index.ts +++ b/src/services/git/index.ts @@ -1,43 +1,40 @@ import node from '../node' - const gitOrigin = 'coderoad' const stashAllFiles = async () => { - // stash files including untracked (eg. newly created file) - const {stdout, stderr} = await node.exec(`git stash --include-untracked`) - if (stderr) { - console.error(stderr) - throw new Error('Error stashing files') - } + // stash files including untracked (eg. newly created file) + const { stdout, stderr } = await node.exec(`git stash --include-untracked`) + if (stderr) { + console.error(stderr) + throw new Error('Error stashing files') + } } const cherryPickCommit = async (commit: string, count = 0): Promise => { - if (count > 1) { - console.warn('cherry-pick failed') - return - } - try { - const {stdout} = await node.exec(`git cherry-pick ${commit}`) - if (!stdout) { - throw new Error('No cherry-pick output') - } - } catch (error) { - console.log('cherry-pick-commit failed') - // stash all files if cherry-pick fails - await stashAllFiles() - return cherryPickCommit(commit, ++count) - } + if (count > 1) { + console.warn('cherry-pick failed') + return + } + try { + const { stdout } = await node.exec(`git cherry-pick ${commit}`) + if (!stdout) { + throw new Error('No cherry-pick output') + } + } catch (error) { + console.log('cherry-pick-commit failed') + // stash all files if cherry-pick fails + await stashAllFiles() + return cherryPickCommit(commit, ++count) + } } - - /* SINGLE git cherry-pick %COMMIT% if fails, will stash all and retry */ export function loadCommit(commit: string): Promise { - return cherryPickCommit(commit) + return cherryPickCommit(commit) } /* @@ -46,95 +43,95 @@ export function loadCommit(commit: string): Promise { */ export async function saveCommit(message: string): Promise { - const {stdout, stderr} = await node.exec(`git commit -am '${message}'`) - if (stderr) { - console.error(stderr) - throw new Error('Error saving progress to Git') - } - console.log('save with commit & continue stdout', stdout) + const { stdout, stderr } = await node.exec(`git commit -am '${message}'`) + if (stderr) { + console.error(stderr) + throw new Error('Error saving progress to Git') + } + console.log('save with commit & continue stdout', stdout) } export async function clear(): Promise { - try { - // commit progress to git - const {stderr} = await node.exec('git reset HEAD --hard && git clean -fd') - if (!stderr) { - return - } - console.error(stderr) - } catch (error) { - console.error(error) - } - throw new Error('Error cleaning up current unsaved work') + try { + // commit progress to git + const { stderr } = await node.exec('git reset HEAD --hard && git clean -fd') + if (!stderr) { + return + } + console.error(stderr) + } catch (error) { + console.error(error) + } + throw new Error('Error cleaning up current unsaved work') } export async function version(): Promise { - const {stdout, stderr} = await node.exec('git --version') - if (!stderr) { - const match = stdout.match(/^git version (\d+\.)?(\d+\.)?(\*|\d+)/) - if (match) { - // eslint-disable-next-line + const { stdout, stderr } = await node.exec('git --version') + if (!stderr) { + const match = stdout.match(/^git version (\d+\.)?(\d+\.)?(\*|\d+)/) + if (match) { + // eslint-disable-next-line const [_, major, minor, patch] = match - return `${major}${minor}${patch}` - } - } - throw new Error('Git not installed. Please install Git') + return `${major}${minor}${patch}` + } + } + throw new Error('Git not installed. Please install Git') } async function init(): Promise { - const {stderr} = await node.exec('git init') - if (stderr) { - throw new Error('Error initializing Gits') - } + const { stderr } = await node.exec('git init') + if (stderr) { + throw new Error('Error initializing Gits') + } } export async function initIfNotExists(): Promise { - const hasGit = await version() + const hasGit = await version() - if (!hasGit) { - throw new Error('Git must be installed') - } + if (!hasGit) { + throw new Error('Git must be installed') + } - const hasGitInit = node.exists('.git') - if (!hasGitInit) { - await init() - } + const hasGitInit = node.exists('.git') + if (!hasGitInit) { + await init() + } } export async function addRemote(repo: string): Promise { - const {stderr} = await node.exec(`git remote add ${gitOrigin} ${repo} && git fetch ${gitOrigin}`) - if (stderr) { - const alreadyExists = stderr.match(`${gitOrigin} already exists.`) - const successfulNewBranch = stderr.match('new branch') - - // validate the response is acceptable - if (!alreadyExists && !successfulNewBranch) { - console.error(stderr) - throw new Error('Error adding git remote') - } - } + const { stderr } = await node.exec(`git remote add ${gitOrigin} ${repo} && git fetch ${gitOrigin}`) + if (stderr) { + const alreadyExists = stderr.match(`${gitOrigin} already exists.`) + const successfulNewBranch = stderr.match('new branch') + + // validate the response is acceptable + if (!alreadyExists && !successfulNewBranch) { + console.error(stderr) + throw new Error('Error adding git remote') + } + } } export async function checkRemoteExists(): Promise { - try { - const {stdout, stderr} = await node.exec('git remote -v') - if (stderr) { - return false - } - // string match on remote output - // TODO: improve the specificity of this regex - return !!stdout.match(gitOrigin) - } catch (error) { - return false - } + try { + const { stdout, stderr } = await node.exec('git remote -v') + if (stderr) { + return false + } + // string match on remote output + // TODO: improve the specificity of this regex + return !!stdout.match(gitOrigin) + } catch (error) { + return false + } } export async function setupRemote(repo: string): Promise { - // check coderoad remote not taken - const hasRemote = await checkRemoteExists() - // git remote add coderoad tutorial - // git fetch coderoad - if (!hasRemote) { - await addRemote(repo) - } + // check coderoad remote not taken + const hasRemote = await checkRemoteExists() + // git remote add coderoad tutorial + // git fetch coderoad + if (!hasRemote) { + await addRemote(repo) + } } diff --git a/src/services/node/index.ts b/src/services/node/index.ts index 5ae93b93..6b91bc9f 100644 --- a/src/services/node/index.ts +++ b/src/services/node/index.ts @@ -1,27 +1,28 @@ import * as fs from 'fs' -import {join} from 'path' -import {exec as cpExec} from 'child_process' -import {promisify} from 'util' +import { join } from 'path' +import { exec as cpExec } from 'child_process' +import { promisify } from 'util' import * as vscode from 'vscode' const asyncExec = promisify(cpExec) class Node { - private workspaceRootPath: string - constructor() { - // set workspace root for node executions - const workspaceRoots: vscode.WorkspaceFolder[] | undefined = vscode.workspace.workspaceFolders - if (!workspaceRoots || !workspaceRoots.length) { - throw new Error('No workspace root path') - } - const workspaceRoot: vscode.WorkspaceFolder = workspaceRoots[0] - this.workspaceRootPath = workspaceRoot.uri.path - } - public exec = (cmd: string): Promise<{stdout: string; stderr: string}> => asyncExec(cmd, { - cwd: this.workspaceRootPath, - }) + private workspaceRootPath: string + constructor() { + // set workspace root for node executions + const workspaceRoots: vscode.WorkspaceFolder[] | undefined = vscode.workspace.workspaceFolders + if (!workspaceRoots || !workspaceRoots.length) { + throw new Error('No workspace root path') + } + const workspaceRoot: vscode.WorkspaceFolder = workspaceRoots[0] + this.workspaceRootPath = workspaceRoot.uri.path + } + public exec = (cmd: string): Promise<{ stdout: string; stderr: string }> => + asyncExec(cmd, { + cwd: this.workspaceRootPath, + }) - public exists = (...paths: string[]): boolean => fs.existsSync(join(this.workspaceRootPath, ...paths)) + public exists = (...paths: string[]): boolean => fs.existsSync(join(this.workspaceRootPath, ...paths)) } export default new Node() diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index 3a228bed..6fe98a99 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -7,36 +7,36 @@ import * as vscode from 'vscode' // storage is unfortunately bound to the vscode extension context // forcing it to be passed in through activation and down to other tools class Storage { - private key: string - private storage: vscode.Memento - private defaultValue: T - constructor({key, storage, defaultValue}: {key: string, storage: vscode.Memento, defaultValue: T}) { - this.storage = storage - this.key = key - this.defaultValue = defaultValue - } - public get = async (): Promise => { - // const value: string | undefined = await this.storage.get(this.key) - // if (value) { - // return JSON.parse(value) - // } - return this.defaultValue - } - public set = (value: T): void => { - const stringValue = JSON.stringify(value) - this.storage.update(this.key, stringValue) - } - public update = async (value: T): Promise => { - const current = await this.get() - const next = JSON.stringify({ - ...current, - ...value, - }) - this.storage.update(this.key, next) - } - public reset = () => { - this.set(this.defaultValue) - } + private key: string + private storage: vscode.Memento + private defaultValue: T + constructor({ key, storage, defaultValue }: { key: string; storage: vscode.Memento; defaultValue: T }) { + this.storage = storage + this.key = key + this.defaultValue = defaultValue + } + public get = async (): Promise => { + // const value: string | undefined = await this.storage.get(this.key) + // if (value) { + // return JSON.parse(value) + // } + return this.defaultValue + } + public set = (value: T): void => { + const stringValue = JSON.stringify(value) + this.storage.update(this.key, stringValue) + } + public update = async (value: T): Promise => { + const current = await this.get() + const next = JSON.stringify({ + ...current, + ...value, + }) + this.storage.update(this.key, next) + } + public reset = () => { + this.set(this.defaultValue) + } } -export default Storage \ No newline at end of file +export default Storage diff --git a/src/services/testRunner/index.ts b/src/services/testRunner/index.ts index ad32b1c6..6d8ecb72 100644 --- a/src/services/testRunner/index.ts +++ b/src/services/testRunner/index.ts @@ -1,68 +1,73 @@ import node from '../../services/node' -import {getOutputChannel} from '../../editor/outputChannel' +import { getOutputChannel } from '../../editor/outputChannel' import parser from './parser' -import {setLatestProcess, isLatestProcess} from './throttle' +import { setLatestProcess, isLatestProcess } from './throttle' export interface Payload { - stepId: string + stepId: string } interface Callbacks { - onSuccess(payload: Payload): void - onFail(payload: Payload): void - onRun(payload: Payload): void - onError(payload: Payload): void + onSuccess(payload: Payload): void + onFail(payload: Payload, message: string): void + onRun(payload: Payload): void + onError(payload: Payload): void } interface TestRunnerConfig { - command: string + command: string } const createTestRunner = (config: TestRunnerConfig, callbacks: Callbacks) => { + const outputChannelName = 'TEST_OUTPUT' - const outputChannelName = 'TEST_OUTPUT' + return async (payload: Payload, onSuccess?: () => void): Promise => { + console.log('------------------- run test ------------------') - return async (payload: Payload, onSuccess?: () => void): Promise => { - console.log('------------------- run test ------------------') + // flag as running + callbacks.onRun(payload) - // flag as running - callbacks.onRun(payload) + let result: { stdout: string | undefined; stderr: string | undefined } + try { + result = await node.exec(config.command) + } catch (err) { + result = err + } + const { stdout, stderr } = result - let result: {stdout: string | undefined, stderr: string | undefined} - try { - result = await node.exec(config.command) - } catch (err) { - result = err - } - const {stdout, stderr} = result + // simple way to throttle requests + if (!stdout) { + return + } - // simple way to throttle requests - if (!stdout) {return } + if (stderr) { + callbacks.onError(payload) - if (stderr) { - callbacks.onError(payload) + // open terminal with error string + const channel = getOutputChannel(outputChannelName) + channel.show(false) + channel.appendLine(stderr) + return + } - // open terminal with error string - const channel = getOutputChannel(outputChannelName) - channel.show(false) - channel.appendLine(stderr) - return - } + // pass or fail? + const tap = parser(stdout) + if (tap.ok) { + callbacks.onSuccess(payload) + if (onSuccess) { + onSuccess() + } + } else { + // TODO: consider logging output to channel + // open terminal with failed test string + // const channel = getOutputChannel(outputChannelName) + // channel.show(false) + // channel.appendLine(tap.message) - // pass or fail? - const tap = parser(stdout) - if (tap.ok) { - callbacks.onSuccess(payload) - if (onSuccess) {onSuccess()} - } else { - // TODO: parse failure message - // open terminal with failed test string - // const channel = getOutputChannel(outputChannelName) - // channel.show(false) - // channel.appendLine(testsFailed.message) - callbacks.onFail(payload) - } - } + const message = tap.message ? tap.message : '' + callbacks.onFail(payload, message) + } + } } -export default createTestRunner \ No newline at end of file +export default createTestRunner diff --git a/src/services/testRunner/parser.test.ts b/src/services/testRunner/parser.test.ts new file mode 100644 index 00000000..260786df --- /dev/null +++ b/src/services/testRunner/parser.test.ts @@ -0,0 +1,31 @@ +import parser from './parser' + +describe('parser', () => { + test('should detect success', () => { + const example = ` +1..2 +ok 1 - Should pass +ok 2 - Should also pass +` + expect(parser(example)).toEqual({ ok: true }) + }) + test('should detect failure', () => { + const example = ` +1..3 +ok 1 - Should pass +not ok 2 - This one fails +ok 3 - Also passes +` + expect(parser(example).ok).toBe(false) + }) + test('should return failure message', () => { + const example = ` +1..4 +ok 1 - Should pass +not ok 2 - First to fail +ok 3 - Also passes +not ok 4 - Second to fail +` + expect(parser(example).message).toBe('First to fail') + }) +}) diff --git a/src/services/testRunner/parser.ts b/src/services/testRunner/parser.ts index e5f6967b..f77ce10c 100644 --- a/src/services/testRunner/parser.ts +++ b/src/services/testRunner/parser.ts @@ -1,15 +1,18 @@ interface ParserOutput { - ok: boolean + ok: boolean + message?: string } const parser = (text: string): ParserOutput => { - const lines = text.split('\n') - for (const line of lines) { - if (line.match(/^not ok /)) { - return {ok: false} - } - } - return {ok: true} + const lines = text.split('\n') + for (const line of lines) { + // parse failed test + const match = line.match(/^not ok \d+ - (.+)+/) + if (!!match) { + return { ok: false, message: match[1] } + } + } + return { ok: true } } -export default parser \ No newline at end of file +export default parser diff --git a/src/test/runTest.ts b/src/test/runTest.ts index 798971a4..db20a303 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -1,30 +1,29 @@ import * as path from 'path' -import {runTests} from 'vscode-test' +import { runTests } from 'vscode-test' async function main() { - // The folder containing the Extension Manifest package.json - // Passed to `--extensionDevelopmentPath` - const extensionDevelopmentPath: string = path.resolve(__dirname, '../../') + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath: string = path.resolve(__dirname, '../../') - // The path to the extension test script - // Passed to --extensionTestsPath - const extensionTestsPath: string = path.resolve(__dirname, './suite/index') + // The path to the extension test script + // Passed to --extensionTestsPath + const extensionTestsPath: string = path.resolve(__dirname, './suite/index') - const config = { - extensionDevelopmentPath, - extensionTestsPath, - launchArgs: ['--disable-extensions'] // turn off other extensions - } + const config = { + extensionDevelopmentPath, + extensionTestsPath, + launchArgs: ['--disable-extensions'], // turn off other extensions + } - // Download VS Code, unzip it and run the integration test - await runTests(config) - .catch((err: Error) => { - console.error(err) - process.exit(1) - }) + // Download VS Code, unzip it and run the integration test + await runTests(config).catch((err: Error) => { + console.error(err) + process.exit(1) + }) - process.exit(0) + process.exit(0) } -main() \ No newline at end of file +main() diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts deleted file mode 100644 index 2a4c4cd1..00000000 --- a/src/test/suite/extension.test.ts +++ /dev/null @@ -1,24 +0,0 @@ - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode' -import {before, after} from 'mocha' -import * as assert from 'assert' -// import * as myExtension from '../../extension' - -suite('Extension tests', () => { - before(() => { - vscode.window.showInformationMessage('Start all tests.') - }) - - test('Loads App', async () => { - await vscode.commands.executeCommand('coderoad.start') - await setTimeout(() => Promise.resolve(), 5000) - // const webview = vscode.window.activeTextEditor - assert.equal(2, 2) - }) - - after(() => { - console.log('done') - }) -}) \ No newline at end of file diff --git a/src/test/suite/extension.ts b/src/test/suite/extension.ts new file mode 100644 index 00000000..b472460d --- /dev/null +++ b/src/test/suite/extension.ts @@ -0,0 +1,23 @@ +// // You can import and use all API from the 'vscode' module +// // as well as import your extension to test it +// import * as vscode from 'vscode' +// import {before, after} from 'mocha' +// import * as assert from 'assert' +// // import * as myExtension from '../../extension' + +// suite('Extension tests', () => { +// before(() => { +// vscode.window.showInformationMessage('Start all tests.') +// }) + +// test('Loads App', async () => { +// await vscode.commands.executeCommand('coderoad.start') +// await setTimeout(() => Promise.resolve(), 5000) +// // const webview = vscode.window.activeTextEditor +// assert.equal(2, 2) +// }) + +// after(() => { +// console.log('done') +// }) +// }) diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index 35a9b96e..3973b3fa 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -1,37 +1,37 @@ -import * as path from 'path' -import * as Mocha from 'mocha' -import * as glob from 'glob' +// import * as path from 'path' +// import * as Mocha from 'mocha' +// import * as glob from 'glob' -export function run(): Promise { - // Create the mocha test - const mocha = new Mocha({ - ui: 'tdd', - }) - mocha.useColors(true) +// export function run(): Promise { +// // Create the mocha test +// const mocha = new Mocha({ +// ui: 'tdd', +// }) +// mocha.useColors(true) - const testsRoot = path.resolve(__dirname, '..') +// const testsRoot = path.resolve(__dirname, '..') - return new Promise((c, e) => { - glob('**/**.test.js', {cwd: testsRoot}, (err, files) => { - if (err) { - return e(err) - } +// return new Promise((c, e) => { +// glob('**/**.test.js', {cwd: testsRoot}, (err, files) => { +// if (err) { +// return e(err) +// } - // Add files to the test suite - files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))) +// // Add files to the test suite +// files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))) - try { - // Run the mocha test - mocha.run(failures => { - if (failures > 0) { - e(new Error(`${failures} tests failed.`)) - } else { - c() - } - }) - } catch (err) { - e(err) - } - }) - }) -} \ No newline at end of file +// try { +// // Run the mocha test +// mocha.run(failures => { +// if (failures > 0) { +// e(new Error(`${failures} tests failed.`)) +// } else { +// c() +// } +// }) +// } catch (err) { +// e(err) +// } +// }) +// }) +// } diff --git a/tsconfig.json b/tsconfig.json index e62adc7c..61ff47cf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,30 +1,30 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "outDir": "build", - "lib": ["es6", "dom"], - "sourceMap": true, - "rootDir": "src", - "baseUrl": "src", - "strict": true /* enable all strict type-checking options */, - /* Additional Checks */ - "forceConsistentCasingInFileNames": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "paths": { - "typings": ["../typings/index.d.ts"], - "typings/graphql": ["../typings/graphql.d.ts"], - "@api": ["services/api/index"], - "@gql/*": ["services/api/gql/*"] - }, - "allowJs": true - }, - "exclude": ["node_modules", ".vscode-test", "build", "resources", "web-app", "*.js", "*.test.ts"] + "compilerOptions": { + "module": "commonjs", + "target": "es2018", + "outDir": "build", + "lib": ["es2018", "dom"], + "sourceMap": true, + "rootDir": "src", + "baseUrl": "src", + "strict": true /* enable all strict type-checking options */, + /* Additional Checks */ + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true, + "suppressImplicitAnyIndexErrors": true, + "noUnusedLocals": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "paths": { + "typings": ["../typings/index.d.ts"], + "typings/graphql": ["../typings/graphql.d.ts"], + "@api": ["services/api/index"], + "@gql/*": ["services/api/gql/*"] + }, + "allowJs": true + }, + "exclude": ["node_modules", ".vscode-test", "build", "resources", "web-app", "*.js", "*.test.ts"] } diff --git a/tslint.json b/tslint.json deleted file mode 100644 index 484ae6aa..00000000 --- a/tslint.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": ["tslint:latest", "tslint-config-prettier"], - "rules": { - "class-name": true, - "curly": true, - "interface-name": [true, "never-prefix"], - "no-string-throw": true, - "no-duplicate-variable": true, - "no-implicit-dependencies": false, - "no-unused-expression": true, - "object-literal-sort-keys": false, - "ordered-imports": false, - "semicolon": [true, "never"], - "triple-equals": true, - "forin": false, - "no-console": false, - "no-submodule-imports": false, - "no-object-literal-type-assertion": false - }, - "defaultSeverity": "warning", - - "linterOptions": { - "exclude": ["node_modules/**", "src/typings/graphql.d.ts"] - } -} diff --git a/web-app/.eslintrc.js b/web-app/.eslintrc.js index 19b4894d..769395ff 100644 --- a/web-app/.eslintrc.js +++ b/web-app/.eslintrc.js @@ -1,11 +1,10 @@ module.exports = { - parser: 'typescript-eslint-parser', - extends: [ - // 'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react - // 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin - // 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier - // 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. - ], + parser: '@typescript-eslint/parser', // Specifies the ESLint parser + extends: [ + 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin + 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier + 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array + ], parserOptions: { ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features sourceType: 'module', // Allows for the use of imports @@ -52,6 +51,11 @@ module.exports = { 'no-return-assign': 'off', 'no-case-declarations': 'off', 'react/no-array-index-key': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-ignore': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/camelcase': 'off', }, settings: { react: { diff --git a/web-app/package-lock.json b/web-app/package-lock.json index 8c132e1a..f7ede962 100644 --- a/web-app/package-lock.json +++ b/web-app/package-lock.json @@ -2472,12 +2472,12 @@ "integrity": "sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==" }, "@typescript-eslint/eslint-plugin": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.3.2.tgz", - "integrity": "sha512-tcnpksq1bXzcIRbYLeXkgp6l+ggEMXXUcl1wsSvL807fRtmvVQKygElwEUf4hBA76dNag3VAK1q2m3vd7qJaZA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.7.0.tgz", + "integrity": "sha512-H5G7yi0b0FgmqaEUpzyBlVh0d9lq4cWG2ap0RKa6BkF3rpBb6IrAoubt1NWh9R2kRs/f0k6XwRDiDz3X/FqXhQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "2.3.2", + "@typescript-eslint/experimental-utils": "2.7.0", "eslint-utils": "^1.4.2", "functional-red-black-tree": "^1.0.1", "regexpp": "^2.0.1", @@ -2485,13 +2485,13 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.3.2.tgz", - "integrity": "sha512-t+JGdTT6dRbmvKDlhlVkEueoZa0fhJNfG6z2cpnRPLwm3VwYr2BjR//acJGC1Yza0I9ZNcDfRY7ubQEvvfG6Jg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.7.0.tgz", + "integrity": "sha512-9/L/OJh2a5G2ltgBWJpHRfGnt61AgDeH6rsdg59BH0naQseSwR7abwHq3D5/op0KYD/zFT4LS5gGvWcMmegTEg==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.3.2", + "@typescript-eslint/typescript-estree": "2.7.0", "eslint-scope": "^5.0.0" }, "dependencies": { @@ -2508,27 +2508,29 @@ } }, "@typescript-eslint/parser": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.3.2.tgz", - "integrity": "sha512-nq1UQeNGdKdqdgF6Ww+Ov2OidWgiL96+JYdXXZ2rkP/OWyc6KMNSbs6MpRCpI8q+PmDa7hBnHNQIo7w/drYccA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.7.0.tgz", + "integrity": "sha512-ctC0g0ZvYclxMh/xI+tyqP0EC2fAo6KicN9Wm2EIao+8OppLfxji7KAGJosQHSGBj3TcqUrA96AjgXuKa5ob2g==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.3.2", - "@typescript-eslint/typescript-estree": "2.3.2", + "@typescript-eslint/experimental-utils": "2.7.0", + "@typescript-eslint/typescript-estree": "2.7.0", "eslint-visitor-keys": "^1.1.0" } }, "@typescript-eslint/typescript-estree": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.3.2.tgz", - "integrity": "sha512-eZNEAai16nwyhIVIEaWQlaUgAU3S9CkQ58qvK0+3IuSdLJD3W1PNuehQFMIhW/mTP1oFR9GNoTcLg7gtXz6lzA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.7.0.tgz", + "integrity": "sha512-vVCE/DY72N4RiJ/2f10PTyYekX2OLaltuSIBqeHYI44GQ940VCYioInIb8jKMrK9u855OEJdFC+HmWAZTnC+Ag==", "dev": true, "requires": { + "debug": "^4.1.1", "glob": "^7.1.4", "is-glob": "^4.0.1", "lodash.unescape": "4.0.1", - "semver": "^6.3.0" + "semver": "^6.3.0", + "tsutils": "^3.17.1" }, "dependencies": { "semver": { @@ -2783,9 +2785,9 @@ } }, "acorn-jsx": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz", - "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", "dev": true }, "acorn-walk": { @@ -6356,9 +6358,9 @@ } }, "eslint": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.5.1.tgz", - "integrity": "sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.6.0.tgz", + "integrity": "sha512-PpEBq7b6qY/qrOmpYQ/jTMDYfuQMELR4g4WI1M/NaSDDD/bdcMb+dj4Hgks7p41kW2caXsPsEZAEAyAgjVVC0g==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -6368,9 +6370,9 @@ "debug": "^4.0.1", "doctrine": "^3.0.0", "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.2", + "eslint-utils": "^1.4.3", "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.1", + "espree": "^6.1.2", "esquery": "^1.0.1", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", @@ -6380,7 +6382,7 @@ "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^6.4.1", + "inquirer": "^7.0.0", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", @@ -6400,12 +6402,30 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { + "ansi-escapes": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz", + "integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==", + "dev": true, + "requires": { + "type-fest": "^0.5.2" + } + }, "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -6427,6 +6447,12 @@ } } }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "eslint-scope": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", @@ -6437,6 +6463,15 @@ "estraverse": "^4.1.1" } }, + "figures": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", + "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, "glob-parent": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", @@ -6453,27 +6488,107 @@ "dev": true }, "import-fresh": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", - "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", "dev": true, "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, + "inquirer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz", + "integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^2.4.2", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^4.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -6481,7 +6596,38 @@ "dev": true, "requires": { "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } } + }, + "type-fest": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", + "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.5.0.tgz", + "integrity": "sha512-cjXp8SbO9VFGW/Z7mbTydqS9to8Z58E5aYhj3e1+Hx7lS9s6gL5ILKNpCqZAFOVYRcSkWPFYljHrEh8QFEK5EQ==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + }, + "dependencies": { + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true } } }, @@ -6803,6 +6949,15 @@ "jsx-ast-utils": "^2.2.1" } }, + "eslint-plugin-prettier": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz", + "integrity": "sha512-A+TZuHZ0KU0cnn56/9mfR7/KjUJ9QNVXUhwvRFSR7PGPe0zQR6PTkmyqg1AtUUEOzTqeRsUwyKFh0oVZKVCrtA==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, "eslint-plugin-react": { "version": "7.14.3", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.14.3.tgz", @@ -6848,12 +7003,12 @@ } }, "eslint-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", - "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.0.0" + "eslint-visitor-keys": "^1.1.0" } }, "eslint-visitor-keys": { @@ -6863,13 +7018,13 @@ "dev": true }, "espree": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.1.tgz", - "integrity": "sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", + "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", "dev": true, "requires": { - "acorn": "^7.0.0", - "acorn-jsx": "^5.0.2", + "acorn": "^7.1.0", + "acorn-jsx": "^5.1.0", "eslint-visitor-keys": "^1.1.0" }, "dependencies": { @@ -7230,6 +7385,12 @@ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", "dev": true }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, "fast-glob": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", @@ -13710,6 +13871,21 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, "pretty-bytes": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz", diff --git a/web-app/package.json b/web-app/package.json index c10c5ca9..dc8ffd90 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -60,9 +60,15 @@ "@types/react": "^16.9.11", "@types/react-dom": "^16.9.4", "@types/storybook__react": "^4.0.2", + "@typescript-eslint/eslint-plugin": "^2.7.0", + "@typescript-eslint/parser": "^2.7.0", "babel-loader": "8.0.5", "babel-plugin-import": "^1.12.1", + "eslint": "^6.6.0", + "eslint-config-prettier": "^6.5.0", + "eslint-plugin-prettier": "^3.1.1", "node-sass": "^4.13.0", + "prettier": "^1.19.1", "react-scripts": "^3.2.0", "sass-loader": "^8.0.0", "typescript-eslint-parser": "^22.0.0" diff --git a/web-app/src/App.tsx b/web-app/src/App.tsx index c70c0b67..d8c9e71a 100644 --- a/web-app/src/App.tsx +++ b/web-app/src/App.tsx @@ -6,11 +6,11 @@ import client from './services/apollo' import Routes from './Routes' const App = () => ( - - - - - + + + + + ) export default App diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx index 3f21eff4..15f68e05 100644 --- a/web-app/src/Routes.tsx +++ b/web-app/src/Routes.tsx @@ -15,36 +15,36 @@ const { Route } = Router const tempSend = (action: any) => console.log('sent') const Routes = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } export default Routes diff --git a/web-app/src/components/Button/index.tsx b/web-app/src/components/Button/index.tsx index c07f2ee8..f135f1ba 100644 --- a/web-app/src/components/Button/index.tsx +++ b/web-app/src/components/Button/index.tsx @@ -2,17 +2,17 @@ import * as React from 'react' import { Button as AlifdButton } from '@alifd/next' interface Props { - style?: React.CSSProperties - children: string | React.ReactNode - disabled?: boolean - type?: 'primary' | 'secondary' | 'normal' - onClick?: () => void + style?: React.CSSProperties + children: string | React.ReactNode + disabled?: boolean + type?: 'primary' | 'secondary' | 'normal' + onClick?: () => void } const Button = (props: Props) => ( - - {props.children} - + + {props.children} + ) export default Button diff --git a/web-app/src/components/Card/index.tsx b/web-app/src/components/Card/index.tsx index e511adbc..62edc14b 100644 --- a/web-app/src/components/Card/index.tsx +++ b/web-app/src/components/Card/index.tsx @@ -2,27 +2,27 @@ import * as React from 'react' import { Card as AlifdCard } from '@alifd/next' const styles = { - card: { - display: 'flex', - width: '100%', - }, + card: { + display: 'flex', + width: '100%', + }, } interface Props { - children: React.ReactNode - onClick?: () => void - style?: React.CSSProperties + children: React.ReactNode + onClick?: () => void + style?: React.CSSProperties } const Card = (props: Props) => ( - - {props.children} - + + {props.children} + ) export default Card diff --git a/web-app/src/components/Checkbox/index.tsx b/web-app/src/components/Checkbox/index.tsx index 2d1e5112..f22b04b1 100644 --- a/web-app/src/components/Checkbox/index.tsx +++ b/web-app/src/components/Checkbox/index.tsx @@ -1,34 +1,34 @@ import * as React from 'react' const styles = { - box: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }, - input: { - border: '1px solid black', - backgroundColor: 'yellow', - }, + box: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + input: { + border: '1px solid black', + backgroundColor: 'yellow', + }, } interface Props { - status: 'COMPLETE' | 'INCOMPLETE' | 'ACTIVE' | 'LOADING' + status: 'COMPLETE' | 'INCOMPLETE' | 'ACTIVE' | 'LOADING' } const Checkbox = (props: Props) => { - const checked = props.status === 'COMPLETE' - // const loading = props.state === 'LOADING' - const onChange = () => { - /* read */ - } - return ( -
- -
- ) + const checked = props.status === 'COMPLETE' + // const loading = props.state === 'LOADING' + const onChange = () => { + /* read */ + } + return ( +
+ +
+ ) } export default Checkbox diff --git a/web-app/src/components/Debugger/debuggerWrapper.tsx b/web-app/src/components/Debugger/debuggerWrapper.tsx index cf65cf22..19a1d990 100644 --- a/web-app/src/components/Debugger/debuggerWrapper.tsx +++ b/web-app/src/components/Debugger/debuggerWrapper.tsx @@ -5,14 +5,14 @@ import stateToString from './stateToString' const SHOW_DEBUGGER = JSON.parse(process.env.REACT_APP_DEBUG || 'false') const debuggerWrapper = (element: React.ReactElement, state: any) => { - if (SHOW_DEBUGGER) { - return ( - - {element} - - ) - } - return element + if (SHOW_DEBUGGER) { + return ( + + {element} + + ) + } + return element } export default debuggerWrapper diff --git a/web-app/src/components/Debugger/index.tsx b/web-app/src/components/Debugger/index.tsx index 7ba10186..0becacf8 100644 --- a/web-app/src/components/Debugger/index.tsx +++ b/web-app/src/components/Debugger/index.tsx @@ -3,24 +3,24 @@ import * as G from 'typings/graphql' import * as CR from 'typings' interface Props { - state: string - tutorial: G.Tutorial - env: CR.Environment - position: CR.Position - progress: CR.Progress - children: React.ReactElement + state: string + tutorial: G.Tutorial + env: CR.Environment + position: CR.Position + progress: CR.Progress + children: React.ReactElement } const Debugger = ({ state, children, env, position, progress, tutorial }: Props) => ( -
-

state: {state}

-

MachineId: {env.machineId}

-

SessionId: {env.sessionId}

-

tutorial: {tutorial ? tutorial.id : 'none'}

-

position: {JSON.stringify(position)}

-

progress: {JSON.stringify(progress)}

- {children} -
+
+

state: {state}

+

MachineId: {env.machineId}

+

SessionId: {env.sessionId}

+

tutorial: {tutorial ? tutorial.id : 'none'}

+

position: {JSON.stringify(position)}

+

progress: {JSON.stringify(progress)}

+ {children} +
) export default Debugger diff --git a/web-app/src/components/Debugger/stateToString.ts b/web-app/src/components/Debugger/stateToString.ts index 705fb492..c20c0bed 100644 --- a/web-app/src/components/Debugger/stateToString.ts +++ b/web-app/src/components/Debugger/stateToString.ts @@ -1,16 +1,16 @@ -const stateToString = (state: string | object, str: string = ''): string => { - if (typeof state === 'object') { - const keys = Object.keys(state) - if (keys && keys.length) { - const key = keys[0] - // @ts-ignore - return stateToString(state[key], str.length ? `${str}.${key}` : key) - } - return str - } else if (typeof state === 'string') { - return str.length ? `${str}.${state}` : state - } - return '' +const stateToString = (state: string | object, str = ''): string => { + if (typeof state === 'object') { + const keys = Object.keys(state) + if (keys && keys.length) { + const key = keys[0] + // @ts-ignore + return stateToString(state[key], str.length ? `${str}.${key}` : key) + } + return str + } else if (typeof state === 'string') { + return str.length ? `${str}.${state}` : state + } + return '' } -export default stateToString \ No newline at end of file +export default stateToString diff --git a/web-app/src/components/Divider.tsx b/web-app/src/components/Divider.tsx index bf24b900..a46b558d 100644 --- a/web-app/src/components/Divider.tsx +++ b/web-app/src/components/Divider.tsx @@ -1,10 +1,10 @@ import * as React from 'react' const styles = { - divider: { - backgroundColor: '#e8e8e8', - height: '0.1rem', - }, + divider: { + backgroundColor: '#e8e8e8', + height: '0.1rem', + }, } const Divider = () =>
diff --git a/web-app/src/components/Error/index.tsx b/web-app/src/components/Error/index.tsx index 5324f63d..abeef5c1 100644 --- a/web-app/src/components/Error/index.tsx +++ b/web-app/src/components/Error/index.tsx @@ -3,43 +3,43 @@ import { ApolloError } from 'apollo-boost' import { GraphQLError } from 'graphql' const styles = { - container: { - color: '#D8000C', - backgroundColor: '#FFBABA', - padding: '1rem', - }, + container: { + color: '#D8000C', + backgroundColor: '#FFBABA', + padding: '1rem', + }, } interface Props { - error: ApolloError + error: ApolloError } const ErrorView = ({ error }: Props) => { - console.log(error) - return ( -
-

Error

- {error.graphQLErrors && ( -
- {error.graphQLErrors.map(({ message, locations, path }: GraphQLError, index: number) => ( -
- [GraphQL error]: Message: {message}, Location: {locations}, Path: {path} -
- ))} -
- )} - {error.networkError && ( -
- [Network error]: {error.networkError.message} -
- )} - {error.extraInfo && ( -

- [Extra info]: {JSON.stringify(error.extraInfo)} -

- )} -
- ) + console.log(error) + return ( +
+

Error

+ {error.graphQLErrors && ( +
+ {error.graphQLErrors.map(({ message, locations, path }: GraphQLError, index: number) => ( +
+ [GraphQL error]: Message: {message}, Location: {locations}, Path: {path} +
+ ))} +
+ )} + {error.networkError && ( +
+ [Network error]: {error.networkError.message} +
+ )} + {error.extraInfo && ( +

+ [Extra info]: {JSON.stringify(error.extraInfo)} +

+ )} +
+ ) } export default ErrorView diff --git a/web-app/src/components/ErrorBoundary/index.tsx b/web-app/src/components/ErrorBoundary/index.tsx index 00d5b5d0..b855ae68 100644 --- a/web-app/src/components/ErrorBoundary/index.tsx +++ b/web-app/src/components/ErrorBoundary/index.tsx @@ -1,23 +1,23 @@ import * as React from 'react' class ErrorBoundary extends React.Component { - public state = { hasError: false } + public state = { hasError: false } - public componentDidCatch(error: Error, info: any) { - // Display fallback UI - this.setState({ hasError: true }) - // You can also log the error to an error reporting service - console.error(JSON.stringify(error)) - console.log(JSON.stringify(info)) - } + public componentDidCatch(error: Error, info: any) { + // Display fallback UI + this.setState({ hasError: true }) + // You can also log the error to an error reporting service + console.error(JSON.stringify(error)) + console.log(JSON.stringify(info)) + } - public render() { - if (this.state.hasError) { - // You can render any custom fallback UI - return

Something went wrong.

- } - return this.props.children - } + public render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return

Something went wrong.

+ } + return this.props.children + } } export default ErrorBoundary diff --git a/web-app/src/components/Icon/index.tsx b/web-app/src/components/Icon/index.tsx index 7ac064dc..e690d973 100644 --- a/web-app/src/components/Icon/index.tsx +++ b/web-app/src/components/Icon/index.tsx @@ -2,14 +2,14 @@ import * as React from 'react' import { Icon as AlifdIcon } from '@alifd/next' interface Props { - type: string - role?: string - style?: React.CSSProperties - size?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' | 'inherit' + type: string + role?: string + style?: React.CSSProperties + size?: 'xxs' | 'xs' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' | 'inherit' } const Icon = (props: Props) => { - return + return } export default Icon diff --git a/web-app/src/components/Loading/index.tsx b/web-app/src/components/Loading/index.tsx index 2a85d70d..9c0c1b17 100644 --- a/web-app/src/components/Loading/index.tsx +++ b/web-app/src/components/Loading/index.tsx @@ -2,11 +2,11 @@ import * as React from 'react' import { Loading } from '@alifd/next' interface Props { - text: string + text: string } const LoadingComponent = ({ text }: Props) => { - return + return } export default LoadingComponent diff --git a/web-app/src/components/Markdown/index.tsx b/web-app/src/components/Markdown/index.tsx index bc746ae0..cab3bc0c 100644 --- a/web-app/src/components/Markdown/index.tsx +++ b/web-app/src/components/Markdown/index.tsx @@ -10,34 +10,34 @@ import './prism' // markdown highlighter instance const md: MarkdownIt = new MarkdownIt({ - breaks: true, - // highlight: syntaxHighlight, - html: true, - linkify: true, + breaks: true, + // highlight: syntaxHighlight, + html: true, + linkify: true, }) - // add emoji: https://github.com/markdown-it/markdown-it-emoji - .use(markdownEmoji) - .use(prism, { - defaultLanguage: 'js', - }) + // add emoji: https://github.com/markdown-it/markdown-it-emoji + .use(markdownEmoji) + .use(prism, { + defaultLanguage: 'js', + }) interface Props { - children: string + children: string } const Markdown = (props: Props) => { - let html: string - try { - html = md.render(props.children) - } catch (error) { - console.log(`failed to parse markdown for ${props.children}`) - html = `
+ let html: string + try { + html = md.render(props.children) + } catch (error) { + console.log(`failed to parse markdown for ${props.children}`) + html = `
ERROR: Failed to parse markdown

${props.children}

` - } - // TODO: sanitize markdown or HTML - return
+ } + // TODO: sanitize markdown or HTML + return
} export default Markdown diff --git a/web-app/src/components/Markdown/prism.ts b/web-app/src/components/Markdown/prism.ts index 86d3d551..fee032e3 100644 --- a/web-app/src/components/Markdown/prism.ts +++ b/web-app/src/components/Markdown/prism.ts @@ -10,4 +10,4 @@ import 'prismjs/components/prism-javascript' import 'prismjs/components/prism-json' import 'prismjs/components/prism-sql' import 'prismjs/components/prism-typescript' -// TODO: import all - current list only supports js related \ No newline at end of file +// TODO: import all - current list only supports js related diff --git a/web-app/src/components/Router/Route.tsx b/web-app/src/components/Router/Route.tsx index e6a64905..d73001df 100644 --- a/web-app/src/components/Router/Route.tsx +++ b/web-app/src/components/Router/Route.tsx @@ -1,6 +1,6 @@ interface Props { - children: any - path: string | string[] + children: any + path: string | string[] } const Route = ({ children }: Props) => children diff --git a/web-app/src/components/Router/index.tsx b/web-app/src/components/Router/index.tsx index 29620c5d..3aa1d84f 100644 --- a/web-app/src/components/Router/index.tsx +++ b/web-app/src/components/Router/index.tsx @@ -9,45 +9,45 @@ import channel from '../../services/channel' import messageBusReceiver from '../../services/channel/receiver' interface Props { - children: any + children: any } interface CloneElementProps { - context: CR.MachineContext - send(action: CR.Action): void + context: CR.MachineContext + send(action: CR.Action): void } // router finds first state match of const Router = ({ children }: Props): React.ReactElement | null => { - const [state, send] = useMachine(machine, { - interpreterOptions: { - logger: console.log.bind('XSTATE:'), - }, - }) - - channel.setMachineSend(send) - - // event bus listener - React.useEffect(messageBusReceiver, []) - - const childArray = React.Children.toArray(children) - for (const child of childArray) { - const { path } = child.props - let pathMatch - if (typeof path === 'string') { - pathMatch = state.matches(path) - } else if (Array.isArray(path)) { - pathMatch = path.some(p => state.matches(p)) - } else { - throw new Error(`Invalid route path ${JSON.stringify(path)}`) - } - if (pathMatch) { - const element = React.cloneElement(child.props.children, { send, context: state.context }) - return debuggerWrapper(element, state) - } - } - console.warn(`No Route matches for ${JSON.stringify(state)}`) - return null + const [state, send] = useMachine(machine, { + interpreterOptions: { + logger: console.log.bind('XSTATE:'), + }, + }) + + channel.setMachineSend(send) + + // event bus listener + React.useEffect(messageBusReceiver, []) + + const childArray = React.Children.toArray(children) + for (const child of childArray) { + const { path } = child.props + let pathMatch + if (typeof path === 'string') { + pathMatch = state.matches(path) + } else if (Array.isArray(path)) { + pathMatch = path.some(p => state.matches(p)) + } else { + throw new Error(`Invalid route path ${JSON.stringify(path)}`) + } + if (pathMatch) { + const element = React.cloneElement(child.props.children, { send, context: state.context }) + return debuggerWrapper(element, state) + } + } + console.warn(`No Route matches for ${JSON.stringify(state)}`) + return null } Router.Route = Route diff --git a/web-app/src/components/StepHelp/index.tsx b/web-app/src/components/StepHelp/index.tsx index b972c5b1..2e959da0 100644 --- a/web-app/src/components/StepHelp/index.tsx +++ b/web-app/src/components/StepHelp/index.tsx @@ -3,51 +3,51 @@ import { Balloon } from '@alifd/next' import Button from '../Button' const styles = { - iconButton: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - width: 30, - height: 30, - fontSize: 18, - color: 'grey', - }, - balloonTitle: { - marginTop: 0, - textAlign: 'center' as 'center', - }, - balloonOptions: { - display: 'flex', - justifyContent: 'center', - }, + iconButton: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + width: 30, + height: 30, + fontSize: 18, + color: 'grey', + }, + balloonTitle: { + marginTop: 0, + textAlign: 'center' as 'center', + }, + balloonOptions: { + display: 'flex', + justifyContent: 'center', + }, } interface Props { - onLoadSolution(): void + onLoadSolution(): void } const StepHelp = (props: Props) => { - // TODO: extract or replace load solution - const [loadedSolution, setLoadedSolution] = React.useState() - const onClickHandler = () => { - if (!loadedSolution) { - setLoadedSolution(true) - props.onLoadSolution() - } - } - const promptLeft = - return ( - -
-

Stuck?

-
- -
-
-
- ) + // TODO: extract or replace load solution + const [loadedSolution, setLoadedSolution] = React.useState() + const onClickHandler = () => { + if (!loadedSolution) { + setLoadedSolution(true) + props.onLoadSolution() + } + } + const promptLeft = + return ( + +
+

Stuck?

+
+ +
+
+
+ ) } export default StepHelp diff --git a/web-app/src/components/Workspace/index.tsx b/web-app/src/components/Workspace/index.tsx index 5e0ac57f..06131a79 100644 --- a/web-app/src/components/Workspace/index.tsx +++ b/web-app/src/components/Workspace/index.tsx @@ -1,31 +1,31 @@ import * as React from 'react' interface Props { - children: React.ReactElement + children: React.ReactElement } const resize = () => ({ - minWidth: window.innerWidth - 20, - minHeight: window.innerHeight - 20, + minWidth: window.innerWidth - 20, + minHeight: window.innerHeight - 20, }) const Workspace = ({ children }: Props) => { - const [dimensions, setDimensions] = React.useState(resize()) + const [dimensions, setDimensions] = React.useState(resize()) - // solution for windows getting off size - React.useEffect(() => { - setDimensions(resize()) - }, [window.innerHeight, window.innerWidth]) + // solution for windows getting off size + React.useEffect(() => { + setDimensions(resize()) + }, [window.innerHeight, window.innerWidth]) - const styles = { - page: { - display: 'flex' as 'flex', - margin: 0, - backgroundColor: 'white', - }, - } + const styles = { + page: { + display: 'flex' as 'flex', + margin: 0, + backgroundColor: 'white', + }, + } - return
{children}
+ return
{children}
} export default Workspace diff --git a/web-app/src/containers/Continue/index.tsx b/web-app/src/containers/Continue/index.tsx index 37955db9..7b4fc4a8 100644 --- a/web-app/src/containers/Continue/index.tsx +++ b/web-app/src/containers/Continue/index.tsx @@ -5,51 +5,51 @@ import * as CR from 'typings' import * as G from 'typings/graphql' const styles = { - page: { - width: '100%', - }, + page: { + width: '100%', + }, } interface Props { - tutorial: G.Tutorial - onContinue(): void - onNew(): void + tutorial: G.Tutorial + onContinue(): void + onNew(): void } export const ContinuePage = (props: Props) => ( -
-

Continue

- -
-

{props.tutorial.version.summary.title}

-

{props.tutorial.version.summary.description}

- -
-
- -
-

Start a New Tutorial

- -
-
-
+
+

Continue

+ +
+

{props.tutorial.version.summary.title}

+

{props.tutorial.version.summary.description}

+ +
+
+ +
+

Start a New Tutorial

+ +
+
+
) interface ContainerProps { - context: CR.MachineContext - send(action: CR.Action | string): void + context: CR.MachineContext + send(action: CR.Action | string): void } const ContinuePageContainer = ({ context, send }: ContainerProps) => { - const { tutorial } = context + const { tutorial } = context - if (!tutorial) { - throw new Error('Tutorial not found') - } + if (!tutorial) { + throw new Error('Tutorial not found') + } - return ( - send('TUTORIAL_START')} onNew={() => send('TUTORIAL_SELECT')} /> - ) + return ( + send('TUTORIAL_START')} onNew={() => send('TUTORIAL_SELECT')} /> + ) } export default ContinuePageContainer diff --git a/web-app/src/containers/LoadingPage.tsx b/web-app/src/containers/LoadingPage.tsx index 46a0f267..a9034be8 100644 --- a/web-app/src/containers/LoadingPage.tsx +++ b/web-app/src/containers/LoadingPage.tsx @@ -2,22 +2,22 @@ import * as React from 'react' import Loading from '../components/Loading' interface Props { - text: string + text: string } const styles = { - page: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - }, + page: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + }, } const LoadingPage = ({ text }: Props) => ( -
- -
+
+ +
) export default LoadingPage diff --git a/web-app/src/containers/New/NewPage.tsx b/web-app/src/containers/New/NewPage.tsx index fc60cb6e..0aeb52ef 100644 --- a/web-app/src/containers/New/NewPage.tsx +++ b/web-app/src/containers/New/NewPage.tsx @@ -3,37 +3,37 @@ import * as G from 'typings/graphql' import TutorialList from './TutorialList' const styles = { - page: { - width: '100%', - }, - header: { - height: '36px', - backgroundColor: '#EBEBEB', - fontSize: '16px', - lineHeight: '16px', - padding: '10px 1rem', - }, - banner: { - height: '50px', - fontSize: '1rem', - padding: '1rem', - }, + page: { + width: '100%', + }, + header: { + height: '36px', + backgroundColor: '#EBEBEB', + fontSize: '16px', + lineHeight: '16px', + padding: '10px 1rem', + }, + banner: { + height: '50px', + fontSize: '1rem', + padding: '1rem', + }, } interface Props { - tutorialList: G.Tutorial[] + tutorialList: G.Tutorial[] } const NewPage = (props: Props) => ( -
-
- CodeRoad -
-
- Select a Tutorial to Start -
- -
+
+
+ CodeRoad +
+
+ Select a Tutorial to Start +
+ +
) export default NewPage diff --git a/web-app/src/containers/New/TutorialList/TutorialItem.tsx b/web-app/src/containers/New/TutorialList/TutorialItem.tsx index e6dd6d7a..195cb1cc 100644 --- a/web-app/src/containers/New/TutorialList/TutorialItem.tsx +++ b/web-app/src/containers/New/TutorialList/TutorialItem.tsx @@ -2,47 +2,47 @@ import * as React from 'react' import Card from '../../../components/Card' const styles = { - card: { - cursor: 'pointer', - }, - languages: { - display: 'flex' as 'flex', - flexDirection: 'row' as 'row', - alignItems: 'center' as 'center', - justifyContent: 'flex-end' as 'flex-end', - width: '100%', - }, - languageIcon: { - width: 30, - height: 30, - }, + card: { + cursor: 'pointer', + }, + languages: { + display: 'flex' as 'flex', + flexDirection: 'row' as 'row', + alignItems: 'center' as 'center', + justifyContent: 'flex-end' as 'flex-end', + width: '100%', + }, + languageIcon: { + width: 30, + height: 30, + }, } interface Props { - title?: string - description?: string - onSelect(): void + title?: string + description?: string + onSelect(): void } // icons from https://konpa.github.io/devicon/ const LanguageIcon = () => ( - - - - + + + + ) const TutorialItem = (props: Props) => ( - -

{props.title || 'Title'}

-

{props.description || 'Description'}

-
- -
-
+ +

{props.title || 'Title'}

+

{props.description || 'Description'}

+
+ +
+
) export default TutorialItem diff --git a/web-app/src/containers/New/TutorialList/index.tsx b/web-app/src/containers/New/TutorialList/index.tsx index 3ee76a40..2322fad6 100644 --- a/web-app/src/containers/New/TutorialList/index.tsx +++ b/web-app/src/containers/New/TutorialList/index.tsx @@ -5,30 +5,30 @@ import TutorialItem from './TutorialItem' import channel from '../../../services/channel' interface Props { - tutorialList: G.Tutorial[] + tutorialList: G.Tutorial[] } const TutorialList = (props: Props) => { - const onSelect = (tutorial: G.Tutorial) => { - channel.machineSend({ - type: 'TUTORIAL_START', - payload: { - tutorial, - }, - }) - } - return ( -
- {props.tutorialList.map((tutorial: G.Tutorial) => ( - onSelect(tutorial)} - title={tutorial.version.summary.title || ''} - description={tutorial.version.summary.description || ''} - /> - ))} -
- ) + const onSelect = (tutorial: G.Tutorial) => { + channel.machineSend({ + type: 'TUTORIAL_START', + payload: { + tutorial, + }, + }) + } + return ( +
+ {props.tutorialList.map((tutorial: G.Tutorial) => ( + onSelect(tutorial)} + title={tutorial.version.summary.title || ''} + description={tutorial.version.summary.description || ''} + /> + ))} +
+ ) } export default TutorialList diff --git a/web-app/src/containers/New/index.tsx b/web-app/src/containers/New/index.tsx index 146e35e7..08efd229 100644 --- a/web-app/src/containers/New/index.tsx +++ b/web-app/src/containers/New/index.tsx @@ -11,32 +11,32 @@ import ErrorView from '../../components/Error' const Loading = () => interface ContainerProps { - send(action: CR.Action): void + send(action: CR.Action): void } interface TutorialsData { - tutorials: G.Tutorial[] + tutorials: G.Tutorial[] } const NewPageContainer = (props: ContainerProps) => { - const { data, loading, error } = useQuery(queryTutorials) - if (loading) { - return - } - - if (error) { - return - } - - if (!data) { - return null - } - - return ( - - - - ) + const { data, loading, error } = useQuery(queryTutorials) + if (loading) { + return + } + + if (error) { + return + } + + if (!data) { + return null + } + + return ( + + + + ) } export default NewPageContainer diff --git a/web-app/src/containers/Overview/OverviewPage.tsx b/web-app/src/containers/Overview/OverviewPage.tsx index 8abd232a..7da0aacd 100644 --- a/web-app/src/containers/Overview/OverviewPage.tsx +++ b/web-app/src/containers/Overview/OverviewPage.tsx @@ -5,82 +5,82 @@ import * as G from 'typings/graphql' import Markdown from '../../components/Markdown' const styles = { - page: { - width: '100%', - }, - summary: { - padding: '0rem 1rem 1rem 1rem', - }, - title: { - fontWeight: 'bold' as 'bold', - }, - description: { - fontSize: '1rem', - }, - header: { - height: '36px', - backgroundColor: '#EBEBEB', - fontSize: '16px', - lineHeight: '16px', - padding: '10px 1rem', - }, - levelList: { - padding: '0rem 1rem', - }, - options: { - display: 'flex' as 'flex', - flexDirection: 'row' as 'row', - alignItems: 'center' as 'center', - justifyContent: 'flex-end' as 'flex-end', - position: 'absolute' as 'absolute', - bottom: 0, - height: '50px', - padding: '1rem', - paddingRight: '2rem', - backgroundColor: 'black', - width: '100%', - }, + page: { + width: '100%', + }, + summary: { + padding: '0rem 1rem 1rem 1rem', + }, + title: { + fontWeight: 'bold' as 'bold', + }, + description: { + fontSize: '1rem', + }, + header: { + height: '36px', + backgroundColor: '#EBEBEB', + fontSize: '16px', + lineHeight: '16px', + padding: '10px 1rem', + }, + levelList: { + padding: '0rem 1rem', + }, + options: { + display: 'flex' as 'flex', + flexDirection: 'row' as 'row', + alignItems: 'center' as 'center', + justifyContent: 'flex-end' as 'flex-end', + position: 'absolute' as 'absolute', + bottom: 0, + height: '50px', + padding: '1rem', + paddingRight: '2rem', + backgroundColor: 'black', + width: '100%', + }, } interface Props { - title: string - description: string - levels: G.Level[] - onNext(): void + title: string + description: string + levels: G.Level[] + onNext(): void } const Summary = ({ title, description, levels, onNext }: Props) => ( -
-
- CodeRoad -
-
-

{title}

- {description} -
-
-
- Levels -
-
- {levels.map((level: G.Level, index: number) => ( -
-

- {index + 1}. {level.title} -

-
{level.description}
-
- ))} -
-
+
+
+ CodeRoad +
+
+

{title}

+ {description} +
+
+
+ Levels +
+
+ {levels.map((level: G.Level, index: number) => ( +
+

+ {index + 1}. {level.title} +

+
{level.description}
+
+ ))} +
+
-
- {/* TODO: Add back button */} - -
-
+
+ {/* TODO: Add back button */} + +
+
) export default Summary diff --git a/web-app/src/containers/Overview/index.tsx b/web-app/src/containers/Overview/index.tsx index c672e10d..ac3e6ec4 100644 --- a/web-app/src/containers/Overview/index.tsx +++ b/web-app/src/containers/Overview/index.tsx @@ -8,57 +8,57 @@ import OverviewPage from './OverviewPage' import ErrorView from '../../components/Error' interface PageProps { - context: CR.MachineContext - send(action: CR.Action): void + context: CR.MachineContext + send(action: CR.Action): void } interface TutorialData { - tutorial: G.Tutorial + tutorial: G.Tutorial } interface TutorialDataVariables { - tutorialId: string - version: string + tutorialId: string + version: string } const Overview = (props: PageProps) => { - const { tutorial } = props.context + const { tutorial } = props.context - if (!tutorial) { - throw new Error('Tutorial not found in summary page') - } - const { loading, error, data } = useQuery(queryTutorial, { - fetchPolicy: 'network-only', // to ensure latest - variables: { - tutorialId: tutorial.id, - version: tutorial.version.version, - }, - }) + if (!tutorial) { + throw new Error('Tutorial not found in summary page') + } + const { loading, error, data } = useQuery(queryTutorial, { + fetchPolicy: 'network-only', // to ensure latest + variables: { + tutorialId: tutorial.id, + version: tutorial.version.version, + }, + }) - if (loading) { - return
Loading Summary...
- } + if (loading) { + return
Loading Summary...
+ } - if (error) { - return - } + if (error) { + return + } - if (!data) { - return null - } + if (!data) { + return null + } - const onNext = () => - props.send({ - type: 'LOAD_TUTORIAL', - payload: { - tutorial: data.tutorial, - }, - }) + const onNext = () => + props.send({ + type: 'LOAD_TUTORIAL', + payload: { + tutorial: data.tutorial, + }, + }) - const { title, description } = data.tutorial.version.summary - const { levels } = data.tutorial.version.data + const { title, description } = data.tutorial.version.summary + const { levels } = data.tutorial.version.data - return + return } export default Overview diff --git a/web-app/src/containers/Tutorial/CompletedPage.tsx b/web-app/src/containers/Tutorial/CompletedPage.tsx index 36a6dba3..9da94ce6 100644 --- a/web-app/src/containers/Tutorial/CompletedPage.tsx +++ b/web-app/src/containers/Tutorial/CompletedPage.tsx @@ -3,28 +3,28 @@ import Button from '../../components/Button' import * as CR from 'typings' const styles = { - options: { - padding: '0rem 1rem', - }, + options: { + padding: '0rem 1rem', + }, } interface Props { - context: CR.MachineContext - send(action: CR.Action | string): void + context: CR.MachineContext + send(action: CR.Action | string): void } const CompletedPage = (props: Props) => { - const selectNewTutorial = () => { - props.send('SELECT_TUTORIAL') - } - return ( -
-

Tutorial Complete

-
- -
-
- ) + const selectNewTutorial = () => { + props.send('SELECT_TUTORIAL') + } + return ( +
+

Tutorial Complete

+
+ +
+
+ ) } export default CompletedPage diff --git a/web-app/src/containers/Tutorial/LevelPage/Level/Step/index.tsx b/web-app/src/containers/Tutorial/LevelPage/Level/Step/index.tsx index f8f336b1..0d75eba6 100644 --- a/web-app/src/containers/Tutorial/LevelPage/Level/Step/index.tsx +++ b/web-app/src/containers/Tutorial/LevelPage/Level/Step/index.tsx @@ -5,53 +5,53 @@ import Markdown from '../../../../../components/Markdown' import StepHelp from '../../../../../components/StepHelp' interface Props { - order: number - content: string - status: T.ProgressStatus - onLoadSolution(): void + order: number + content: string + status: T.ProgressStatus + onLoadSolution(): void } const styles = { - card: { - display: 'grid', - gridTemplateColumns: '25px 1fr', - padding: '1rem 1rem 1rem 0.2rem', - }, - content: { - margin: 0, - }, - options: { - display: 'flex' as 'flex', - flexDirection: 'row' as 'row', - justifyContent: 'flex-end', - alignItems: 'center' as 'center', - padding: '0.5rem', - }, + card: { + display: 'grid', + gridTemplateColumns: '25px 1fr', + padding: '1rem 1rem 1rem 0.2rem', + }, + content: { + margin: 0, + }, + options: { + display: 'flex' as 'flex', + flexDirection: 'row' as 'row', + justifyContent: 'flex-end', + alignItems: 'center' as 'center', + padding: '0.5rem', + }, } const Step = (props: Props) => { - const showStep = props.status !== 'INCOMPLETE' - if (!showStep) { - return null - } - const showLoadSolution = props.status === 'ACTIVE' - return ( -
-
-
- -
-
- {props.content || ''} -
-
- {showLoadSolution && ( -
- -
- )} -
- ) + const showStep = props.status !== 'INCOMPLETE' + if (!showStep) { + return null + } + const showLoadSolution = props.status === 'ACTIVE' + return ( +
+
+
+ +
+
+ {props.content || ''} +
+
+ {showLoadSolution && ( +
+ +
+ )} +
+ ) } export default Step diff --git a/web-app/src/containers/Tutorial/LevelPage/Level/index.tsx b/web-app/src/containers/Tutorial/LevelPage/Level/index.tsx index 4bea734b..dfdc58f2 100644 --- a/web-app/src/containers/Tutorial/LevelPage/Level/index.tsx +++ b/web-app/src/containers/Tutorial/LevelPage/Level/index.tsx @@ -7,102 +7,102 @@ import Button from '../../../../components/Button' import Markdown from '../../../../components/Markdown' const styles = { - card: { - padding: 0, - width: '100%', - }, - header: { - height: '36px', - backgroundColor: '#EBEBEB', - fontSize: '16px', - lineHeight: '16px', - padding: '10px 1rem', - }, - content: { - padding: '0rem 1rem', - paddingBottom: '1rem', - }, - options: { - padding: '0rem 1rem', - }, - steps: { - padding: '1rem 16px', - }, - title: { - fontSize: '1.2rem', - fontWeight: 'bold' as 'bold', - lineHeight: '1.2rem', - }, - footer: { - height: '36px', - backgroundColor: 'black', - fontSize: '16px', - lineHeight: '16px', - padding: '10px 1rem', - color: 'white', - position: 'absolute' as 'absolute', - bottom: 0, - width: '100%', - }, + card: { + padding: 0, + width: '100%', + }, + header: { + height: '36px', + backgroundColor: '#EBEBEB', + fontSize: '16px', + lineHeight: '16px', + padding: '10px 1rem', + }, + content: { + padding: '0rem 1rem', + paddingBottom: '1rem', + }, + options: { + padding: '0rem 1rem', + }, + steps: { + padding: '1rem 16px', + }, + title: { + fontSize: '1.2rem', + fontWeight: 'bold' as 'bold', + lineHeight: '1.2rem', + }, + footer: { + height: '36px', + backgroundColor: 'black', + fontSize: '16px', + lineHeight: '16px', + padding: '10px 1rem', + color: 'white', + position: 'absolute' as 'absolute', + bottom: 0, + width: '100%', + }, } interface Props { - level: G.Level & { status: T.ProgressStatus; index: number; steps: Array } - onContinue(): void - onLoadSolution(): void + level: G.Level & { status: T.ProgressStatus; index: number; steps: Array } + onContinue(): void + onLoadSolution(): void } const Level = ({ level, onContinue, onLoadSolution }: Props) => { - if (!level.steps) { - throw new Error('No Stage steps found') - } + if (!level.steps) { + throw new Error('No Stage steps found') + } - return ( -
-
-
- Learn -
-
-

{level.title}

- {level.content || ''} -
-
+ return ( +
+
+
+ Learn +
+
+

{level.title}

+ {level.content || ''} +
+
-
-
Tasks
-
- {level.steps.map((step: (G.Step & { status: T.ProgressStatus }) | null, index: number) => { - if (!step) { - return null - } - return ( - - ) - })} -
-
+
+
Tasks
+
+ {level.steps.map((step: (G.Step & { status: T.ProgressStatus }) | null, index: number) => { + if (!step) { + return null + } + return ( + + ) + })} +
+
- {level.status === 'COMPLETE' && ( -
- -
- )} -
-
- - {level.index ? `${level.index.toString()}.` : ''} {level.title} - -
-
-
- ) + {level.status === 'COMPLETE' && ( +
+ +
+ )} +
+
+ + {level.index ? `${level.index.toString()}.` : ''} {level.title} + +
+
+
+ ) } export default Level diff --git a/web-app/src/containers/Tutorial/LevelPage/index.tsx b/web-app/src/containers/Tutorial/LevelPage/index.tsx index b9d4eb1b..ecd089c2 100644 --- a/web-app/src/containers/Tutorial/LevelPage/index.tsx +++ b/web-app/src/containers/Tutorial/LevelPage/index.tsx @@ -6,50 +6,50 @@ import * as selectors from '../../../services/selectors' import Level from './Level' interface PageProps { - context: T.MachineContext - send(action: T.Action): void + context: T.MachineContext + send(action: T.Action): void } const LevelSummaryPageContainer = (props: PageProps) => { - const { position, progress } = props.context - - const version = selectors.currentVersion(props.context) - const levelData: G.Level = selectors.currentLevel(props.context) - - const onContinue = (): void => { - props.send({ - type: 'LEVEL_NEXT', - payload: { - LevelId: position.levelId, - }, - }) - } - - const onLoadSolution = (): void => { - props.send({ type: 'STEP_SOLUTION_LOAD' }) - } - - const level: G.Level & { - status: T.ProgressStatus - index: number - steps: Array - } = { - ...levelData, - index: version.data.levels.findIndex((l: G.Level) => l.id === position.levelId), - status: progress.levels[position.levelId] ? 'COMPLETE' : 'ACTIVE', - steps: levelData.steps.map((step: G.Step) => { - // label step status for step component - let status: T.ProgressStatus = 'INCOMPLETE' - if (progress.steps[step.id]) { - status = 'COMPLETE' - } else if (step.id === position.stepId) { - status = 'ACTIVE' - } - return { ...step, status } - }), - } - - return + const { position, progress } = props.context + + const version = selectors.currentVersion(props.context) + const levelData: G.Level = selectors.currentLevel(props.context) + + const onContinue = (): void => { + props.send({ + type: 'LEVEL_NEXT', + payload: { + LevelId: position.levelId, + }, + }) + } + + const onLoadSolution = (): void => { + props.send({ type: 'STEP_SOLUTION_LOAD' }) + } + + const level: G.Level & { + status: T.ProgressStatus + index: number + steps: Array + } = { + ...levelData, + index: version.data.levels.findIndex((l: G.Level) => l.id === position.levelId), + status: progress.levels[position.levelId] ? 'COMPLETE' : 'ACTIVE', + steps: levelData.steps.map((step: G.Step) => { + // label step status for step component + let status: T.ProgressStatus = 'INCOMPLETE' + if (progress.steps[step.id]) { + status = 'COMPLETE' + } else if (step.id === position.stepId) { + status = 'ACTIVE' + } + return { ...step, status } + }), + } + + return } export default LevelSummaryPageContainer diff --git a/web-app/src/services/apollo/auth.ts b/web-app/src/services/apollo/auth.ts index a1637dc3..f6b001f9 100644 --- a/web-app/src/services/apollo/auth.ts +++ b/web-app/src/services/apollo/auth.ts @@ -1,17 +1,17 @@ -import {Operation} from 'apollo-boost' +import { Operation } from 'apollo-boost' let authToken: string | null = null export const setAuthToken = (token: string | null) => { - authToken = token + authToken = token } export const authorizeHeaders = (operation: Operation) => { - if (authToken) { - operation.setContext({ - headers: { - 'Authorization': authToken - } - }) - } + if (authToken) { + operation.setContext({ + headers: { + Authorization: authToken, + }, + }) + } } diff --git a/web-app/src/services/apollo/index.ts b/web-app/src/services/apollo/index.ts index b5d8708d..ec433a21 100644 --- a/web-app/src/services/apollo/index.ts +++ b/web-app/src/services/apollo/index.ts @@ -1,12 +1,12 @@ -import ApolloClient, {InMemoryCache} from 'apollo-boost' +import ApolloClient, { InMemoryCache } from 'apollo-boost' -import {authorizeHeaders} from './auth' +import { authorizeHeaders } from './auth' export const cache = new InMemoryCache() const client = new ApolloClient({ - uri: process.env.REACT_APP_GQL_URI, - request: authorizeHeaders, - cache, + uri: process.env.REACT_APP_GQL_URI, + request: authorizeHeaders, + cache, }) -export default client \ No newline at end of file +export default client diff --git a/web-app/src/services/apollo/mutations/authenticate.ts b/web-app/src/services/apollo/mutations/authenticate.ts index b5a1a243..d90d425a 100644 --- a/web-app/src/services/apollo/mutations/authenticate.ts +++ b/web-app/src/services/apollo/mutations/authenticate.ts @@ -1,23 +1,15 @@ -import {gql} from 'apollo-boost' +import { gql } from 'apollo-boost' export default gql` - mutation Authenticate( - $machineId: String!, - $sessionId: String!, - $editor: Editor! - ) { - editorLogin(input: { - machineId: $machineId, - sessionId: $sessionId, - editor: $editor - }) { - token - user { - id - name - email - avatarUrl - } - } - } + mutation Authenticate($machineId: String!, $sessionId: String!, $editor: Editor!) { + editorLogin(input: { machineId: $machineId, sessionId: $sessionId, editor: $editor }) { + token + user { + id + name + email + avatarUrl + } + } + } ` diff --git a/web-app/src/services/apollo/queries/tutorial.ts b/web-app/src/services/apollo/queries/tutorial.ts index c339f914..64d43efa 100644 --- a/web-app/src/services/apollo/queries/tutorial.ts +++ b/web-app/src/services/apollo/queries/tutorial.ts @@ -1,60 +1,60 @@ -import {gql} from 'apollo-boost' +import { gql } from 'apollo-boost' export default gql` query getTutorial($tutorialId: ID!, $version: String) { - tutorial(id: $tutorialId) { - id - version (version: $version) { - version - summary { - title - description - } - data { - config { - testRunner { - command - fileFormats - } - repo { - uri - branch - } - } - init { - setup { - commits - commands - } - } - levels { - id - title - description - content - setup { - commits - commands - files - } - steps { - id - content - setup { - commits - commands - files - listeners - } - solution { - commits - commands - files - } - } - } - } - } - } - } + tutorial(id: $tutorialId) { + id + version(version: $version) { + version + summary { + title + description + } + data { + config { + testRunner { + command + fileFormats + } + repo { + uri + branch + } + } + init { + setup { + commits + commands + } + } + levels { + id + title + description + content + setup { + commits + commands + files + } + steps { + id + content + setup { + commits + commands + files + listeners + } + solution { + commits + commands + files + } + } + } + } + } + } + } ` diff --git a/web-app/src/services/apollo/queries/tutorials.ts b/web-app/src/services/apollo/queries/tutorials.ts index a91fced4..fa7891f2 100644 --- a/web-app/src/services/apollo/queries/tutorials.ts +++ b/web-app/src/services/apollo/queries/tutorials.ts @@ -1,4 +1,4 @@ -import {gql} from 'apollo-boost' +import { gql } from 'apollo-boost' export default gql` query getTutorials { @@ -7,16 +7,16 @@ export default gql` version { version createdBy { - id + id } createdAt updatedBy { - id + id } updatedAt summary { - title - description + title + description } } } diff --git a/web-app/src/services/channel/index.ts b/web-app/src/services/channel/index.ts index 18d0c700..dea59cf8 100644 --- a/web-app/src/services/channel/index.ts +++ b/web-app/src/services/channel/index.ts @@ -1,60 +1,66 @@ -import {Action} from 'typings' +import { Action } from 'typings' -declare var acquireVsCodeApi: any +declare let acquireVsCodeApi: any interface ReceivedEvent { - data: Action + data: Action } class Channel { - constructor() { - // setup mock if browser only - // @ts-ignore - if (!window.acquireVsCodeApi) { - // @ts-ignore - require('./mock') - } - - // Loads VSCode webview connection with editor - const editor = acquireVsCodeApi() - - this.editorSend = editor.postMessage - } - public machineSend = (action: Action | string) => { /* */} - public editorSend = (action: Action) => { /* */} - - public setMachineSend = (send: any) => { - this.machineSend = send - } - public receive = (event: ReceivedEvent) => { - // NOTE: must call event.data, cannot destructure. VSCode acts odd - const action = event.data - - // @ts-ignore // ignore browser events from plugins - if (action.source) {return } - - console.log(`CLIENT RECEIVE: ${action.type}`, action) - - // messages from core - switch (action.type) { - case 'ENV_LOAD': - case 'AUTHENTICATED': - case 'TUTORIAL_LOADED': - case 'NEW_TUTORIAL': - case 'TUTORIAL_CONFIGURED': - case 'CONTINUE_TUTORIAL': - case 'TEST_PASS': - case 'TEST_FAIL': - case 'TEST_RUNNING': - case 'TEST_ERROR': - this.machineSend(action) - return - default: - if (action.type) { - console.warn(`Unknown received action ${action.type}`, action) - } - } - } + constructor() { + // setup mock if browser only + // @ts-ignore + if (!window.acquireVsCodeApi) { + // @ts-ignore + require('./mock') + } + + // Loads VSCode webview connection with editor + const editor = acquireVsCodeApi() + + this.editorSend = editor.postMessage + } + public machineSend = (action: Action | string) => { + /* */ + } + public editorSend = (action: Action) => { + /* */ + } + + public setMachineSend = (send: any) => { + this.machineSend = send + } + public receive = (event: ReceivedEvent) => { + // NOTE: must call event.data, cannot destructure. VSCode acts odd + const action = event.data + + // @ts-ignore // ignore browser events from plugins + if (action.source) { + return + } + + console.log(`CLIENT RECEIVE: ${action.type}`, action) + + // messages from core + switch (action.type) { + case 'ENV_LOAD': + case 'AUTHENTICATED': + case 'TUTORIAL_LOADED': + case 'NEW_TUTORIAL': + case 'TUTORIAL_CONFIGURED': + case 'CONTINUE_TUTORIAL': + case 'TEST_PASS': + case 'TEST_FAIL': + case 'TEST_RUNNING': + case 'TEST_ERROR': + this.machineSend(action) + return + default: + if (action.type) { + console.warn(`Unknown received action ${action.type}`, action) + } + } + } } export default new Channel() diff --git a/web-app/src/services/channel/mock.ts b/web-app/src/services/channel/mock.ts index 30044f23..43ab9dfa 100644 --- a/web-app/src/services/channel/mock.ts +++ b/web-app/src/services/channel/mock.ts @@ -1,33 +1,32 @@ -import {Action} from 'typings' +import { Action } from 'typings' import channel from './index' const createReceiveEvent = (action: Action) => ({ - data: action + data: action, }) // mock vscode from client side development // @ts-ignore window.acquireVsCodeApi = () => ({ - postMessage(action: Action) { - - switch (action.type) { - case 'TUTORIAL_START': - return setTimeout(() => { - const receiveAction: Action = { - type: 'TUTORIAL_LOADED' - } - channel.receive(createReceiveEvent(receiveAction)) - }, 1000) - case 'TEST_RUN': - return setTimeout(() => { - const receiveAction: Action = { - type: 'TEST_PASS', - payload: action.payload, - } - channel.receive(createReceiveEvent(receiveAction)) - }, 1000) - default: - console.warn(`${action.type} not found in post message mock`) - } - } -}) \ No newline at end of file + postMessage(action: Action) { + switch (action.type) { + case 'TUTORIAL_START': + return setTimeout(() => { + const receiveAction: Action = { + type: 'TUTORIAL_LOADED', + } + channel.receive(createReceiveEvent(receiveAction)) + }, 1000) + case 'TEST_RUN': + return setTimeout(() => { + const receiveAction: Action = { + type: 'TEST_PASS', + payload: action.payload, + } + channel.receive(createReceiveEvent(receiveAction)) + }, 1000) + default: + console.warn(`${action.type} not found in post message mock`) + } + }, +}) diff --git a/web-app/src/services/channel/receiver.ts b/web-app/src/services/channel/receiver.ts index b4abe3d0..9cf20789 100644 --- a/web-app/src/services/channel/receiver.ts +++ b/web-app/src/services/channel/receiver.ts @@ -1,12 +1,12 @@ import channel from './index' const messageBusReceiver = () => { - // update state based on response from editor - const listener = 'message' - window.addEventListener(listener, channel.receive) - return () => { - window.removeEventListener(listener, channel.receive) - } + // update state based on response from editor + const listener = 'message' + window.addEventListener(listener, channel.receive) + return () => { + window.removeEventListener(listener, channel.receive) + } } -export default messageBusReceiver \ No newline at end of file +export default messageBusReceiver diff --git a/web-app/src/services/selectors/index.ts b/web-app/src/services/selectors/index.ts index 315726a6..585517eb 100644 --- a/web-app/src/services/selectors/index.ts +++ b/web-app/src/services/selectors/index.ts @@ -1,3 +1,3 @@ export * from './tutorial' export * from './position' -export * from './progress' \ No newline at end of file +export * from './progress' diff --git a/web-app/src/services/selectors/position.ts b/web-app/src/services/selectors/position.ts index 0b02131e..29dc5dd5 100644 --- a/web-app/src/services/selectors/position.ts +++ b/web-app/src/services/selectors/position.ts @@ -1,21 +1,18 @@ -import {createSelector} from 'reselect' +import { createSelector } from 'reselect' import * as G from 'typings/graphql' import * as CR from 'typings' import * as tutorial from './tutorial' export const defaultPosition = () => ({ - levelId: '', - stepId: '' + levelId: '', + stepId: '', }) -export const initialPosition = createSelector( - tutorial.currentVersion, - (version: G.TutorialVersion) => { - const level = version.data.levels[0] - const position: CR.Position = { - levelId: level.id, - stepId: level.steps[0].id, - } - return position - } -) +export const initialPosition = createSelector(tutorial.currentVersion, (version: G.TutorialVersion) => { + const level = version.data.levels[0] + const position: CR.Position = { + levelId: level.id, + stepId: level.steps[0].id, + } + return position +}) diff --git a/web-app/src/services/selectors/progress.ts b/web-app/src/services/selectors/progress.ts index 60aa9bbf..4573c51a 100644 --- a/web-app/src/services/selectors/progress.ts +++ b/web-app/src/services/selectors/progress.ts @@ -1,5 +1,5 @@ export const defaultProgress = () => ({ - levels: {}, - steps: {}, - complete: false -}) \ No newline at end of file + levels: {}, + steps: {}, + complete: false, +}) diff --git a/web-app/src/services/selectors/tutorial.ts b/web-app/src/services/selectors/tutorial.ts index f34de7d8..08c707ad 100644 --- a/web-app/src/services/selectors/tutorial.ts +++ b/web-app/src/services/selectors/tutorial.ts @@ -1,47 +1,48 @@ -import {MachineContext} from 'typings' +import { MachineContext } from 'typings' import * as G from 'typings/graphql' -import {createSelector} from 'reselect' +import { createSelector } from 'reselect' -export const currentTutorial = ({tutorial}: MachineContext): G.Tutorial => { - if (!tutorial) { - throw new Error('Tutorial not found') - } - return tutorial +export const currentTutorial = ({ tutorial }: MachineContext): G.Tutorial => { + if (!tutorial) { + throw new Error('Tutorial not found') + } + return tutorial } -export const currentVersion = createSelector( - currentTutorial, - (tutorial: G.Tutorial) => { - if (!tutorial.version) { - throw new Error('Tutorial version not found') - } - return tutorial.version - }) +export const currentVersion = createSelector(currentTutorial, (tutorial: G.Tutorial) => { + if (!tutorial.version) { + throw new Error('Tutorial version not found') + } + return tutorial.version +}) -export const currentLevel = (context: MachineContext): G.Level => createSelector( - currentVersion, - (version: G.TutorialVersion): G.Level => { - // merge in the updated position - // sent with the test to ensure consistency - const levels: G.Level[] = version.data.levels +export const currentLevel = (context: MachineContext): G.Level => + createSelector( + currentVersion, + (version: G.TutorialVersion): G.Level => { + // merge in the updated position + // sent with the test to ensure consistency + const levels: G.Level[] = version.data.levels - const levelIndex = levels.findIndex((l: G.Level) => l.id === context.position.levelId) - if (levelIndex < 0) { - throw new Error('Level not found when selecting level') - } - const level: G.Level = levels[levelIndex] + const levelIndex = levels.findIndex((l: G.Level) => l.id === context.position.levelId) + if (levelIndex < 0) { + throw new Error('Level not found when selecting level') + } + const level: G.Level = levels[levelIndex] - return level - })(context) + return level + }, + )(context) -export const currentStep = (context: MachineContext): G.Step => createSelector( - currentLevel, - (level: G.Level): G.Step => { - const steps: G.Step[] = level.steps - const step: G.Step | undefined = steps.find((s: G.Step) => s.id === context.position.stepId) - if (!step) { - throw new Error('No Step found') - } - return step - } -)(context) +export const currentStep = (context: MachineContext): G.Step => + createSelector( + currentLevel, + (level: G.Level): G.Step => { + const steps: G.Step[] = level.steps + const step: G.Step | undefined = steps.find((s: G.Step) => s.id === context.position.stepId) + if (!step) { + throw new Error('No Step found') + } + return step + }, + )(context) diff --git a/web-app/src/services/state/actions/api.ts b/web-app/src/services/state/actions/api.ts index 43878832..6fb730ba 100644 --- a/web-app/src/services/state/actions/api.ts +++ b/web-app/src/services/state/actions/api.ts @@ -2,46 +2,47 @@ import * as CR from 'typings' import * as G from 'typings/graphql' import client from '../../apollo' import authenticateMutation from '../../apollo/mutations/authenticate' -import {setAuthToken} from '../../apollo/auth' +import { setAuthToken } from '../../apollo/auth' import channel from '../../channel' interface AuthenticateData { - editorLogin: { - token: string - user: G.User - } + editorLogin: { + token: string + user: G.User + } } interface AuthenticateVariables { - machineId: string - sessionId: string - editor: 'VSCODE' + machineId: string + sessionId: string + editor: 'VSCODE' } export default { - authenticate: (async (context: CR.MachineContext): Promise => { + authenticate: async (context: CR.MachineContext): Promise => { + const result = await client + .mutate({ + mutation: authenticateMutation, + variables: { + machineId: context.env.machineId, + sessionId: context.env.sessionId, + editor: 'VSCODE', + }, + }) + .catch(console.error) - const result = await client.mutate({ - mutation: authenticateMutation, - variables: { - machineId: context.env.machineId, - sessionId: context.env.sessionId, - editor: 'VSCODE', - } - }).catch(console.error) - - if (!result || !result.data) { - // TODO: handle failed authentication - console.error('ERROR: Authentication failed') - return - } - const {token} = result.data.editorLogin - // add token to headers - setAuthToken(token) - // pass authenticated action back to state machine - channel.receive({data: {type: 'AUTHENTICATED'}}) - }), - userTutorialComplete(context: CR.MachineContext) { - console.log('should update user tutorial as complete') - } -} \ No newline at end of file + if (!result || !result.data) { + // TODO: handle failed authentication + console.error('ERROR: Authentication failed') + return + } + const { token } = result.data.editorLogin + // add token to headers + setAuthToken(token) + // pass authenticated action back to state machine + channel.receive({ data: { type: 'AUTHENTICATED' } }) + }, + userTutorialComplete(context: CR.MachineContext) { + console.log('should update user tutorial as complete') + }, +} diff --git a/web-app/src/services/state/actions/context.ts b/web-app/src/services/state/actions/context.ts index 853d6be4..fbddd195 100644 --- a/web-app/src/services/state/actions/context.ts +++ b/web-app/src/services/state/actions/context.ts @@ -1,212 +1,214 @@ -import {assign, send} from 'xstate' +import { assign, send } from 'xstate' import * as G from 'typings/graphql' import * as CR from 'typings' import * as selectors from '../../selectors' export default { - setEnv: assign({ - env: (context: CR.MachineContext, event: CR.MachineEvent) => { - return { - ...context.env, - ...event.payload.env - } - } - }), - continueTutorial: assign({ - tutorial: (context: CR.MachineContext, event: CR.MachineEvent) => { - return event.payload.tutorial - }, - progress: (context: CR.MachineContext, event: CR.MachineEvent) => { - return event.payload.progress - }, - position: (context: CR.MachineContext, event: CR.MachineEvent) => { - return event.payload.position - }, - }), - newTutorial: assign({ - tutorial: (context: CR.MachineContext, event: CR.MachineEvent): any => { - return event.payload.tutorial - }, - progress: (): CR.Progress => { - return {levels: {}, steps: {}, complete: false} - } - }), - initTutorial: assign({ - // loads complete tutorial - tutorial: (context: CR.MachineContext, event: CR.MachineEvent): any => { - return event.payload.tutorial - }, - }), - // @ts-ignore - initPosition: assign({ - position: (context: CR.MachineContext, event: CR.MachineEvent): CR.Position => { - const position: CR.Position = selectors.initialPosition(event.payload) - return position - }, - }), - // @ts-ignore - updateStepPosition: assign({ - position: (context: CR.MachineContext, event: CR.MachineEvent): CR.Position => { - - // TODO: calculate from progress - - const {position} = context - // merge in the updated position - // sent with the test to ensure consistency - const level: G.Level = selectors.currentLevel(context) - const steps: G.Step[] = level.steps - - // final step but not completed - if (steps[steps.length - 1].id === position.stepId) { - return position - } - - const stepIndex = steps.findIndex((s: G.Step) => s.id === position.stepId) - - const step: G.Step = steps[stepIndex + 1] - - const nextPosition: CR.Position = { - ...position, - stepId: step.id - } - - return nextPosition - }, - }), - // @ts-ignore - updateLevelPosition: assign({ - position: (context: CR.MachineContext): CR.Position => { - const {position} = context - const version = selectors.currentVersion(context) - // merge in the updated position - // sent with the test to ensure consistency - const levels: G.Level[] = version.data.levels - - const levelIndex = levels.findIndex((l: G.Level) => l.id === position.levelId) - const level: G.Level = levels[levelIndex + 1] - - const nextPosition: CR.Position = { - levelId: level.id, - stepId: level.steps[0].id, - } - - return nextPosition - }, - }), - // @ts-ignore - updateLevelProgress: assign({ - progress: (context: CR.MachineContext, event: CR.MachineEvent): CR.Progress => { - // update progress by tracking completed - const {progress, position} = context - - const levelId: string = position.levelId - - progress.levels[levelId] = true - - return progress - }, - }), - // @ts-ignore - updateStepProgress: assign({ - progress: (context: CR.MachineContext, event: CR.MachineEvent): CR.Progress => { - // update progress by tracking completed - const currentProgress: CR.Progress = context.progress - - const {stepId} = event.payload - - currentProgress.steps[stepId] = true - - return currentProgress - }, - }), - // @ts-ignore - updatePosition: assign({ - position: (context: CR.MachineContext, event: CR.MachineEvent): CR.Progress => { - const {position} = event.payload - return position - } - }), - loadNext: send((context: CR.MachineContext): CR.Action => { - const {position, progress} = context - - const level = selectors.currentLevel(context) - - const steps: G.Step[] = level.steps - - const stepIndex = steps.findIndex((s: G.Step) => s.id === position.stepId) - const stepComplete = progress.steps[position.stepId] - const finalStep = (stepIndex > -1 && stepIndex === steps.length - 1) - const hasNextStep = (!finalStep && !stepComplete) - - - // NEXT STEP - if (hasNextStep) { - const nextPosition = {...position, stepId: steps[stepIndex + 1].id} - return {type: 'NEXT_STEP', payload: {position: nextPosition}} - } - - // has next level? - - if (!context.tutorial) { - throw new Error('Tutorial not found') - } - - const levels = context.tutorial.version.data.levels || [] - const levelIndex = levels.findIndex((l: G.Level) => l.id === position.levelId) - const finalLevel = (levelIndex > -1 && levelIndex === levels.length - 1) - const hasNextLevel = (!finalLevel) - - // NEXT LEVEL - if (hasNextLevel) { - const nextLevel = levels[levelIndex + 1] - const nextPosition = { - levelId: nextLevel.id, - stepId: nextLevel.steps[0].id, - } - return {type: 'NEXT_LEVEL', payload: {position: nextPosition}} - } - - // COMPLETED - return {type: 'COMPLETED'} - }), - stepNext: send((context: CR.MachineContext): CR.Action => { - const {position, progress} = context - - const level: G.Level = selectors.currentLevel(context) - - const {steps} = level - // TODO: verify not -1 - const stepIndex = steps.findIndex((s: G.Step) => s.id === position.stepId) - const finalStep = stepIndex === steps.length - 1 - const stepComplete = progress.steps[position.stepId] - // not final step, or final step but not complete - const hasNextStep = !finalStep || !stepComplete - - if (hasNextStep) { - const nextStep = steps[stepIndex + 1] - return { - type: 'LOAD_NEXT_STEP', - payload: { - step: nextStep - } - } - } else { - return { - type: 'LEVEL_COMPLETE' - } - } - }), - reset: assign({ - tutorial() { - return null - }, - progress(): CR.Progress { - const progress: CR.Progress = selectors.defaultProgress() - return progress - }, - position(): CR.Position { - const position: CR.Position = selectors.defaultPosition() - return position - } - }) -} \ No newline at end of file + setEnv: assign({ + env: (context: CR.MachineContext, event: CR.MachineEvent) => { + return { + ...context.env, + ...event.payload.env, + } + }, + }), + continueTutorial: assign({ + tutorial: (context: CR.MachineContext, event: CR.MachineEvent) => { + return event.payload.tutorial + }, + progress: (context: CR.MachineContext, event: CR.MachineEvent) => { + return event.payload.progress + }, + position: (context: CR.MachineContext, event: CR.MachineEvent) => { + return event.payload.position + }, + }), + newTutorial: assign({ + tutorial: (context: CR.MachineContext, event: CR.MachineEvent): any => { + return event.payload.tutorial + }, + progress: (): CR.Progress => { + return { levels: {}, steps: {}, complete: false } + }, + }), + initTutorial: assign({ + // loads complete tutorial + tutorial: (context: CR.MachineContext, event: CR.MachineEvent): any => { + return event.payload.tutorial + }, + }), + // @ts-ignore + initPosition: assign({ + position: (context: CR.MachineContext, event: CR.MachineEvent): CR.Position => { + const position: CR.Position = selectors.initialPosition(event.payload) + return position + }, + }), + // @ts-ignore + updateStepPosition: assign({ + position: (context: CR.MachineContext, event: CR.MachineEvent): CR.Position => { + // TODO: calculate from progress + + const { position } = context + // merge in the updated position + // sent with the test to ensure consistency + const level: G.Level = selectors.currentLevel(context) + const steps: G.Step[] = level.steps + + // final step but not completed + if (steps[steps.length - 1].id === position.stepId) { + return position + } + + const stepIndex = steps.findIndex((s: G.Step) => s.id === position.stepId) + + const step: G.Step = steps[stepIndex + 1] + + const nextPosition: CR.Position = { + ...position, + stepId: step.id, + } + + return nextPosition + }, + }), + // @ts-ignore + updateLevelPosition: assign({ + position: (context: CR.MachineContext): CR.Position => { + const { position } = context + const version = selectors.currentVersion(context) + // merge in the updated position + // sent with the test to ensure consistency + const levels: G.Level[] = version.data.levels + + const levelIndex = levels.findIndex((l: G.Level) => l.id === position.levelId) + const level: G.Level = levels[levelIndex + 1] + + const nextPosition: CR.Position = { + levelId: level.id, + stepId: level.steps[0].id, + } + + return nextPosition + }, + }), + // @ts-ignore + updateLevelProgress: assign({ + progress: (context: CR.MachineContext, event: CR.MachineEvent): CR.Progress => { + // update progress by tracking completed + const { progress, position } = context + + const levelId: string = position.levelId + + progress.levels[levelId] = true + + return progress + }, + }), + // @ts-ignore + updateStepProgress: assign({ + progress: (context: CR.MachineContext, event: CR.MachineEvent): CR.Progress => { + // update progress by tracking completed + const currentProgress: CR.Progress = context.progress + + const { stepId } = event.payload + + currentProgress.steps[stepId] = true + + return currentProgress + }, + }), + // @ts-ignore + updatePosition: assign({ + position: (context: CR.MachineContext, event: CR.MachineEvent): CR.Progress => { + const { position } = event.payload + return position + }, + }), + loadNext: send( + (context: CR.MachineContext): CR.Action => { + const { position, progress } = context + + const level = selectors.currentLevel(context) + + const steps: G.Step[] = level.steps + + const stepIndex = steps.findIndex((s: G.Step) => s.id === position.stepId) + const stepComplete = progress.steps[position.stepId] + const finalStep = stepIndex > -1 && stepIndex === steps.length - 1 + const hasNextStep = !finalStep && !stepComplete + + // NEXT STEP + if (hasNextStep) { + const nextPosition = { ...position, stepId: steps[stepIndex + 1].id } + return { type: 'NEXT_STEP', payload: { position: nextPosition } } + } + + // has next level? + + if (!context.tutorial) { + throw new Error('Tutorial not found') + } + + const levels = context.tutorial.version.data.levels || [] + const levelIndex = levels.findIndex((l: G.Level) => l.id === position.levelId) + const finalLevel = levelIndex > -1 && levelIndex === levels.length - 1 + const hasNextLevel = !finalLevel + + // NEXT LEVEL + if (hasNextLevel) { + const nextLevel = levels[levelIndex + 1] + const nextPosition = { + levelId: nextLevel.id, + stepId: nextLevel.steps[0].id, + } + return { type: 'NEXT_LEVEL', payload: { position: nextPosition } } + } + + // COMPLETED + return { type: 'COMPLETED' } + }, + ), + stepNext: send( + (context: CR.MachineContext): CR.Action => { + const { position, progress } = context + + const level: G.Level = selectors.currentLevel(context) + + const { steps } = level + // TODO: verify not -1 + const stepIndex = steps.findIndex((s: G.Step) => s.id === position.stepId) + const finalStep = stepIndex === steps.length - 1 + const stepComplete = progress.steps[position.stepId] + // not final step, or final step but not complete + const hasNextStep = !finalStep || !stepComplete + + if (hasNextStep) { + const nextStep = steps[stepIndex + 1] + return { + type: 'LOAD_NEXT_STEP', + payload: { + step: nextStep, + }, + } + } else { + return { + type: 'LEVEL_COMPLETE', + } + } + }, + ), + reset: assign({ + tutorial() { + return null + }, + progress(): CR.Progress { + const progress: CR.Progress = selectors.defaultProgress() + return progress + }, + position(): CR.Position { + const position: CR.Position = selectors.defaultPosition() + return position + }, + }), +} diff --git a/web-app/src/services/state/actions/editor.ts b/web-app/src/services/state/actions/editor.ts index 813d784e..f021272a 100644 --- a/web-app/src/services/state/actions/editor.ts +++ b/web-app/src/services/state/actions/editor.ts @@ -6,103 +6,105 @@ import client from '../../apollo' import tutorialQuery from '../../apollo/queries/tutorial' interface TutorialData { - tutorial: G.Tutorial + tutorial: G.Tutorial } interface TutorialDataVariables { - tutorialId: string - version: string + tutorialId: string + version: string } export default { - loadEnv() { - channel.editorSend({ - type: 'ENV_GET' - }) - }, - loadStoredTutorial() { - // send message to editor to see if there is existing tutorial progress - // in local storage on the editor - channel.editorSend({ - type: 'EDITOR_TUTORIAL_LOAD', - }) - }, - // TODO: syncProgress unused - syncProgress(context: CR.MachineContext) { - // sync progress in editor local storage for persistence - channel.editorSend({ - type: 'EDITOR_SYNC_PROGRESS', - payload: { - progress: context.progress, - } - }) - }, - initializeTutorial(context: CR.MachineContext, event: CR.MachineEvent) { - // setup test runner and git - if (!context.tutorial) { - throw new Error('Tutorial not available to load') - } + loadEnv(): void { + channel.editorSend({ + type: 'ENV_GET', + }) + }, + loadStoredTutorial(): void { + // send message to editor to see if there is existing tutorial progress + // in local storage on the editor + channel.editorSend({ + type: 'EDITOR_TUTORIAL_LOAD', + }) + }, + // TODO: syncProgress unused + syncProgress(context: CR.MachineContext): void { + // sync progress in editor local storage for persistence + channel.editorSend({ + type: 'EDITOR_SYNC_PROGRESS', + payload: { + progress: context.progress, + }, + }) + }, + initializeTutorial(context: CR.MachineContext, event: CR.MachineEvent) { + // setup test runner and git + if (!context.tutorial) { + throw new Error('Tutorial not available to load') + } - client.query({ - query: tutorialQuery, - variables: { - tutorialId: context.tutorial.id, - version: context.tutorial.version.version, - } - }).then((result) => { - if (!result || !result.data || !result.data.tutorial) { - return Promise.reject('No tutorial returned from tutorial config query') - } + client + .query({ + query: tutorialQuery, + variables: { + tutorialId: context.tutorial.id, + version: context.tutorial.version.version, + }, + }) + .then(result => { + if (!result || !result.data || !result.data.tutorial) { + return Promise.reject('No tutorial returned from tutorial config query') + } - channel.editorSend({ - type: 'EDITOR_TUTORIAL_CONFIG', - payload: {tutorial: result.data.tutorial}, - }) - }) - .catch((error: Error) => { - return Promise.reject(`Failed to load tutorial config ${error.message}`) - }) - }, - continueConfig() { - channel.editorSend({ - type: 'EDITOR_TUTORIAL_CONTINUE_CONFIG', - }) - }, - loadLevel(context: CR.MachineContext): void { - const level: G.Level = selectors.currentLevel(context) - if (level.setup) { - // load step actions - channel.editorSend({ - type: 'SETUP_ACTIONS', - payload: level.setup, - }) - } - }, - loadStep(context: CR.MachineContext): void { - const step: G.Step = selectors.currentStep(context) - if (step.setup) { - // load step actions - channel.editorSend({ - type: 'SETUP_ACTIONS', - payload: { - stepId: step.id, - ...step.setup, - } - }) - } - }, - editorLoadSolution(context: CR.MachineContext): void { - const step: G.Step = selectors.currentStep(context) - // tell editor to load solution commit - channel.editorSend({ - type: 'SOLUTION_ACTIONS', - payload: { - stepId: step.id, - ...step.solution, - } - }) - }, - clearStorage(): void { - channel.editorSend({type: 'TUTORIAL_CLEAR'}) - } -} \ No newline at end of file + channel.editorSend({ + type: 'EDITOR_TUTORIAL_CONFIG', + payload: { tutorial: result.data.tutorial }, + }) + }) + .catch((error: Error) => { + return Promise.reject(`Failed to load tutorial config ${error.message}`) + }) + }, + continueConfig() { + channel.editorSend({ + type: 'EDITOR_TUTORIAL_CONTINUE_CONFIG', + }) + }, + loadLevel(context: CR.MachineContext): void { + const level: G.Level = selectors.currentLevel(context) + if (level.setup) { + // load step actions + channel.editorSend({ + type: 'SETUP_ACTIONS', + payload: level.setup, + }) + } + }, + loadStep(context: CR.MachineContext): void { + const step: G.Step = selectors.currentStep(context) + if (step.setup) { + // load step actions + channel.editorSend({ + type: 'SETUP_ACTIONS', + payload: { + stepId: step.id, + ...step.setup, + }, + }) + } + }, + editorLoadSolution(context: CR.MachineContext): void { + const step: G.Step = selectors.currentStep(context) + // tell editor to load solution commit + channel.editorSend({ + type: 'SOLUTION_ACTIONS', + payload: { + stepId: step.id, + ...step.solution, + }, + }) + }, + clearStorage(): void { + channel.editorSend({ type: 'TUTORIAL_CLEAR' }) + }, +} diff --git a/web-app/src/services/state/actions/index.ts b/web-app/src/services/state/actions/index.ts index 0af5449f..c2622429 100644 --- a/web-app/src/services/state/actions/index.ts +++ b/web-app/src/services/state/actions/index.ts @@ -3,7 +3,7 @@ import contextActions from './context' import apiActions from './api' export default { - ...editorActions, - ...contextActions, - ...apiActions, + ...editorActions, + ...contextActions, + ...apiActions, } diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts index a15e8eae..0835dffd 100644 --- a/web-app/src/services/state/machine.ts +++ b/web-app/src/services/state/machine.ts @@ -1,194 +1,196 @@ -import {Machine, MachineOptions} from 'xstate' +import { Machine, MachineOptions } from 'xstate' import * as CR from 'typings' import actions from './actions' const options: MachineOptions = { - // @ts-ignore - actions + // @ts-ignore + actions, } export const machine = Machine( - { - id: 'root', - initial: 'Start', - context: { - env: {machineId: '', sessionId: '', token: ''}, - tutorial: null, - position: {levelId: '', stepId: ''}, - progress: { - levels: {}, - steps: {}, - complete: false - } - }, - states: { - Start: { - initial: 'Startup', - states: { - Startup: { - onEntry: ['loadEnv'], - on: { - ENV_LOAD: { - target: 'Authenticate', - actions: ['setEnv'], - } - } - }, - Authenticate: { - onEntry: ['authenticate'], - on: { - AUTHENTICATED: 'NewOrContinue' - }, - }, - NewOrContinue: { - onEntry: ['loadStoredTutorial'], - on: { - CONTINUE_TUTORIAL: { - target: 'ContinueTutorial', - actions: ['continueTutorial'], - }, - NEW_TUTORIAL: { - target: 'SelectTutorial', - } - }, - }, - SelectTutorial: { - onEntry: ['clearStorage'], - id: 'start-new-tutorial', - on: { - TUTORIAL_START: { - target: '#tutorial', - actions: ['newTutorial'], - }, - }, - }, - ContinueTutorial: { - on: { - TUTORIAL_START: { - target: '#tutorial-level', - actions: ['continueConfig'], - }, - TUTORIAL_SELECT: 'SelectTutorial' - }, - }, - }, - }, - Tutorial: { - id: 'tutorial', - initial: 'Initialize', - states: { - // TODO: move Initialize into New Tutorial setup - Initialize: { - onEntry: ['initializeTutorial'], - on: { - TUTORIAL_CONFIGURED: 'Summary', - // TUTORIAL_CONFIG_ERROR: 'Start' // TODO: should handle error - } - }, - Summary: { - on: { - LOAD_TUTORIAL: { - target: 'Level', - actions: ['initPosition', 'initTutorial'] - } - }, - }, - LoadNext: { - id: 'tutorial-load-next', - onEntry: ['loadNext'], - on: { - NEXT_STEP: { - target: 'Level', - actions: ['updatePosition'] - }, - NEXT_LEVEL: { - target: 'Level', // TODO should return to levels summary page - actions: ['updatePosition'] - }, - COMPLETED: '#completed-tutorial' - } - }, - Level: { - initial: 'Load', - states: { - Load: { - onEntry: ['loadLevel', 'loadStep'], - after: { - 0: 'Normal' - } - }, - Normal: { - id: 'tutorial-level', - on: { - TEST_RUNNING: 'TestRunning', - STEP_SOLUTION_LOAD: { - actions: ['editorLoadSolution'], - }, - }, - }, - TestRunning: { - onEntry: ['testStart'], - on: { - TEST_PASS: { - target: 'TestPass', - actions: ['updateStepProgress'] - }, - TEST_FAIL: 'TestFail', - TEST_ERROR: 'TestError' - }, - }, - TestError: { - onEntry: ['testFail'], - after: { - 0: 'Normal' - } - }, - TestPass: { - onExit: ['updateStepPosition'], - after: { - 1000: 'StepNext', - }, - }, - TestFail: { - onEntry: ['testFail'], - after: { - 0: 'Normal', - }, - }, - StepNext: { - onEntry: ['stepNext'], - on: { - LOAD_NEXT_STEP: { - target: 'Normal', - actions: ['loadStep'] - }, - LEVEL_COMPLETE: { - target: "LevelComplete", - actions: ['updateLevelProgress'] - } - } - }, - LevelComplete: { - onEntry: ['syncProgress'], - on: { - LEVEL_NEXT: '#tutorial-load-next', - }, - }, - }, - }, - Completed: { - id: 'completed-tutorial', - onEntry: ['syncProgress', 'userTutorialComplete'], - on: { - SELECT_TUTORIAL: { - target: '#start-new-tutorial', - actions: ['reset'] - } - } - }, - }, - }, - }, - }, options) + { + id: 'root', + initial: 'Start', + context: { + env: { machineId: '', sessionId: '', token: '' }, + tutorial: null, + position: { levelId: '', stepId: '' }, + progress: { + levels: {}, + steps: {}, + complete: false, + }, + }, + states: { + Start: { + initial: 'Startup', + states: { + Startup: { + onEntry: ['loadEnv'], + on: { + ENV_LOAD: { + target: 'Authenticate', + actions: ['setEnv'], + }, + }, + }, + Authenticate: { + onEntry: ['authenticate'], + on: { + AUTHENTICATED: 'NewOrContinue', + }, + }, + NewOrContinue: { + onEntry: ['loadStoredTutorial'], + on: { + CONTINUE_TUTORIAL: { + target: 'ContinueTutorial', + actions: ['continueTutorial'], + }, + NEW_TUTORIAL: { + target: 'SelectTutorial', + }, + }, + }, + SelectTutorial: { + onEntry: ['clearStorage'], + id: 'start-new-tutorial', + on: { + TUTORIAL_START: { + target: '#tutorial', + actions: ['newTutorial'], + }, + }, + }, + ContinueTutorial: { + on: { + TUTORIAL_START: { + target: '#tutorial-level', + actions: ['continueConfig'], + }, + TUTORIAL_SELECT: 'SelectTutorial', + }, + }, + }, + }, + Tutorial: { + id: 'tutorial', + initial: 'Initialize', + states: { + // TODO: move Initialize into New Tutorial setup + Initialize: { + onEntry: ['initializeTutorial'], + on: { + TUTORIAL_CONFIGURED: 'Summary', + // TUTORIAL_CONFIG_ERROR: 'Start' // TODO: should handle error + }, + }, + Summary: { + on: { + LOAD_TUTORIAL: { + target: 'Level', + actions: ['initPosition', 'initTutorial'], + }, + }, + }, + LoadNext: { + id: 'tutorial-load-next', + onEntry: ['loadNext'], + on: { + NEXT_STEP: { + target: 'Level', + actions: ['updatePosition'], + }, + NEXT_LEVEL: { + target: 'Level', // TODO should return to levels summary page + actions: ['updatePosition'], + }, + COMPLETED: '#completed-tutorial', + }, + }, + Level: { + initial: 'Load', + states: { + Load: { + onEntry: ['loadLevel', 'loadStep'], + after: { + 0: 'Normal', + }, + }, + Normal: { + id: 'tutorial-level', + on: { + TEST_RUNNING: 'TestRunning', + STEP_SOLUTION_LOAD: { + actions: ['editorLoadSolution'], + }, + }, + }, + TestRunning: { + onEntry: ['testStart'], + on: { + TEST_PASS: { + target: 'TestPass', + actions: ['updateStepProgress'], + }, + TEST_FAIL: 'TestFail', + TEST_ERROR: 'TestError', + }, + }, + TestError: { + onEntry: ['testFail'], + after: { + 0: 'Normal', + }, + }, + TestPass: { + onExit: ['updateStepPosition'], + after: { + 1000: 'StepNext', + }, + }, + TestFail: { + onEntry: ['testFail'], + after: { + 0: 'Normal', + }, + }, + StepNext: { + onEntry: ['stepNext'], + on: { + LOAD_NEXT_STEP: { + target: 'Normal', + actions: ['loadStep'], + }, + LEVEL_COMPLETE: { + target: 'LevelComplete', + actions: ['updateLevelProgress'], + }, + }, + }, + LevelComplete: { + onEntry: ['syncProgress'], + on: { + LEVEL_NEXT: '#tutorial-load-next', + }, + }, + }, + }, + Completed: { + id: 'completed-tutorial', + onEntry: ['syncProgress', 'userTutorialComplete'], + on: { + SELECT_TUTORIAL: { + target: '#start-new-tutorial', + actions: ['reset'], + }, + }, + }, + }, + }, + }, + }, + options, +) export default machine diff --git a/web-app/src/services/xstate-react/index.ts b/web-app/src/services/xstate-react/index.ts index 99a3774b..2e85bec9 100644 --- a/web-app/src/services/xstate-react/index.ts +++ b/web-app/src/services/xstate-react/index.ts @@ -1,145 +1,117 @@ -import {useState, useRef, useEffect} from 'react' -import { - interpret, - EventObject, - StateMachine, - State, - Interpreter, - InterpreterOptions, - MachineOptions -} from 'xstate' +import { useState, useRef, useEffect } from 'react' +import { interpret, EventObject, StateMachine, State, Interpreter, InterpreterOptions, MachineOptions } from 'xstate' interface UseMachineOptions { /** * If provided, will be merged with machine's context. */ - context?: Partial + context?: Partial /** * If `true`, service will start immediately (before mount). */ - immediate: boolean + immediate: boolean } const defaultOptions = { - immediate: false + immediate: false, } export function useMachine( - machine: StateMachine, - options: Partial & - Partial> & - Partial> = defaultOptions -): [ - State, - Interpreter['send'], - Interpreter - ] { - const { - context, - guards, - actions, - activities, - services, - delays, - immediate, - ...interpreterOptions - } = options - - const machineConfig = { - context, - guards, - actions, - activities, - services, - delays - } - - // Reference the machine - const machineRef = useRef | null>(null) - - // Create the machine only once - // See https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily - if (machineRef.current === null) { - machineRef.current = machine.withConfig(machineConfig, { - ...machine.context, - ...context - } as TContext) - } - - // Reference the service - const serviceRef = useRef | null>(null) - - // Create the service only once - if (serviceRef.current === null) { - serviceRef.current = interpret( - machineRef.current, - interpreterOptions - ).onTransition(state => { - // Update the current machine state when a transition occurs - if (state.changed) { - setCurrent(state) - } - }) - } - - const service = serviceRef.current - - // Make sure actions are kept updated when they change. - // This mutation assignment is safe because the service instance is only used - // in one place -- this hook's caller. - useEffect(() => { - Object.assign(service.machine.options.actions, actions) - }, [service.machine.options.actions, actions]) - - // Start service immediately (before mount) if specified in options - if (immediate) { - service.start() - } - - // Keep track of the current machine state - const [current, setCurrent] = useState(service.initialState) - - useEffect(() => { - // Start the service when the component mounts. - // Note: the service will start only if it hasn't started already. - service.start() - - return () => { - // Stop the service when the component unmounts - service.stop() - } - }, [service]) - - return [current, service.send, service] + machine: StateMachine, + options: Partial & + Partial> & + Partial> = defaultOptions, +): [State, Interpreter['send'], Interpreter] { + const { context, guards, actions, activities, services, delays, immediate, ...interpreterOptions } = options + + const machineConfig = { + context, + guards, + actions, + activities, + services, + delays, + } + + // Reference the machine + const machineRef = useRef | null>(null) + + // Create the machine only once + // See https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily + if (machineRef.current === null) { + machineRef.current = machine.withConfig(machineConfig, { + ...machine.context, + ...context, + } as TContext) + } + + // Reference the service + const serviceRef = useRef | null>(null) + + // Create the service only once + if (serviceRef.current === null) { + serviceRef.current = interpret(machineRef.current, interpreterOptions).onTransition(state => { + // Update the current machine state when a transition occurs + if (state.changed) { + setCurrent(state) // eslint-disable-line + } + }) + } + + const service = serviceRef.current + + // Make sure actions are kept updated when they change. + // This mutation assignment is safe because the service instance is only used + // in one place -- this hook's caller. + useEffect(() => { + Object.assign(service.machine.options.actions, actions) + }, [service.machine.options.actions, actions]) + + // Start service immediately (before mount) if specified in options + if (immediate) { + service.start() + } + + // Keep track of the current machine state + const [current, setCurrent] = useState(service.initialState) + + useEffect(() => { + // Start the service when the component mounts. + // Note: the service will start only if it hasn't started already. + service.start() + + return () => { + // Stop the service when the component unmounts + service.stop() + } + }, [service]) + + return [current, service.send, service] } export function useService( - service: Interpreter -): [ - State, - Interpreter['send'], - Interpreter - ] { - const [current, setCurrent] = useState(service.state) - - useEffect(() => { - // Set to current service state as there is a possibility - // of a transition occurring between the initial useState() - // initialization and useEffect() commit. - setCurrent(service.state) - - const listener = (state: State) => { - if (state.changed) { - setCurrent(state) - } - } - - service.onTransition(listener) - - return () => { - service.off(listener) - } - }, [service]) - - return [current, service.send, service] + service: Interpreter, +): [State, Interpreter['send'], Interpreter] { + const [current, setCurrent] = useState(service.state) + + useEffect(() => { + // Set to current service state as there is a possibility + // of a transition occurring between the initial useState() + // initialization and useEffect() commit. + setCurrent(service.state) + + const listener = (state: State) => { + if (state.changed) { + setCurrent(state) + } + } + + service.onTransition(listener) + + return () => { + service.off(listener) + } + }, [service]) + + return [current, service.send, service] } diff --git a/web-app/stories/Step.stories.tsx b/web-app/stories/Step.stories.tsx index 91407222..3bf5b8ec 100644 --- a/web-app/stories/Step.stories.tsx +++ b/web-app/stories/Step.stories.tsx @@ -35,7 +35,7 @@ storiesOf('Level', module) .addDecorator(withKnobs) .add('Step', () => ( (