Просмотр исходного кода

Merge pull request #4114 from weseek/support/import-growi-commons

Support/import growi commons
Yuki Takei 4 лет назад
Родитель
Сommit
1596521a06
31 измененных файлов с 1233 добавлено и 80 удалено
  1. 1 0
      .devcontainer/devcontainer.json
  2. 4 0
      .eslintrc.js
  3. 21 3
      package.json
  4. 0 10
      packages/app/package.json
  5. 1 0
      packages/core/.eslintignore
  6. 27 0
      packages/core/README.md
  7. 60 0
      packages/core/jest.config.js
  8. 22 0
      packages/core/package.json
  9. 10 0
      packages/core/src/index.js
  10. 14 0
      packages/core/src/plugin/model/tag-context.js
  11. 71 0
      packages/core/src/plugin/service/tag-cache-manager.js
  12. 59 0
      packages/core/src/plugin/util/args-parser.js
  13. 88 0
      packages/core/src/plugin/util/custom-tag-utils.js
  14. 70 0
      packages/core/src/plugin/util/option-parser.js
  15. 56 0
      packages/core/src/service/localstorage-manager.js
  16. 125 0
      packages/core/src/test/plugin/service/tag-cache-manager.test.js
  17. 42 0
      packages/core/src/test/plugin/util/args-parser.test.js
  18. 72 0
      packages/core/src/test/plugin/util/custom-tag-utils.test.js
  19. 32 0
      packages/core/src/test/plugin/util/option-parser.test.js
  20. 86 0
      packages/core/src/test/service/localstorage-manager.test.js
  21. 21 0
      packages/core/src/test/util/env-utils.test.js
  22. 26 0
      packages/core/src/test/util/path-utils.test.js
  23. 47 0
      packages/core/src/util/basic-interceptor.js
  24. 17 0
      packages/core/src/util/env-utils.js
  25. 120 0
      packages/core/src/util/path-utils.js
  26. 14 0
      packages/core/tsconfig.json
  27. 0 1
      packages/plugin-attachment-refs/package.json
  28. 1 7
      packages/plugin-pukiwiki-like-linker/package.json
  29. 1 7
      packages/slack/package.json
  30. 1 8
      packages/slackbot-proxy/package.json
  31. 124 44
      yarn.lock

+ 1 - 0
.devcontainer/devcontainer.json

@@ -16,6 +16,7 @@
 	"extensions": [
 	"extensions": [
 		"dbaeumer.vscode-eslint",
 		"dbaeumer.vscode-eslint",
 		"eamodio.gitlens",
 		"eamodio.gitlens",
+    "firsttris.vscode-jest-runner",
 		"msjsdiag.debugger-for-chrome",
 		"msjsdiag.debugger-for-chrome",
 		"firefox-devtools.vscode-firefox-debug",
 		"firefox-devtools.vscode-firefox-debug",
 		"editorconfig.editorconfig",
 		"editorconfig.editorconfig",

+ 4 - 0
.eslintrc.js

@@ -26,5 +26,9 @@ module.exports = {
         FunctionExpression: { body: 1, parameters: 2 },
         FunctionExpression: { body: 1, parameters: 2 },
       },
       },
     ],
     ],
+    'jest/no-standalone-expect': [
+      'error',
+      { additionalTestBlockFunctions: ['each.test'] },
+    ],
   },
   },
 };
 };

+ 21 - 3
package.json

@@ -21,8 +21,12 @@
   },
   },
   "private": true,
   "private": true,
   "workspaces": {
   "workspaces": {
-    "packages": ["packages/*"],
-    "nohoist": ["**/slackbot-proxy/bootstrap"]
+    "packages": [
+      "packages/*"
+    ],
+    "nohoist": [
+      "**/slackbot-proxy/bootstrap"
+    ]
   },
   },
   "scripts": {
   "scripts": {
     "start": "yarn app:server",
     "start": "yarn app:server",
@@ -37,8 +41,17 @@
     "server:prod": "echo !!! CAUTION !!! ==> The script 'server:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:server"
     "server:prod": "echo !!! CAUTION !!! ==> The script 'server:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:server"
   },
   },
   "dependencies": {
   "dependencies": {
+    "cross-env": "^7.0.0",
+    "dotenv-flow": "^3.2.0",
+    "npm-run-all": "^4.1.5",
+    "ts-node": "^9.1.1",
+    "tsconfig-paths": "^3.9.0",
+    "typescript": "^4.2.3"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@types/jest": "^26.0.22",
+    "@types/node": "^14.14.35",
+    "@types/rewire": "^2.5.28",
     "@typescript-eslint/eslint-plugin": "^4.28.5",
     "@typescript-eslint/eslint-plugin": "^4.28.5",
     "@typescript-eslint/parser": "^4.28.5",
     "@typescript-eslint/parser": "^4.28.5",
     "eslint": "^7.31.0",
     "eslint": "^7.31.0",
@@ -48,7 +61,12 @@
     "eslint-plugin-jest": "^24.3.2",
     "eslint-plugin-jest": "^24.3.2",
     "eslint-plugin-react": "^7.24.0",
     "eslint-plugin-react": "^7.24.0",
     "eslint-plugin-react-hooks": "^4.2.0",
     "eslint-plugin-react-hooks": "^4.2.0",
-    "lerna": "^4.0.0"
+    "jest": "^27.0.6",
+    "jest-date-mock": "^1.0.8",
+    "jest-localstorage-mock": "^2.4.14",
+    "lerna": "^4.0.0",
+    "rewire": "^5.0.0",
+    "ts-jest": "^27.0.4"
   },
   },
   "engines": {
   "engines": {
     "node": "^12 || ^14",
     "node": "^12 || ^14",

+ 0 - 10
packages/app/package.json

@@ -78,12 +78,10 @@
     "connect-mongo": "^4.4.1",
     "connect-mongo": "^4.4.1",
     "connect-redis": "^4.0.4",
     "connect-redis": "^4.0.4",
     "cookie-parser": "^1.4.5",
     "cookie-parser": "^1.4.5",
-    "cross-env": "^7.0.0",
     "csrf": "^3.1.0",
     "csrf": "^3.1.0",
     "date-fns": "^2.0.0",
     "date-fns": "^2.0.0",
     "detect-indent": "^6.0.0",
     "detect-indent": "^6.0.0",
     "diff": "^5.0.0",
     "diff": "^5.0.0",
-    "dotenv-flow": "^3.2.0",
     "elasticsearch": "^16.0.0",
     "elasticsearch": "^16.0.0",
     "entities": "^2.0.0",
     "entities": "^2.0.0",
     "esa-nodejs": "^0.0.7",
     "esa-nodejs": "^0.0.7",
@@ -118,7 +116,6 @@
     "multer-autoreap": "^1.0.3",
     "multer-autoreap": "^1.0.3",
     "nodemailer": "^6.6.2",
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
     "nodemailer-ses-transport": "~1.5.0",
-    "npm-run-all": "^4.1.2",
     "openid-client": "=2.5.0",
     "openid-client": "=2.5.0",
     "passport": "^0.4.0",
     "passport": "^0.4.0",
     "passport-github": "^1.1.0",
     "passport-github": "^1.1.0",
@@ -139,9 +136,6 @@
     "string-width": "=4.2.2",
     "string-width": "=4.2.2",
     "swagger-jsdoc": "^3.4.0",
     "swagger-jsdoc": "^3.4.0",
     "swig-templates": "^2.0.2",
     "swig-templates": "^2.0.2",
-    "ts-node": "^9.1.1",
-    "tsconfig-paths": "^3.9.0",
-    "typescript": "^4.2.3",
     "uglifycss": "^0.0.29",
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
     "universal-bunyan": "^0.9.2",
     "unzipper": "^0.10.5",
     "unzipper": "^0.10.5",
@@ -163,7 +157,6 @@
     "@types/compression": "^1.7.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",
     "@types/multer": "^1.4.5",
     "@types/multer": "^1.4.5",
-    "@types/node": "^14.14.35",
     "@types/react-dom": "^17.0.9",
     "@types/react-dom": "^17.0.9",
     "autoprefixer": "^9.0.0",
     "autoprefixer": "^9.0.0",
     "bootstrap": "^4.5.0",
     "bootstrap": "^4.5.0",
@@ -183,8 +176,6 @@
     "hard-source-webpack-plugin": "^0.13.1",
     "hard-source-webpack-plugin": "^0.13.1",
     "i18next-browser-languagedetector": "^4.0.1",
     "i18next-browser-languagedetector": "^4.0.1",
     "imports-loader": "^0.8.0",
     "imports-loader": "^0.8.0",
-    "jest": "^27.0.6",
-    "jest-date-mock": "^1.0.8",
     "jquery-slimscroll": "^1.3.8",
     "jquery-slimscroll": "^1.3.8",
     "jquery-ui": "^1.12.1",
     "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
     "jquery.cookie": "~1.4.1",
@@ -238,7 +229,6 @@
     "terser-webpack-plugin": "^4.1.0",
     "terser-webpack-plugin": "^4.1.0",
     "throttle-debounce": "^2.0.0",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",
     "toastr": "^2.1.2",
-    "ts-jest": "^27.0.4",
     "ts-loader": "^8.3.0",
     "ts-loader": "^8.3.0",
     "ts-node-dev": "^1.1.6",
     "ts-node-dev": "^1.1.6",
     "tsc-alias": "^1.2.9",
     "tsc-alias": "^1.2.9",

+ 1 - 0
packages/core/.eslintignore

@@ -0,0 +1 @@
+/dist/**

+ 27 - 0
packages/core/README.md

@@ -0,0 +1,27 @@
+# growi-commons
+
+[![dependencies status](https://david-dm.org/weseek/growi-commons.svg)](https://david-dm.org/weseek/growi-commons)
+[![devDependencies Status](https://david-dm.org/weseek/growi-commons/dev-status.svg)](https://david-dm.org/weseek/growi-commons?type=dev)
+[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+
+[GROWI](https://growi.org) Commons Libraries to develop GROWI and plugins
+
+
+Overview
+--------
+
+growi-commons package is includes some functions, classes and modules to develop GROWI substance and GROWI plugins.
+
+Install
+--------
+
+1. install plugin
+
+    ```
+    $ npm install --save growi-commons
+    ```
+
+Documentation
+------------
+
+See https://docs.growi.org/api/commons/

+ 60 - 0
packages/core/jest.config.js

@@ -0,0 +1,60 @@
+// For a detailed explanation regarding each configuration property, visit:
+// https://jestjs.io/docs/en/configuration.html
+
+const MODULE_NAME_MAPPING = {
+  '^~/(.+)$': '<rootDir>/src/$1',
+};
+
+module.exports = {
+
+  preset: 'ts-jest/presets/js-with-ts',
+
+  moduleNameMapper: MODULE_NAME_MAPPING,
+
+  // Automatically clear mock calls and instances between every test
+  clearMocks: true,
+
+  // Indicates whether the coverage information should be collected while executing the test
+  collectCoverage: true,
+
+  // An array of glob patterns indicating a set of files for which coverage information should be collected
+  // collectCoverageFrom: undefined,
+
+  // The directory where Jest should output its coverage files
+  coverageDirectory: 'coverage',
+
+  // An array of regexp pattern strings used to skip coverage collection
+  coveragePathIgnorePatterns: [
+    '/node_modules/',
+  ],
+
+  // An object that configures minimum threshold enforcement for coverage results
+  // TODO: activate -- 2020.03.24 Yuki Takei
+  // coverageThreshold: {
+  //   global: {
+  //     branches: 70,
+  //     functions: 70,
+  //     lines: 70,
+  //     statements: 70,
+  //   },
+  // },
+
+  // An array of file extensions your modules use
+  moduleFileExtensions: [
+    'js',
+    'json',
+    'jsx',
+    'ts',
+    'tsx',
+    'node',
+  ],
+
+  // The test environment that will be used for testing
+  testEnvironment: 'node',
+
+  // The glob patterns Jest uses to detect test files
+  testMatch: [
+    '**/src/**/__tests__/**/*.[jt]s?(x)',
+    '**/src/**/?(*.)+(spec|test).[tj]s?(x)',
+  ],
+};

+ 22 - 0
packages/core/package.json

@@ -0,0 +1,22 @@
+{
+  "name": "@growi/core",
+  "version": "4.3.3-RC",
+  "description": "GROWI Core Libraries",
+  "license": "MIT",
+  "keywords": [
+    "growi"
+  ],
+  "main": "src/index.js",
+  "files": [
+    "src"
+  ],
+  "scripts": {
+    "lint:js": "eslint **/*.{js,jsx}",
+    "lint:styles": "stylelint src/styles/scss/**/*.scss",
+    "lint": "npm-run-all -p lint:*",
+    "test": "jest --verbose"
+  },
+  "dependencies": {},
+  "devDependencies": {
+  }
+}

+ 10 - 0
packages/core/src/index.js

@@ -0,0 +1,10 @@
+module.exports = {
+  BasicInterceptor: require('./util/basic-interceptor'),
+  pathUtils: require('./util/path-utils'),
+  envUtils: require('./util/env-utils'),
+  // plugin
+  customTagUtils: require('./plugin/util/custom-tag-utils'),
+  TagCacheManager: require('./plugin/service/tag-cache-manager'),
+  // service
+  LocalStorageManager: require('./service/localstorage-manager'),
+};

+ 14 - 0
packages/core/src/plugin/model/tag-context.js

@@ -0,0 +1,14 @@
+/**
+ * Context class for custom-tag-utils#findTagAndReplace
+ */
+class TagContext {
+
+  constructor(initArgs = {}) {
+    this.tagExpression = initArgs.tagExpression || null;
+    this.method = initArgs.method || null;
+    this.args = initArgs.args || null;
+  }
+
+}
+
+module.exports = TagContext;

+ 71 - 0
packages/core/src/plugin/service/tag-cache-manager.js

@@ -0,0 +1,71 @@
+const LocalStorageManager = require('../../service/localstorage-manager');
+
+/**
+ * Service Class for caching React state and TagContext
+ */
+class TagCacheManager {
+
+  /**
+   * @callback generateCacheKey
+   * @param {TagContext} tagContext - TagContext instance
+   * @returns {string} Cache key from TagContext
+   *
+   */
+
+  /**
+   * Constructor
+   * @param {string} cacheNs Used as LocalStorageManager namespace
+   * @param {generateCacheKey} generateCacheKey
+   */
+  constructor(cacheNs, generateCacheKey) {
+    if (cacheNs == null) {
+      throw new Error('args \'cacheNs\' is required.');
+    }
+    if (generateCacheKey == null) {
+      throw new Error('args \'generateCacheKey\' is required.');
+    }
+    if (typeof generateCacheKey !== 'function') {
+      throw new Error('args \'generateCacheKey\' should be function.');
+    }
+
+    this.cacheNs = cacheNs;
+    this.generateCacheKey = generateCacheKey;
+  }
+
+  /**
+   * Retrieve state cache object from local storage
+   * @param {TagContext} tagContext
+   * @returns {object} a cache object that correspont to the specified `tagContext`
+   */
+  getStateCache(tagContext) {
+    const localStorageManager = LocalStorageManager.getInstance();
+
+    const key = this.generateCacheKey(tagContext);
+    const stateCache = localStorageManager.retrieveFromSessionStorage(this.cacheNs, key);
+
+    return stateCache;
+  }
+
+  /**
+   * store state object of React Component with specified key
+   *
+   * @param {TagContext} tagContext
+   * @param {object} state state object of React Component
+   */
+  cacheState(tagContext, state) {
+    const localStorageManager = LocalStorageManager.getInstance();
+    const key = this.generateCacheKey(tagContext);
+    localStorageManager.saveToSessionStorage(this.cacheNs, key, state);
+  }
+
+  /**
+   * clear all state caches
+   */
+  clearAllStateCaches() {
+    const localStorageManager = LocalStorageManager.getInstance();
+    localStorageManager.clearAllStateCaches(this.cacheNs);
+  }
+
+}
+
+module.exports = TagCacheManager;

+ 59 - 0
packages/core/src/plugin/util/args-parser.js

@@ -0,0 +1,59 @@
+/**
+ * Arguments parser for custom tag
+ */
+class ArgsParser {
+
+  /**
+   * @typedef ParseArgsResult
+   * @property {string} firstArgsKey - key of the first argument
+   * @property {string} firstArgsValue - value of the first argument
+   * @property {object} options - key of the first argument
+   */
+
+  /**
+   * parse plugin argument strings
+   *
+   * @static
+   * @param {string} str
+   * @returns {ParseArgsResult}
+   */
+  static parse(str) {
+    let firstArgsKey = null;
+    let firstArgsValue = null;
+    const options = {};
+
+    if (str != null && str.length > 0) {
+      const splittedArgs = str.split(',');
+
+      splittedArgs.forEach((rawArg, index) => {
+        const arg = rawArg.trim();
+
+        // parse string like 'key1=value1, key2=value2, ...'
+        // see https://regex101.com/r/pYHcOM/1
+        const match = arg.match(/([^=]+)=?(.+)?/);
+
+        if (match == null) {
+          return;
+        }
+
+        const key = match[1];
+        const value = match[2] || true;
+        options[key] = value;
+
+        if (index === 0) {
+          firstArgsKey = key;
+          firstArgsValue = value;
+        }
+      });
+    }
+
+    return {
+      firstArgsKey,
+      firstArgsValue,
+      options,
+    };
+  }
+
+}
+
+module.exports = ArgsParser;

+ 88 - 0
packages/core/src/plugin/util/custom-tag-utils.js

@@ -0,0 +1,88 @@
+const TagContext = require('../model/tag-context');
+
+/**
+ * @private
+ *
+ * create random strings
+ * @see http://qiita.com/ryounagaoka/items/4736c225bdd86a74d59c
+ *
+ * @param {number} length
+ * @return {string} random strings
+ */
+function createRandomStr(length) {
+  const bag = 'abcdefghijklmnopqrstuvwxyz0123456789';
+  let generated = '';
+  for (let i = 0; i < length; i++) {
+    generated += bag[Math.floor(Math.random() * bag.length)];
+  }
+  return generated;
+}
+
+/**
+ * @typedef FindTagAndReplaceResult
+ * @property {string} html - HTML string
+ * @property {Object} tagContextMap - Object.<string, [TagContext]{@link ../model/tag-context.html#TagContext}>
+ *
+ * @memberof customTagUtils
+ */
+/**
+ * @param {RegExp} tagPattern
+ * @param {string} html
+ * @param {function} replace replace function
+ * @return {FindTagAndReplaceResult}
+ *
+ * @memberof customTagUtils
+ */
+function findTagAndReplace(tagPattern, html, replace) {
+  let replacedHtml = html;
+  const tagContextMap = {};
+
+  if (tagPattern == null || html == null) {
+    return { html: replacedHtml, tagContextMap };
+  }
+
+  // see: https://regex101.com/r/NQq3s9/9
+  const pattern = new RegExp(`\\$(${tagPattern.source})\\((.*?)\\)(?=[<\\[\\s\\$])|\\$(${tagPattern.source})\\((.*)\\)(?![<\\[\\s\\$])`, 'g');
+
+  replacedHtml = html.replace(pattern, (all, group1, group2, group3, group4) => {
+    const tagExpression = all;
+    const method = (group1 || group3).trim();
+    const args = (group2 || group4 || '').trim();
+
+    // create contexts
+    const tagContext = new TagContext({ tagExpression, method, args });
+
+    if (replace != null) {
+      return replace(tagContext);
+    }
+
+    // replace with empty dom
+    const domId = `${method}-${createRandomStr(8)}`;
+    tagContextMap[domId] = tagContext;
+    return `<div id="${domId}"></div>`;
+  });
+
+  return { html: replacedHtml, tagContextMap };
+}
+
+/**
+ * @namespace customTagUtils
+ */
+module.exports = {
+  findTagAndReplace,
+  /**
+   * Context class used by findTagAndReplace
+   * @memberof customTagUtils
+   */
+  TagContext,
+  /**
+   * [ArgsParser]{@link ./args-parser#ArgsParser}
+   * @memberof customTagUtils
+   */
+  ArgsParser: require('./args-parser'),
+  /**
+   * [OptionParser]{@link ./option-parser#OptionParser}
+   * @memberof customTagUtils
+   */
+  OptionParser: require('./option-parser'),
+};

+ 70 - 0
packages/core/src/plugin/util/option-parser.js

@@ -0,0 +1,70 @@
+/**
+ * Options parser for custom tag
+ */
+class OptionParser {
+
+  /**
+   * @typedef ParseRangeResult
+   * @property {number} start - start index
+   * @property {number} end - end index
+   */
+
+  /**
+   * Parse range expression
+   *
+   * <ul>
+   *  <li>ex:</li>
+   *  <ul>
+   *    <li>1:2 -> { start: 1, end: 2 }</li>
+   *    <li>1:  -> { start: 1, end: -1 }</li>
+   *    <li>2+3 -> { start: 1, end: 5 }</li>
+   *  </ul>
+   * </ul>
+   *
+   * @see https://regex101.com/r/w4KCwC/4
+   *
+   * @static
+   * @param {string} str
+   * @returns {ParseRangeResult}
+   */
+  static parseRange(str) {
+    if (str == null) {
+      return null;
+    }
+
+    // see: https://regex101.com/r/w4KCwC/4
+    const match = str.match(/^(-?[0-9]+)(([:+]{1})(-?[0-9]+)?)?$/);
+    if (!match) {
+      return null;
+    }
+
+    // determine start
+    let start;
+    let end;
+
+    // has operator
+    if (match[3] != null) {
+      start = +match[1];
+      const operator = match[3];
+
+      // determine end
+      if (operator === ':') {
+        end = +match[4] || -1; // set last(-1) if undefined
+      }
+      else if (operator === '+') {
+        end = +match[4] || 0; // plus zero if undefined
+        end += start;
+      }
+    }
+    // don't have operator
+    else {
+      start = 1;
+      end = +match[1];
+    }
+
+    return { start, end };
+  }
+
+}
+
+module.exports = OptionParser;

+ 56 - 0
packages/core/src/service/localstorage-manager.js

@@ -0,0 +1,56 @@
+let _instance = null;
+class LocalStorageManager {
+
+  static getInstance() {
+    if (_instance == null) {
+      _instance = new LocalStorageManager();
+    }
+
+    return _instance;
+  }
+
+  /**
+   * retrieve and return parsed JSON object
+   * @param {string} namespace
+   * @param {string} key
+   * @returns {object}
+   */
+  retrieveFromSessionStorage(namespace, key) {
+    const item = JSON.parse(sessionStorage.getItem(namespace)) || {};
+    if (key != null) {
+      return item[key];
+    }
+    return item;
+  }
+
+  /**
+   * save JavaScript object as stringified JSON object
+   *
+   * @param {string} namespace
+   * @param {string | object} cacheObjOrKey cache object or key (if third param is undefined)
+   * @param {object} cacheObj
+   */
+  saveToSessionStorage(namespace, cacheObjOrKey, cacheObj) {
+    let item = JSON.parse(sessionStorage.getItem(namespace)) || {};
+    if (cacheObj !== undefined) {
+      const key = cacheObjOrKey;
+      item[key] = cacheObj;
+    }
+    else {
+      item = cacheObjOrKey;
+    }
+    sessionStorage.setItem(namespace, JSON.stringify(item));
+  }
+
+  /**
+   * clear all state caches
+   *
+   * @param {string} namespace
+   */
+  clearAllStateCaches(namespace) {
+    sessionStorage.removeItem(namespace);
+  }
+
+}
+
+module.exports = LocalStorageManager;

+ 125 - 0
packages/core/src/test/plugin/service/tag-cache-manager.test.js

@@ -0,0 +1,125 @@
+/* eslint-disable import/first */
+
+// import each from 'jest-each';
+jest.mock('~/service/localstorage-manager');
+
+import LocalStorageManager from '~/service/localstorage-manager';
+import TagCacheManager from '~/plugin/service/tag-cache-manager';
+/* eslint-enable import/first */
+
+describe('TagCacheManager.constructor', () => {
+
+  test('throws Exception when \'cacheNs\' is null', () => {
+    const generateCacheKeyMock = jest.fn();
+
+    expect(() => {
+      // eslint-disable-next-line no-new
+      new TagCacheManager(null, generateCacheKeyMock);
+    }).toThrowError(/cacheNs/);
+  });
+
+  test('throws Exception when \'generateCacheKey\' is null', () => {
+    expect(() => {
+      // eslint-disable-next-line no-new
+      new TagCacheManager('dummy ns', null);
+    }).toThrowError(/generateCacheKey/);
+  });
+
+  test('throws Exception when \'generateCacheKey\' is not function', () => {
+    expect(() => {
+      // eslint-disable-next-line no-new
+      new TagCacheManager('dummy ns', {});
+    }).toThrowError(/generateCacheKey/);
+  });
+
+  test('set params', () => {
+    const generateCacheKeyMock = jest.fn();
+
+    const instance = new TagCacheManager('dummy ns', generateCacheKeyMock);
+    expect(instance).not.toBeNull();
+    expect(instance.cacheNs).toBe('dummy ns');
+    expect(instance.generateCacheKey).toBe(generateCacheKeyMock);
+  });
+
+});
+
+describe('TagCacheManager', () => {
+
+  let generateCacheKeyMock = null;
+  let localStorageManagerMock = null;
+
+  let tagCacheManager = null;
+
+
+  beforeEach(() => {
+    generateCacheKeyMock = jest.fn();
+    localStorageManagerMock = jest.fn();
+
+    // mock for LocalStorageManager.getInstance
+    LocalStorageManager.getInstance = jest.fn();
+    LocalStorageManager.getInstance.mockReturnValue(localStorageManagerMock);
+
+    tagCacheManager = new TagCacheManager('dummy ns', generateCacheKeyMock);
+  });
+
+  test('.getStateCache', () => {
+    // partial mock
+    tagCacheManager.generateCacheKey = jest.fn().mockReturnValue('dummy key');
+
+    // mock for LocalStorageManager
+    const stateCacheMock = jest.fn();
+    localStorageManagerMock.retrieveFromSessionStorage = jest.fn();
+    localStorageManagerMock.retrieveFromSessionStorage
+      .mockReturnValue(stateCacheMock);
+
+    const tagContextMock = jest.fn();
+
+    // when
+    const result = tagCacheManager.getStateCache(tagContextMock);
+    // then
+    expect(result).not.toBeNull();
+    expect(result).toBe(stateCacheMock);
+    const generateCacheKeyMockCalls = tagCacheManager.generateCacheKey.mock.calls;
+    expect(generateCacheKeyMockCalls.length).toBe(1);
+    expect(generateCacheKeyMockCalls[0][0]).toBe(tagContextMock);
+    const retrieveFromSessionStorageMockCalls = localStorageManagerMock.retrieveFromSessionStorage.mock.calls;
+    expect(retrieveFromSessionStorageMockCalls.length).toBe(1);
+    expect(retrieveFromSessionStorageMockCalls[0][0]).toBe('dummy ns');
+    expect(retrieveFromSessionStorageMockCalls[0][1]).toBe('dummy key');
+  });
+
+  test('.getStateCache with state object', () => {
+    // partial mock
+    tagCacheManager.generateCacheKey = jest.fn().mockReturnValue('dummy key');
+
+    // mock for LocalStorageManager
+    localStorageManagerMock.saveToSessionStorage = jest.fn();
+
+    const tagContextMock = jest.fn();
+    const stateMock = jest.fn();
+
+    // when
+    tagCacheManager.cacheState(tagContextMock, stateMock);
+    // then
+    const generateCacheKeyMockCalls = tagCacheManager.generateCacheKey.mock.calls;
+    expect(generateCacheKeyMockCalls.length).toBe(1);
+    expect(generateCacheKeyMockCalls[0][0]).toBe(tagContextMock);
+    const saveToSessionStorageMockCalls = localStorageManagerMock.saveToSessionStorage.mock.calls;
+    expect(saveToSessionStorageMockCalls.length).toBe(1);
+    expect(saveToSessionStorageMockCalls[0][0]).toBe('dummy ns');
+    expect(saveToSessionStorageMockCalls[0][1]).toBe('dummy key');
+    expect(saveToSessionStorageMockCalls[0][2]).toBe(stateMock);
+  });
+
+  test('.clearAllStateCaches', () => {
+    // mock for LocalStorageManager
+    localStorageManagerMock.clearAllStateCaches = jest.fn();
+
+    // when
+    tagCacheManager.clearAllStateCaches();
+    // then
+    const clearAllStateCachesMockCalls = localStorageManagerMock.clearAllStateCaches.mock.calls;
+    expect(clearAllStateCachesMockCalls.length).toBe(1);
+    expect(clearAllStateCachesMockCalls[0][0]).toBe('dummy ns');
+  });
+});

+ 42 - 0
packages/core/src/test/plugin/util/args-parser.test.js

@@ -0,0 +1,42 @@
+import ArgsParser from '~/plugin/util/args-parser';
+
+describe('args-parser', () => {
+
+  test('.parse(null) returns default object', () => {
+    const result = ArgsParser.parse(null);
+
+    expect(result.firstArgsKey).toBeNull();
+    expect(result.firstArgsValue).toBeNull();
+    expect(result.options).toEqual({});
+  });
+
+  test('.parse(\'prefix=/Level1\') returns a valid results', () => {
+    const result = ArgsParser.parse('prefix=/Level1');
+
+    expect(result.firstArgsKey).toBe('prefix');
+    expect(result.firstArgsValue).toBe('/Level1');
+  });
+
+  test('.parse(\'key, opt1=1, opt2=2\') returns a valid results', () => {
+    const result = ArgsParser.parse('key, opt1=1, opt2=2');
+
+    expect(result.firstArgsKey).toBe('key');
+    expect(result.firstArgsValue).toBeTruthy();
+
+    expect(Object.keys(result.options).length).toBe(3);
+    expect(result.options.key).toBeTruthy();
+    expect(result.options.opt1).toBe('1');
+    expect(result.options.opt2).toBe('2');
+  });
+
+  test('.parse(\'key, \') returns a valid results', () => {
+    const result = ArgsParser.parse('key, ');
+
+    expect(result.firstArgsKey).toBe('key');
+    expect(result.firstArgsValue).toBeTruthy();
+
+    expect(Object.keys(result.options).length).toBe(1);
+    expect(result.options.key).toBeTruthy();
+  });
+
+});

+ 72 - 0
packages/core/src/test/plugin/util/custom-tag-utils.test.js

@@ -0,0 +1,72 @@
+import rewire from 'rewire';
+
+import customTagUtils from '~/plugin/util/custom-tag-utils';
+
+const rewiredCustomTagUtils = rewire('../../../plugin/util/custom-tag-utils');
+
+describe('customTagUtils', () => {
+
+  test('exports TagContext', () => {
+    expect(customTagUtils.TagContext).not.toBeNull();
+    expect(typeof customTagUtils.TagContext).toBe('function');
+  });
+
+  test('exports ArgsParser', () => {
+    expect(customTagUtils.ArgsParser).not.toBeNull();
+    expect(typeof customTagUtils.ArgsParser).toBe('function');
+  });
+
+  test('exports OptionParser', () => {
+    expect(customTagUtils.OptionParser).not.toBeNull();
+    expect(typeof customTagUtils.OptionParser).toBe('function');
+  });
+
+  test('.createRandomStr(10) returns random string', () => {
+    // get private resource
+    const createRandomStr = rewiredCustomTagUtils.__get__('createRandomStr');
+    expect(createRandomStr(10)).toMatch(/^[a-z0-9]{10}$/);
+  });
+
+  test('.findTagAndReplace() returns default object when tagPattern is null', () => {
+    const htmlMock = jest.fn();
+    htmlMock.replace = jest.fn();
+
+    const result = customTagUtils.findTagAndReplace(null, '');
+
+    expect(result).toEqual({ html: '', tagContextMap: {} });
+    expect(htmlMock.replace).not.toHaveBeenCalled();
+  });
+
+  test('.findTagAndReplace() returns default object when html is null', () => {
+    const tagPatternMock = jest.fn();
+    tagPatternMock.source = jest.fn();
+
+    const result = customTagUtils.findTagAndReplace(tagPatternMock, null);
+
+    expect(result).toEqual({ html: null, tagContextMap: {} });
+    expect(tagPatternMock.source).not.toHaveBeenCalled();
+  });
+
+  test('.findTagAndReplace() works correctly', () => {
+    // setup mocks for private function
+    rewiredCustomTagUtils.__set__('createRandomStr', (length) => {
+      return 'dummyDomId';
+    });
+
+    const tagPattern = /ls|lsx/;
+    const html = '<section><h1>header</h1>\n$ls(/)</section>';
+
+    const result = rewiredCustomTagUtils.findTagAndReplace(tagPattern, html);
+
+    expect(result.html).toMatch(/<section><h1>header<\/h1>\n<div id="ls-dummyDomId"><\/div>/);
+    expect(result.tagContextMap).toEqual({
+      'ls-dummyDomId': {
+        tagExpression: '$ls(/)',
+        method: 'ls',
+        args: '/',
+      },
+    });
+  });
+
+
+});

+ 32 - 0
packages/core/src/test/plugin/util/option-parser.test.js

@@ -0,0 +1,32 @@
+import each from 'jest-each';
+
+import OptionParser from '~/plugin/util/option-parser';
+
+describe('option-parser', () => {
+
+  test('.parseRange(null) returns null', () => {
+    expect(OptionParser.parseRange(null)).toBeNull();
+  });
+
+  each`
+    arg
+    ${'aaa'}
+    ${'5++2'}
+    ${'5:+2'}
+  `.test('.parseRange(\'$arg\') returns null', ({ arg }) => {
+    expect(OptionParser.parseRange(arg)).toBeNull();
+  });
+
+  each`
+    arg       | start | end
+    ${'1'}    | ${1} | ${1}
+    ${'2:1'}  | ${2} | ${1}
+    ${'2:'}   | ${2} | ${-1}
+    ${'10:-3'}   | ${10} | ${-3}
+    ${'5+2'}   | ${5} | ${7}
+    ${'5+'}   | ${5} | ${5}
+  `.test('.parseRange(\'$arg\') returns { start: $start, end : $end }', ({ arg, start, end }) => {
+    expect(OptionParser.parseRange(arg)).toEqual({ start, end });
+  });
+
+});

+ 86 - 0
packages/core/src/test/service/localstorage-manager.test.js

@@ -0,0 +1,86 @@
+// eslint-disable-next-line import/no-unresolved
+import 'jest-localstorage-mock';
+
+import LocalStorageManager from '~/service/localstorage-manager';
+
+let localStorageManager = null;
+
+beforeEach(() => {
+  localStorageManager = LocalStorageManager.getInstance();
+
+  // == init jest-localstorage-mock
+  // reset the storage
+  localStorage.clear();
+  sessionStorage.clear();
+  // set preset data
+  sessionStorage.setItem('localstorage-manager-test', JSON.stringify({ foo: 'bar' }));
+  // reset mocks
+  localStorage.setItem.mockClear();
+  sessionStorage.setItem.mockClear();
+});
+
+describe('LocalStorageManager', () => {
+  test('.getInstance() returns the same instance', () => {
+    expect(LocalStorageManager.getInstance()).toBe(localStorageManager);
+  });
+
+  test('.retrieveFromSessionStorage() with unknown namespace returns the empty object', () => {
+    const item = localStorageManager.retrieveFromSessionStorage('unknown namespace');
+    expect(item).toEqual({});
+  });
+
+  test('.retrieveFromSessionStorage() without key returns the preset data', () => {
+    const item = localStorageManager.retrieveFromSessionStorage('localstorage-manager-test');
+    expect(item).toEqual({ foo: 'bar' });
+  });
+
+  test('.retrieveFromSessionStorage() with key returns the preset data', () => {
+    const item = localStorageManager.retrieveFromSessionStorage('localstorage-manager-test', 'foo');
+    expect(item).toBe('bar');
+  });
+
+  test('.saveToSessionStorage() without key works fine', () => {
+    localStorageManager.saveToSessionStorage(
+      'localstorage-manager-test',
+      { foo: { qux: 'quux' } },
+    );
+
+    expect(sessionStorage.__STORE__.length).toBe(1);
+    expect(sessionStorage.setItem)
+      .toHaveBeenLastCalledWith(
+        'localstorage-manager-test',
+        JSON.stringify({ foo: { qux: 'quux' } }),
+      );
+  });
+
+  test('.saveToSessionStorage() with key works fine', () => {
+    localStorageManager.saveToSessionStorage(
+      'localstorage-manager-test',
+      'baz',
+      { qux: 'quux' },
+    );
+
+    expect(sessionStorage.__STORE__.length).toBe(1);
+    expect(sessionStorage.setItem)
+      .toHaveBeenLastCalledWith(
+        'localstorage-manager-test',
+        JSON.stringify({ foo: 'bar', baz: { qux: 'quux' } }),
+      );
+  });
+
+  test('.saveToSessionStorage() with unknown key works fine', () => {
+    localStorageManager.saveToSessionStorage(
+      'localstorage-manager-test-unknown-key',
+      'baz',
+      { qux: 'quux' },
+    );
+
+    expect(sessionStorage.__STORE__.length).toBe(2);
+    expect(sessionStorage.setItem)
+      .toHaveBeenLastCalledWith(
+        'localstorage-manager-test-unknown-key',
+        JSON.stringify({ baz: { qux: 'quux' } }),
+      );
+  });
+
+});

+ 21 - 0
packages/core/src/test/util/env-utils.test.js

@@ -0,0 +1,21 @@
+import envUtils from '~/util/env-utils';
+
+
+describe('env-utils', () => {
+  describe('.toBoolean', () => {
+
+    test('should convert to true', () => {
+      expect(envUtils.toBoolean('true')).toBe(true);
+      expect(envUtils.toBoolean('True')).toBe(true);
+      expect(envUtils.toBoolean(1)).toBe(true);
+    });
+
+    test('should convert to false', () => {
+      expect(envUtils.toBoolean(undefined)).toBe(false);
+      expect(envUtils.toBoolean(null)).toBe(false);
+      expect(envUtils.toBoolean('false')).toBe(false);
+      expect(envUtils.toBoolean(0)).toBe(false);
+    });
+
+  });
+});

+ 26 - 0
packages/core/src/test/util/path-utils.test.js

@@ -0,0 +1,26 @@
+import pathUtils from '~/util/path-utils';
+
+
+describe('page-utils', () => {
+  describe('.normalizePath', () => {
+    test('should return the root path with empty string', () => {
+      expect(pathUtils.normalizePath('')).toBe('/');
+    });
+
+    test('should return the root path as is', () => {
+      expect(pathUtils.normalizePath('/')).toBe('/');
+    });
+
+    test('should add heading slash', () => {
+      expect(pathUtils.normalizePath('hoge/fuga')).toBe('/hoge/fuga');
+    });
+
+    test('should remove trailing slash', () => {
+      expect(pathUtils.normalizePath('/hoge/fuga/')).toBe('/hoge/fuga');
+    });
+
+    test('should remove unnecessary slashes', () => {
+      expect(pathUtils.normalizePath('//hoge/fuga//')).toBe('/hoge/fuga');
+    });
+  });
+});

+ 47 - 0
packages/core/src/util/basic-interceptor.js

@@ -0,0 +1,47 @@
+/**
+ * Basic Interceptor class
+ */
+class BasicInterceptor {
+
+  /**
+   * getter for id
+   */
+  getId() {
+    return this.constructor.name;
+  }
+
+  /**
+   * return whether this interceptor works by specified contextName
+   *
+   * @param {string} contextName
+   * @return {boolean}
+   */
+  isInterceptWhen(contextName) {
+    // implement this
+    return false;
+  }
+
+  /**
+   * return whether this interceptor processes in parallel mode or sequencial mode
+   * @return {boolean}
+   */
+  isProcessableParallel() {
+    // implement this
+    return true;
+  }
+
+  /**
+   * process method
+   *
+   * @param {string} contextName
+   * @param {any} args
+   * @return {Promise<any>}
+   */
+  process(contextName, ...args) {
+    // override this
+    return Promise.resolve(...args);
+  }
+
+}
+
+module.exports = BasicInterceptor;

+ 17 - 0
packages/core/src/util/env-utils.js

@@ -0,0 +1,17 @@
+/**
+ * convert to boolean
+ *
+ * @param {string} value
+ * @returns {boolean}
+ * @memberof envUtils
+ */
+function toBoolean(value) {
+  return /^(true|1)$/i.test(value);
+}
+
+/**
+ * @namespace envUtils
+ */
+module.exports = {
+  toBoolean,
+};

+ 120 - 0
packages/core/src/util/path-utils.js

@@ -0,0 +1,120 @@
+/**
+ * @private
+ *
+ *
+ * @param {string} path
+ * @returns {RegExpMatchArray}
+ * @memberof pathUtils
+ */
+function matchSlashes(path) {
+  // https://regex101.com/r/Z21fEd/5
+  return path.match(/^((\/+)?(.+?))(\/+)?$/);
+}
+
+/**
+ *
+ * @param {string} path
+ * @returns {boolean}
+ * @memberof pathUtils
+ */
+function hasHeadingSlash(path) {
+  if (path === '') {
+    return false;
+  }
+  const match = matchSlashes(path);
+  return (match[2] != null);
+}
+
+/**
+ *
+ * @param {string} path
+ * @returns {boolean}
+ * @memberof pathUtils
+ */
+function hasTrailingSlash(path) {
+  if (path === '') {
+    return false;
+  }
+  const match = matchSlashes(path);
+  return (match[4] != null);
+}
+
+/**
+ *
+ * @param {string} path
+ * @returns {string}
+ * @memberof pathUtils
+ */
+function addHeadingSlash(path) {
+  if (path === '/') {
+    return path;
+  }
+
+  if (!hasHeadingSlash(path)) {
+    return `/${path}`;
+  }
+  return path;
+}
+
+/**
+ *
+ * @param {string} path
+ * @returns {string}
+ * @memberof pathUtils
+ */
+function addTrailingSlash(path) {
+  if (path === '/') {
+    return path;
+  }
+
+  if (!hasTrailingSlash(path)) {
+    return `${path}/`;
+  }
+  return path;
+}
+
+/**
+ *
+ * @param {string} path
+ * @returns {string}
+ * @memberof pathUtils
+ */
+function removeTrailingSlash(path) {
+  if (path === '/') {
+    return path;
+  }
+
+  const match = matchSlashes(path);
+  return match[1];
+}
+
+/**
+ * A short-hand method to add heading slash and remove trailing slash.
+ *
+ * @param {string} path
+ * @returns {string}
+ * @memberof pathUtils
+ */
+function normalizePath(path) {
+  if (path === '' || path === '/') {
+    return '/';
+  }
+
+  const match = matchSlashes(path);
+  if (match == null) {
+    return '/';
+  }
+  return `/${match[3]}`;
+}
+
+/**
+ * @namespace pathUtils
+ */
+module.exports = {
+  hasHeadingSlash,
+  hasTrailingSlash,
+  addHeadingSlash,
+  addTrailingSlash,
+  removeTrailingSlash,
+  normalizePath,
+};

+ 14 - 0
packages/core/tsconfig.json

@@ -0,0 +1,14 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "~/*": ["./src/*"],
+    }
+  },
+  "exclude": [
+    "node_modules",
+    "dist",
+    "**/*.test.ts"
+  ]
+}

+ 0 - 1
packages/plugin-attachment-refs/package.json

@@ -23,7 +23,6 @@
   "dependencies": {
   "dependencies": {
     "browser-bunyan": "^1.6.3",
     "browser-bunyan": "^1.6.3",
     "bunyan": "^1.8.15",
     "bunyan": "^1.8.15",
-    "dotenv-flow": "^3.2.0",
     "growi-commons": "^5.0.4",
     "growi-commons": "^5.0.4",
     "http-errors": "^1.8.0",
     "http-errors": "^1.8.0",
     "react-images": "~1.0.0",
     "react-images": "~1.0.0",

+ 1 - 7
packages/plugin-pukiwiki-like-linker/package.json

@@ -20,15 +20,9 @@
     "test": ""
     "test": ""
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@types/jest": "^26.0.22",
     "browser-bunyan": "^1.6.3",
     "browser-bunyan": "^1.6.3",
-    "cross-env": "^7.0.0",
-    "jest": "^27.0.6",
-    "npm-run-all": "^4.1.2",
     "stylelint": "^13.2.0",
     "stylelint": "^13.2.0",
     "stylelint-config-recess-order": "^2.0.1",
     "stylelint-config-recess-order": "^2.0.1",
-    "ts-jest": "^27.0.4",
-    "tsc-alias": "^1.2.9",
-    "typescript": "^4.2.3"
+    "tsc-alias": "^1.2.9"
   }
   }
 }
 }

+ 1 - 7
packages/slack/package.json

@@ -16,18 +16,12 @@
     "axios": "^0.21.1",
     "axios": "^0.21.1",
     "browser-bunyan": "^1.6.3",
     "browser-bunyan": "^1.6.3",
     "bunyan": "^1.8.15",
     "bunyan": "^1.8.15",
-    "dotenv-flow": "^3.2.0",
     "extensible-custom-error": "^0.0.7",
     "extensible-custom-error": "^0.0.7",
     "http-errors": "^1.8.0",
     "http-errors": "^1.8.0",
     "universal-bunyan": "^0.9.2"
     "universal-bunyan": "^0.9.2"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",
-    "@types/jest": "^26.0.22",
-    "cross-env": "^7.0.0",
-    "jest": "^27.0.6",
-    "ts-jest": "^27.0.4",
-    "tsc-alias": "^1.2.9",
-    "typescript": "^4.2.3"
+    "tsc-alias": "^1.2.9"
   }
   }
 }
 }

+ 1 - 8
packages/slackbot-proxy/package.json

@@ -37,8 +37,6 @@
     "bunyan": "^1.8.15",
     "bunyan": "^1.8.15",
     "compression": "^1.7.4",
     "compression": "^1.7.4",
     "cookie-parser": "^1.4.5",
     "cookie-parser": "^1.4.5",
-    "cross-env": "^7.0.0",
-    "dotenv-flow": "^3.2.0",
     "express-bunyan-logger": "^1.3.3",
     "express-bunyan-logger": "^1.3.3",
     "extensible-custom-error": "^0.0.7",
     "extensible-custom-error": "^0.0.7",
     "helmet": "^4.6.0",
     "helmet": "^4.6.0",
@@ -55,13 +53,8 @@
     "@tsed/schema": "^6.43.0",
     "@tsed/schema": "^6.43.0",
     "bootstrap": "^5.0.2",
     "bootstrap": "^5.0.2",
     "browser-bunyan": "^1.6.3",
     "browser-bunyan": "^1.6.3",
-    "jest": "^27.0.6",
     "morgan": "^1.10.0",
     "morgan": "^1.10.0",
-    "ts-jest": "^27.0.4",
-    "ts-node": "^9.1.1",
     "ts-node-dev": "^1.1.6",
     "ts-node-dev": "^1.1.6",
-    "tsc-alias": "^1.2.9",
-    "tsconfig-paths": "^3.9.0",
-    "typescript": "^4.2.3"
+    "tsc-alias": "^1.2.9"
   }
   }
 }
 }

+ 124 - 44
yarn.lock

@@ -2885,6 +2885,11 @@
   resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
   resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
   integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
   integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
 
 
+"@types/rewire@^2.5.28":
+  version "2.5.28"
+  resolved "https://registry.yarnpkg.com/@types/rewire/-/rewire-2.5.28.tgz#ff34de38c4269fe74e2597195d4918c25d42ebad"
+  integrity sha512-uD0j/AQOa5le7afuK+u+woi8jNKF1vf3DN0H7LCJhft/lNNibUr7VcAesdgtWfEKveZol3ZG1CJqwx2Bhrnl8w==
+
 "@types/serve-static@*":
 "@types/serve-static@*":
   version "1.13.9"
   version "1.13.9"
   resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e"
   resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e"
@@ -3290,7 +3295,7 @@ acorn-globals@^6.0.0:
     acorn "^7.1.1"
     acorn "^7.1.1"
     acorn-walk "^7.1.1"
     acorn-walk "^7.1.1"
 
 
-acorn-jsx@^5.3.1:
+acorn-jsx@^5.2.0, acorn-jsx@^5.3.1:
   version "5.3.2"
   version "5.3.2"
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
@@ -6149,7 +6154,7 @@ cross-spawn@^3.0.0:
     lru-cache "^4.0.1"
     lru-cache "^4.0.1"
     which "^1.2.9"
     which "^1.2.9"
 
 
-cross-spawn@^5.0.1, cross-spawn@^5.1.0:
+cross-spawn@^5.0.1:
   version "5.1.0"
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
   dependencies:
   dependencies:
@@ -7581,6 +7586,13 @@ eslint-scope@^5.1.1:
     esrecurse "^4.3.0"
     esrecurse "^4.3.0"
     estraverse "^4.1.1"
     estraverse "^4.1.1"
 
 
+eslint-utils@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
+  integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
+  dependencies:
+    eslint-visitor-keys "^1.1.0"
+
 eslint-utils@^2.0.0, eslint-utils@^2.1.0:
 eslint-utils@^2.0.0, eslint-utils@^2.1.0:
   version "2.1.0"
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
   resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
@@ -7605,6 +7617,49 @@ eslint-visitor-keys@^2.0.0:
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8"
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8"
   integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
   integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
 
 
+eslint@^6.8.0:
+  version "6.8.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
+  integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
+  dependencies:
+    "@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 "^12.1.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.3"
+    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"
+
 eslint@^7.31.0:
 eslint@^7.31.0:
   version "7.31.0"
   version "7.31.0"
   resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.31.0.tgz#f972b539424bf2604907a970860732c5d99d3aca"
   resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.31.0.tgz#f972b539424bf2604907a970860732c5d99d3aca"
@@ -7651,6 +7706,15 @@ eslint@^7.31.0:
     text-table "^0.2.0"
     text-table "^0.2.0"
     v8-compile-cache "^2.0.3"
     v8-compile-cache "^2.0.3"
 
 
+espree@^6.1.2:
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a"
+  integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==
+  dependencies:
+    acorn "^7.1.1"
+    acorn-jsx "^5.2.0"
+    eslint-visitor-keys "^1.1.0"
+
 espree@^7.3.0, espree@^7.3.1:
 espree@^7.3.0, espree@^7.3.1:
   version "7.3.1"
   version "7.3.1"
   resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
   resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
@@ -7669,7 +7733,7 @@ esprima@^4.0.1:
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
 
 
-esquery@^1.4.0:
+esquery@^1.0.1, esquery@^1.4.0:
   version "1.4.0"
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
   resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
   integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
   integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
@@ -7707,7 +7771,7 @@ etag@1.8.1, etag@^1.8.1, etag@~1.8.1:
   version "1.8.1"
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
 
 
-event-stream@^3.3.2, event-stream@~3.3.0:
+event-stream@^3.3.2:
   version "3.3.4"
   version "3.3.4"
   resolved "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
   resolved "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
   dependencies:
   dependencies:
@@ -8124,7 +8188,7 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
 
-fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.4:
+fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6:
   version "2.0.6"
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
 
 
@@ -8929,6 +8993,13 @@ glob-parent@^3.1.0:
     is-glob "^3.1.0"
     is-glob "^3.1.0"
     path-dirname "^1.0.0"
     path-dirname "^1.0.0"
 
 
+glob-parent@^5.0.0, glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
 glob-parent@^5.1.0:
 glob-parent@^5.1.0:
   version "5.1.0"
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2"
@@ -8936,13 +9007,6 @@ glob-parent@^5.1.0:
   dependencies:
   dependencies:
     is-glob "^4.0.1"
     is-glob "^4.0.1"
 
 
-glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
-  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
-  dependencies:
-    is-glob "^4.0.1"
-
 glob@7.1.4, glob@^7.1.4:
 glob@7.1.4, glob@^7.1.4:
   version "7.1.4"
   version "7.1.4"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
@@ -9033,6 +9097,13 @@ globals@^11.1.0:
   version "11.11.0"
   version "11.11.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-11.11.0.tgz#dcf93757fa2de5486fbeed7118538adf789e9c2e"
   resolved "https://registry.yarnpkg.com/globals/-/globals-11.11.0.tgz#dcf93757fa2de5486fbeed7118538adf789e9c2e"
 
 
+globals@^12.1.0:
+  version "12.4.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8"
+  integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==
+  dependencies:
+    type-fest "^0.8.1"
+
 globals@^13.6.0, globals@^13.9.0:
 globals@^13.6.0, globals@^13.9.0:
   version "13.10.0"
   version "13.10.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-13.10.0.tgz#60ba56c3ac2ca845cfbf4faeca727ad9dd204676"
   resolved "https://registry.yarnpkg.com/globals/-/globals-13.10.0.tgz#60ba56c3ac2ca845cfbf4faeca727ad9dd204676"
@@ -10039,7 +10110,7 @@ inline-style-prefixer@^3.0.8:
     bowser "^1.7.3"
     bowser "^1.7.3"
     css-in-js-utils "^2.0.0"
     css-in-js-utils "^2.0.0"
 
 
-inquirer@^7.3.3:
+inquirer@^7.0.0, inquirer@^7.3.3:
   version "7.3.3"
   version "7.3.3"
   resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
   resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
   integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
   integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
@@ -10963,6 +11034,11 @@ jest-leak-detector@^27.0.6:
     jest-get-type "^27.0.6"
     jest-get-type "^27.0.6"
     pretty-format "^27.0.6"
     pretty-format "^27.0.6"
 
 
+jest-localstorage-mock@^2.4.14:
+  version "2.4.14"
+  resolved "https://registry.yarnpkg.com/jest-localstorage-mock/-/jest-localstorage-mock-2.4.14.tgz#f42f2ce66ac2675955d537c99f93cff807967d0f"
+  integrity sha512-B+Y0y3J4wBOHdmcFSicWmVYMFAZFbJvjs1EfRIzUJRg2UAK+YVVUgTn7/MdjENey5xbBKmraBmKY5EX+x1NJXA==
+
 jest-matcher-utils@^27.0.6:
 jest-matcher-utils@^27.0.6:
   version "27.0.6"
   version "27.0.6"
   resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.0.6.tgz#2a8da1e86c620b39459f4352eaa255f0d43e39a9"
   resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.0.6.tgz#2a8da1e86c620b39459f4352eaa255f0d43e39a9"
@@ -11692,6 +11768,13 @@ leven@^3.1.0:
   resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
   resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
   integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
   integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
 
 
+levn@^0.3.0, levn@~0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+  dependencies:
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+
 levn@^0.4.1:
 levn@^0.4.1:
   version "0.4.1"
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
   resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
@@ -11700,13 +11783,6 @@ levn@^0.4.1:
     prelude-ls "^1.2.1"
     prelude-ls "^1.2.1"
     type-check "~0.4.0"
     type-check "~0.4.0"
 
 
-levn@~0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
-  dependencies:
-    prelude-ls "~1.1.2"
-    type-check "~0.3.2"
-
 libnpmaccess@^4.0.1:
 libnpmaccess@^4.0.1:
   version "4.0.1"
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-4.0.1.tgz#17e842e03bef759854adf6eb6c2ede32e782639f"
   resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-4.0.1.tgz#17e842e03bef759854adf6eb6c2ede32e782639f"
@@ -13626,20 +13702,6 @@ npm-registry-fetch@^9.0.0:
     minizlib "^2.0.0"
     minizlib "^2.0.0"
     npm-package-arg "^8.0.0"
     npm-package-arg "^8.0.0"
 
 
-npm-run-all@^4.1.2:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.2.tgz#90d62d078792d20669139e718621186656cea056"
-  dependencies:
-    ansi-styles "^3.2.0"
-    chalk "^2.1.0"
-    cross-spawn "^5.1.0"
-    memorystream "^0.3.1"
-    minimatch "^3.0.4"
-    ps-tree "^1.1.0"
-    read-pkg "^3.0.0"
-    shell-quote "^1.6.1"
-    string.prototype.padend "^3.0.0"
-
 npm-run-all@^4.1.5:
 npm-run-all@^4.1.5:
   version "4.1.5"
   version "4.1.5"
   resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba"
   resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba"
@@ -14058,6 +14120,18 @@ optionator@^0.8.1:
     type-check "~0.3.2"
     type-check "~0.3.2"
     wordwrap "~1.0.0"
     wordwrap "~1.0.0"
 
 
+optionator@^0.8.3:
+  version "0.8.3"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
+  integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+  dependencies:
+    deep-is "~0.1.3"
+    fast-levenshtein "~2.0.6"
+    levn "~0.3.0"
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+    word-wrap "~1.2.3"
+
 optionator@^0.9.1:
 optionator@^0.9.1:
   version "0.9.1"
   version "0.9.1"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
@@ -15576,12 +15650,6 @@ prr@~1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
   resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
 
 
-ps-tree@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014"
-  dependencies:
-    event-stream "~3.3.0"
-
 pseudomap@^1.0.2:
 pseudomap@^1.0.2:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
   resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
@@ -16544,6 +16612,11 @@ regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.1:
     call-bind "^1.0.2"
     call-bind "^1.0.2"
     define-properties "^1.1.3"
     define-properties "^1.1.3"
 
 
+regexpp@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
+  integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
+
 regexpp@^3.1.0:
 regexpp@^3.1.0:
   version "3.2.0"
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
@@ -16881,6 +16954,13 @@ reveal.js@^3.5.0:
   version "3.6.0"
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/reveal.js/-/reveal.js-3.6.0.tgz#ce0e64f30cbebd6e5ce885c2f384085c5e5821e8"
   resolved "https://registry.yarnpkg.com/reveal.js/-/reveal.js-3.6.0.tgz#ce0e64f30cbebd6e5ce885c2f384085c5e5821e8"
 
 
+rewire@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/rewire/-/rewire-5.0.0.tgz#c4e6558206863758f6234d8f11321793ada2dbff"
+  integrity sha512-1zfitNyp9RH5UDyGGLe9/1N0bMlPQ0WrX0Tmg11kMHBpqwPJI4gfPpP7YngFyLbFmhXh19SToAG0sKKEFcOIJA==
+  dependencies:
+    eslint "^6.8.0"
+
 rgb-regex@^1.0.1:
 rgb-regex@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
   resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
@@ -17165,7 +17245,7 @@ semver@^6.0.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b"
   integrity sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==
   integrity sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==
 
 
-semver@^6.3.0:
+semver@^6.1.2, semver@^6.3.0:
   version "6.3.0"
   version "6.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
@@ -18334,7 +18414,7 @@ strip-json-comments@^2.0.0, strip-json-comments@~2.0.1:
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
   integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
   integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
 
 
-strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
+strip-json-comments@^3.0.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
   version "3.1.1"
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
@@ -18721,7 +18801,7 @@ table@^4.0.1:
     slice-ansi "1.0.0"
     slice-ansi "1.0.0"
     string-width "^2.1.1"
     string-width "^2.1.1"
 
 
-table@^5.4.6:
+table@^5.2.3, table@^5.4.6:
   version "5.4.6"
   version "5.4.6"
   resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
   resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
   integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
   integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
@@ -20321,7 +20401,7 @@ window-size@^0.2.0:
   version "0.2.0"
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
   resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
 
 
-word-wrap@^1.2.3:
+word-wrap@^1.2.3, word-wrap@~1.2.3:
   version "1.2.3"
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
   integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
   integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==