Index: frontend/node_modules/ignore/LICENSE-MIT
===================================================================
--- frontend/node_modules/ignore/LICENSE-MIT	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/ignore/LICENSE-MIT	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,21 @@
+Copyright (c) 2013 Kael Zhang <i@kael.me>, contributors
+http://kael.me/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Index: frontend/node_modules/ignore/README.md
===================================================================
--- frontend/node_modules/ignore/README.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/ignore/README.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,412 @@
+<table><thead>
+  <tr>
+    <th>Linux</th>
+    <th>OS X</th>
+    <th>Windows</th>
+    <th>Coverage</th>
+    <th>Downloads</th>
+  </tr>
+</thead><tbody><tr>
+  <td colspan="2" align="center">
+    <a href="https://github.com/kaelzhang/node-ignore/actions/workflows/nodejs.yml">
+    <img
+      src="https://github.com/kaelzhang/node-ignore/actions/workflows/nodejs.yml/badge.svg"
+      alt="Build Status" /></a>
+  </td>
+  <td align="center">
+    <a href="https://ci.appveyor.com/project/kaelzhang/node-ignore">
+    <img
+      src="https://ci.appveyor.com/api/projects/status/github/kaelzhang/node-ignore?branch=master&svg=true"
+      alt="Windows Build Status" /></a>
+  </td>
+  <td align="center">
+    <a href="https://codecov.io/gh/kaelzhang/node-ignore">
+    <img
+      src="https://codecov.io/gh/kaelzhang/node-ignore/branch/master/graph/badge.svg"
+      alt="Coverage Status" /></a>
+  </td>
+  <td align="center">
+    <a href="https://www.npmjs.org/package/ignore">
+    <img
+      src="http://img.shields.io/npm/dm/ignore.svg"
+      alt="npm module downloads per month" /></a>
+  </td>
+</tr></tbody></table>
+
+# ignore
+
+`ignore` is a manager, filter and parser which implemented in pure JavaScript according to the [.gitignore spec 2.22.1](http://git-scm.com/docs/gitignore).
+
+`ignore` is used by eslint, gitbook and [many others](https://www.npmjs.com/browse/depended/ignore).
+
+Pay **ATTENTION** that [`minimatch`](https://www.npmjs.org/package/minimatch) (which used by `fstream-ignore`) does not follow the gitignore spec.
+
+To filter filenames according to a .gitignore file, I recommend this npm package, `ignore`.
+
+To parse an `.npmignore` file, you should use `minimatch`, because an `.npmignore` file is parsed by npm using `minimatch` and it does not work in the .gitignore way.
+
+### Tested on
+
+`ignore` is fully tested, and has more than **five hundreds** of unit tests.
+
+- Linux + Node: `0.8` - `7.x`
+- Windows + Node: `0.10` - `7.x`, node < `0.10` is not tested due to the lack of support of appveyor.
+
+Actually, `ignore` does not rely on any versions of node specially.
+
+Since `4.0.0`, ignore will no longer support `node < 6` by default, to use in node < 6, `require('ignore/legacy')`. For details, see [CHANGELOG](https://github.com/kaelzhang/node-ignore/blob/master/CHANGELOG.md).
+
+## Table Of Main Contents
+
+- [Usage](#usage)
+- [`Pathname` Conventions](#pathname-conventions)
+- See Also:
+  - [`glob-gitignore`](https://www.npmjs.com/package/glob-gitignore) matches files using patterns and filters them according to gitignore rules.
+- [Upgrade Guide](#upgrade-guide)
+
+## Install
+
+```sh
+npm i ignore
+```
+
+## Usage
+
+```js
+import ignore from 'ignore'
+const ig = ignore().add(['.abc/*', '!.abc/d/'])
+```
+
+### Filter the given paths
+
+```js
+const paths = [
+  '.abc/a.js',    // filtered out
+  '.abc/d/e.js'   // included
+]
+
+ig.filter(paths)        // ['.abc/d/e.js']
+ig.ignores('.abc/a.js') // true
+```
+
+### As the filter function
+
+```js
+paths.filter(ig.createFilter()); // ['.abc/d/e.js']
+```
+
+### Win32 paths will be handled
+
+```js
+ig.filter(['.abc\\a.js', '.abc\\d\\e.js'])
+// if the code above runs on windows, the result will be
+// ['.abc\\d\\e.js']
+```
+
+## Why another ignore?
+
+- `ignore` is a standalone module, and is much simpler so that it could easy work with other programs, unlike [isaacs](https://npmjs.org/~isaacs)'s [fstream-ignore](https://npmjs.org/package/fstream-ignore) which must work with the modules of the fstream family.
+
+- `ignore` only contains utility methods to filter paths according to the specified ignore rules, so
+  - `ignore` never try to find out ignore rules by traversing directories or fetching from git configurations.
+  - `ignore` don't cares about sub-modules of git projects.
+
+- Exactly according to [gitignore man page](http://git-scm.com/docs/gitignore), fixes some known matching issues of fstream-ignore, such as:
+  - '`/*.js`' should only match '`a.js`', but not '`abc/a.js`'.
+  - '`**/foo`' should match '`foo`' anywhere.
+  - Prevent re-including a file if a parent directory of that file is excluded.
+  - Handle trailing whitespaces:
+    - `'a '`(one space) should not match `'a  '`(two spaces).
+    - `'a \ '` matches `'a  '`
+  - All test cases are verified with the result of `git check-ignore`.
+
+# Methods
+
+## .add(pattern: string | Ignore): this
+## .add(patterns: Array<string | Ignore>): this
+
+- **pattern** `String | Ignore` An ignore pattern string, or the `Ignore` instance
+- **patterns** `Array<String | Ignore>` Array of ignore patterns.
+
+Adds a rule or several rules to the current manager.
+
+Returns `this`
+
+Notice that a line starting with `'#'`(hash) is treated as a comment. Put a backslash (`'\'`) in front of the first hash for patterns that begin with a hash, if you want to ignore a file with a hash at the beginning of the filename.
+
+```js
+ignore().add('#abc').ignores('#abc')    // false
+ignore().add('\\#abc').ignores('#abc')   // true
+```
+
+`pattern` could either be a line of ignore pattern or a string of multiple ignore patterns, which means we could just `ignore().add()` the content of a ignore file:
+
+```js
+ignore()
+.add(fs.readFileSync(filenameOfGitignore).toString())
+.filter(filenames)
+```
+
+`pattern` could also be an `ignore` instance, so that we could easily inherit the rules of another `Ignore` instance.
+
+## <strike>.addIgnoreFile(path)</strike>
+
+REMOVED in `3.x` for now.
+
+To upgrade `ignore@2.x` up to `3.x`, use
+
+```js
+import fs from 'fs'
+
+if (fs.existsSync(filename)) {
+  ignore().add(fs.readFileSync(filename).toString())
+}
+```
+
+instead.
+
+## .filter(paths: Array&lt;Pathname&gt;): Array&lt;Pathname&gt;
+
+```ts
+type Pathname = string
+```
+
+Filters the given array of pathnames, and returns the filtered array.
+
+- **paths** `Array.<Pathname>` The array of `pathname`s to be filtered.
+
+### `Pathname` Conventions:
+
+#### 1. `Pathname` should be a `path.relative()`d pathname
+
+`Pathname` should be a string that have been `path.join()`ed, or the return value of `path.relative()` to the current directory,
+
+```js
+// WRONG, an error will be thrown
+ig.ignores('./abc')
+
+// WRONG, for it will never happen, and an error will be thrown
+// If the gitignore rule locates at the root directory,
+// `'/abc'` should be changed to `'abc'`.
+// ```
+// path.relative('/', '/abc')  -> 'abc'
+// ```
+ig.ignores('/abc')
+
+// WRONG, that it is an absolute path on Windows, an error will be thrown
+ig.ignores('C:\\abc')
+
+// Right
+ig.ignores('abc')
+
+// Right
+ig.ignores(path.join('./abc'))  // path.join('./abc') -> 'abc'
+```
+
+In other words, each `Pathname` here should be a relative path to the directory of the gitignore rules.
+
+Suppose the dir structure is:
+
+```
+/path/to/your/repo
+    |-- a
+    |   |-- a.js
+    |
+    |-- .b
+    |
+    |-- .c
+         |-- .DS_store
+```
+
+Then the `paths` might be like this:
+
+```js
+[
+  'a/a.js'
+  '.b',
+  '.c/.DS_store'
+]
+```
+
+#### 2. filenames and dirnames
+
+`node-ignore` does NO `fs.stat` during path matching, so for the example below:
+
+```js
+// First, we add a ignore pattern to ignore a directory
+ig.add('config/')
+
+// `ig` does NOT know if 'config', in the real world,
+//   is a normal file, directory or something.
+
+ig.ignores('config')
+// `ig` treats `config` as a file, so it returns `false`
+
+ig.ignores('config/')
+// returns `true`
+```
+
+Specially for people who develop some library based on `node-ignore`, it is important to understand that.
+
+Usually, you could use [`glob`](http://npmjs.org/package/glob) with `option.mark = true` to fetch the structure of the current directory:
+
+```js
+import glob from 'glob'
+
+glob('**', {
+  // Adds a / character to directory matches.
+  mark: true
+}, (err, files) => {
+  if (err) {
+    return console.error(err)
+  }
+
+  let filtered = ignore().add(patterns).filter(files)
+  console.log(filtered)
+})
+```
+
+## .ignores(pathname: Pathname): boolean
+
+> new in 3.2.0
+
+Returns `Boolean` whether `pathname` should be ignored.
+
+```js
+ig.ignores('.abc/a.js')    // true
+```
+
+## .createFilter()
+
+Creates a filter function which could filter an array of paths with `Array.prototype.filter`.
+
+Returns `function(path)` the filter function.
+
+## .test(pathname: Pathname) since 5.0.0
+
+Returns `TestResult`
+
+```ts
+interface TestResult {
+  ignored: boolean
+  // true if the `pathname` is finally unignored by some negative pattern
+  unignored: boolean
+}
+```
+
+- `{ignored: true, unignored: false}`: the `pathname` is ignored
+- `{ignored: false, unignored: true}`: the `pathname` is unignored
+- `{ignored: false, unignored: false}`: the `pathname` is never matched by any ignore rules.
+
+## static `ignore.isPathValid(pathname): boolean` since 5.0.0
+
+Check whether the `pathname` is an valid `path.relative()`d path according to the [convention](#1-pathname-should-be-a-pathrelatived-pathname).
+
+This method is **NOT** used to check if an ignore pattern is valid.
+
+```js
+ignore.isPathValid('./foo')  // false
+```
+
+## ignore(options)
+
+### `options.ignorecase` since 4.0.0
+
+Similar as the `core.ignorecase` option of [git-config](https://git-scm.com/docs/git-config), `node-ignore` will be case insensitive if `options.ignorecase` is set to `true` (the default value), otherwise case sensitive.
+
+```js
+const ig = ignore({
+  ignorecase: false
+})
+
+ig.add('*.png')
+
+ig.ignores('*.PNG')  // false
+```
+
+### `options.ignoreCase?: boolean` since 5.2.0
+
+Which is alternative to `options.ignoreCase`
+
+### `options.allowRelativePaths?: boolean` since 5.2.0
+
+This option brings backward compatibility with projects which based on `ignore@4.x`. If `options.allowRelativePaths` is `true`, `ignore` will not check whether the given path to be tested is [`path.relative()`d](#pathname-conventions).
+
+However, passing a relative path, such as `'./foo'` or `'../foo'`, to test if it is ignored or not is not a good practise, which might lead to unexpected behavior
+
+```js
+ignore({
+  allowRelativePaths: true
+}).ignores('../foo/bar.js') // And it will not throw
+```
+
+****
+
+# Upgrade Guide
+
+## Upgrade 4.x -> 5.x
+
+Since `5.0.0`, if an invalid `Pathname` passed into `ig.ignores()`, an error will be thrown, unless `options.allowRelative = true` is passed to the `Ignore` factory.
+
+While `ignore < 5.0.0` did not make sure what the return value was, as well as
+
+```ts
+.ignores(pathname: Pathname): boolean
+
+.filter(pathnames: Array<Pathname>): Array<Pathname>
+
+.createFilter(): (pathname: Pathname) => boolean
+
+.test(pathname: Pathname): {ignored: boolean, unignored: boolean}
+```
+
+See the convention [here](#1-pathname-should-be-a-pathrelatived-pathname) for details.
+
+If there are invalid pathnames, the conversion and filtration should be done by users.
+
+```js
+import {isPathValid} from 'ignore' // introduced in 5.0.0
+
+const paths = [
+  // invalid
+  //////////////////
+  '',
+  false,
+  '../foo',
+  '.',
+  //////////////////
+
+  // valid
+  'foo'
+]
+.filter(isValidPath)
+
+ig.filter(paths)
+```
+
+## Upgrade 3.x -> 4.x
+
+Since `4.0.0`, `ignore` will no longer support node < 6, to use `ignore` in node < 6:
+
+```js
+var ignore = require('ignore/legacy')
+```
+
+## Upgrade 2.x -> 3.x
+
+- All `options` of 2.x are unnecessary and removed, so just remove them.
+- `ignore()` instance is no longer an [`EventEmitter`](nodejs.org/api/events.html), and all events are unnecessary and removed.
+- `.addIgnoreFile()` is removed, see the [.addIgnoreFile](#addignorefilepath) section for details.
+
+****
+
+# Collaborators
+
+- [@whitecolor](https://github.com/whitecolor) *Alex*
+- [@SamyPesse](https://github.com/SamyPesse) *Samy Pessé*
+- [@azproduction](https://github.com/azproduction) *Mikhail Davydov*
+- [@TrySound](https://github.com/TrySound) *Bogdan Chadkin*
+- [@JanMattner](https://github.com/JanMattner) *Jan Mattner*
+- [@ntwb](https://github.com/ntwb) *Stephen Edgar*
+- [@kasperisager](https://github.com/kasperisager) *Kasper Isager*
+- [@sandersn](https://github.com/sandersn) *Nathan Shively-Sanders*
Index: frontend/node_modules/ignore/index.d.ts
===================================================================
--- frontend/node_modules/ignore/index.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/ignore/index.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,61 @@
+type Pathname = string
+
+interface TestResult {
+  ignored: boolean
+  unignored: boolean
+}
+
+export interface Ignore {
+  /**
+   * Adds one or several rules to the current manager.
+   * @param  {string[]} patterns
+   * @returns IgnoreBase
+   */
+  add(patterns: string | Ignore | readonly (string | Ignore)[]): this
+
+  /**
+   * Filters the given array of pathnames, and returns the filtered array.
+   * NOTICE that each path here should be a relative path to the root of your repository.
+   * @param paths the array of paths to be filtered.
+   * @returns The filtered array of paths
+   */
+  filter(pathnames: readonly Pathname[]): Pathname[]
+
+  /**
+   * Creates a filter function which could filter
+   * an array of paths with Array.prototype.filter.
+   */
+  createFilter(): (pathname: Pathname) => boolean
+
+  /**
+   * Returns Boolean whether pathname should be ignored.
+   * @param  {string} pathname a path to check
+   * @returns boolean
+   */
+  ignores(pathname: Pathname): boolean
+
+  /**
+   * Returns whether pathname should be ignored or unignored
+   * @param  {string} pathname a path to check
+   * @returns TestResult
+   */
+  test(pathname: Pathname): TestResult
+}
+
+export interface Options {
+  ignorecase?: boolean
+  // For compatibility
+  ignoreCase?: boolean
+  allowRelativePaths?: boolean
+}
+
+/**
+ * Creates new ignore manager.
+ */
+declare function ignore(options?: Options): Ignore
+
+declare namespace ignore {
+  export function isPathValid (pathname: string): boolean
+}
+
+export default ignore
Index: frontend/node_modules/ignore/index.js
===================================================================
--- frontend/node_modules/ignore/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/ignore/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,636 @@
+// A simple implementation of make-array
+function makeArray (subject) {
+  return Array.isArray(subject)
+    ? subject
+    : [subject]
+}
+
+const EMPTY = ''
+const SPACE = ' '
+const ESCAPE = '\\'
+const REGEX_TEST_BLANK_LINE = /^\s+$/
+const REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/
+const REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/
+const REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/
+const REGEX_SPLITALL_CRLF = /\r?\n/g
+// /foo,
+// ./foo,
+// ../foo,
+// .
+// ..
+const REGEX_TEST_INVALID_PATH = /^\.*\/|^\.+$/
+
+const SLASH = '/'
+
+// Do not use ternary expression here, since "istanbul ignore next" is buggy
+let TMP_KEY_IGNORE = 'node-ignore'
+/* istanbul ignore else */
+if (typeof Symbol !== 'undefined') {
+  TMP_KEY_IGNORE = Symbol.for('node-ignore')
+}
+const KEY_IGNORE = TMP_KEY_IGNORE
+
+const define = (object, key, value) =>
+  Object.defineProperty(object, key, {value})
+
+const REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g
+
+const RETURN_FALSE = () => false
+
+// Sanitize the range of a regular expression
+// The cases are complicated, see test cases for details
+const sanitizeRange = range => range.replace(
+  REGEX_REGEXP_RANGE,
+  (match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0)
+    ? match
+    // Invalid range (out of order) which is ok for gitignore rules but
+    //   fatal for JavaScript regular expression, so eliminate it.
+    : EMPTY
+)
+
+// See fixtures #59
+const cleanRangeBackSlash = slashes => {
+  const {length} = slashes
+  return slashes.slice(0, length - length % 2)
+}
+
+// > If the pattern ends with a slash,
+// > it is removed for the purpose of the following description,
+// > but it would only find a match with a directory.
+// > In other words, foo/ will match a directory foo and paths underneath it,
+// > but will not match a regular file or a symbolic link foo
+// >  (this is consistent with the way how pathspec works in general in Git).
+// '`foo/`' will not match regular file '`foo`' or symbolic link '`foo`'
+// -> ignore-rules will not deal with it, because it costs extra `fs.stat` call
+//      you could use option `mark: true` with `glob`
+
+// '`foo/`' should not continue with the '`..`'
+const REPLACERS = [
+
+  [
+    // remove BOM
+    // TODO:
+    // Other similar zero-width characters?
+    /^\uFEFF/,
+    () => EMPTY
+  ],
+
+  // > Trailing spaces are ignored unless they are quoted with backslash ("\")
+  [
+    // (a\ ) -> (a )
+    // (a  ) -> (a)
+    // (a ) -> (a)
+    // (a \ ) -> (a  )
+    /((?:\\\\)*?)(\\?\s+)$/,
+    (_, m1, m2) => m1 + (
+      m2.indexOf('\\') === 0
+        ? SPACE
+        : EMPTY
+    )
+  ],
+
+  // replace (\ ) with ' '
+  // (\ ) -> ' '
+  // (\\ ) -> '\\ '
+  // (\\\ ) -> '\\ '
+  [
+    /(\\+?)\s/g,
+    (_, m1) => {
+      const {length} = m1
+      return m1.slice(0, length - length % 2) + SPACE
+    }
+  ],
+
+  // Escape metacharacters
+  // which is written down by users but means special for regular expressions.
+
+  // > There are 12 characters with special meanings:
+  // > - the backslash \,
+  // > - the caret ^,
+  // > - the dollar sign $,
+  // > - the period or dot .,
+  // > - the vertical bar or pipe symbol |,
+  // > - the question mark ?,
+  // > - the asterisk or star *,
+  // > - the plus sign +,
+  // > - the opening parenthesis (,
+  // > - the closing parenthesis ),
+  // > - and the opening square bracket [,
+  // > - the opening curly brace {,
+  // > These special characters are often called "metacharacters".
+  [
+    /[\\$.|*+(){^]/g,
+    match => `\\${match}`
+  ],
+
+  [
+    // > a question mark (?) matches a single character
+    /(?!\\)\?/g,
+    () => '[^/]'
+  ],
+
+  // leading slash
+  [
+
+    // > A leading slash matches the beginning of the pathname.
+    // > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
+    // A leading slash matches the beginning of the pathname
+    /^\//,
+    () => '^'
+  ],
+
+  // replace special metacharacter slash after the leading slash
+  [
+    /\//g,
+    () => '\\/'
+  ],
+
+  [
+    // > A leading "**" followed by a slash means match in all directories.
+    // > For example, "**/foo" matches file or directory "foo" anywhere,
+    // > the same as pattern "foo".
+    // > "**/foo/bar" matches file or directory "bar" anywhere that is directly
+    // >   under directory "foo".
+    // Notice that the '*'s have been replaced as '\\*'
+    /^\^*\\\*\\\*\\\//,
+
+    // '**/foo' <-> 'foo'
+    () => '^(?:.*\\/)?'
+  ],
+
+  // starting
+  [
+    // there will be no leading '/'
+    //   (which has been replaced by section "leading slash")
+    // If starts with '**', adding a '^' to the regular expression also works
+    /^(?=[^^])/,
+    function startingReplacer () {
+      // If has a slash `/` at the beginning or middle
+      return !/\/(?!$)/.test(this)
+        // > Prior to 2.22.1
+        // > If the pattern does not contain a slash /,
+        // >   Git treats it as a shell glob pattern
+        // Actually, if there is only a trailing slash,
+        //   git also treats it as a shell glob pattern
+
+        // After 2.22.1 (compatible but clearer)
+        // > If there is a separator at the beginning or middle (or both)
+        // > of the pattern, then the pattern is relative to the directory
+        // > level of the particular .gitignore file itself.
+        // > Otherwise the pattern may also match at any level below
+        // > the .gitignore level.
+        ? '(?:^|\\/)'
+
+        // > Otherwise, Git treats the pattern as a shell glob suitable for
+        // >   consumption by fnmatch(3)
+        : '^'
+    }
+  ],
+
+  // two globstars
+  [
+    // Use lookahead assertions so that we could match more than one `'/**'`
+    /\\\/\\\*\\\*(?=\\\/|$)/g,
+
+    // Zero, one or several directories
+    // should not use '*', or it will be replaced by the next replacer
+
+    // Check if it is not the last `'/**'`
+    (_, index, str) => index + 6 < str.length
+
+      // case: /**/
+      // > A slash followed by two consecutive asterisks then a slash matches
+      // >   zero or more directories.
+      // > For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on.
+      // '/**/'
+      ? '(?:\\/[^\\/]+)*'
+
+      // case: /**
+      // > A trailing `"/**"` matches everything inside.
+
+      // #21: everything inside but it should not include the current folder
+      : '\\/.+'
+  ],
+
+  // normal intermediate wildcards
+  [
+    // Never replace escaped '*'
+    // ignore rule '\*' will match the path '*'
+
+    // 'abc.*/' -> go
+    // 'abc.*'  -> skip this rule,
+    //    coz trailing single wildcard will be handed by [trailing wildcard]
+    /(^|[^\\]+)(\\\*)+(?=.+)/g,
+
+    // '*.js' matches '.js'
+    // '*.js' doesn't match 'abc'
+    (_, p1, p2) => {
+      // 1.
+      // > An asterisk "*" matches anything except a slash.
+      // 2.
+      // > Other consecutive asterisks are considered regular asterisks
+      // > and will match according to the previous rules.
+      const unescaped = p2.replace(/\\\*/g, '[^\\/]*')
+      return p1 + unescaped
+    }
+  ],
+
+  [
+    // unescape, revert step 3 except for back slash
+    // For example, if a user escape a '\\*',
+    // after step 3, the result will be '\\\\\\*'
+    /\\\\\\(?=[$.|*+(){^])/g,
+    () => ESCAPE
+  ],
+
+  [
+    // '\\\\' -> '\\'
+    /\\\\/g,
+    () => ESCAPE
+  ],
+
+  [
+    // > The range notation, e.g. [a-zA-Z],
+    // > can be used to match one of the characters in a range.
+
+    // `\` is escaped by step 3
+    /(\\)?\[([^\]/]*?)(\\*)($|\])/g,
+    (match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE
+      // '\\[bar]' -> '\\\\[bar\\]'
+      ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}`
+      : close === ']'
+        ? endEscape.length % 2 === 0
+          // A normal case, and it is a range notation
+          // '[bar]'
+          // '[bar\\\\]'
+          ? `[${sanitizeRange(range)}${endEscape}]`
+          // Invalid range notaton
+          // '[bar\\]' -> '[bar\\\\]'
+          : '[]'
+        : '[]'
+  ],
+
+  // ending
+  [
+    // 'js' will not match 'js.'
+    // 'ab' will not match 'abc'
+    /(?:[^*])$/,
+
+    // WTF!
+    // https://git-scm.com/docs/gitignore
+    // changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)
+    // which re-fixes #24, #38
+
+    // > If there is a separator at the end of the pattern then the pattern
+    // > will only match directories, otherwise the pattern can match both
+    // > files and directories.
+
+    // 'js*' will not match 'a.js'
+    // 'js/' will not match 'a.js'
+    // 'js' will match 'a.js' and 'a.js/'
+    match => /\/$/.test(match)
+      // foo/ will not match 'foo'
+      ? `${match}$`
+      // foo matches 'foo' and 'foo/'
+      : `${match}(?=$|\\/$)`
+  ],
+
+  // trailing wildcard
+  [
+    /(\^|\\\/)?\\\*$/,
+    (_, p1) => {
+      const prefix = p1
+        // '\^':
+        // '/*' does not match EMPTY
+        // '/*' does not match everything
+
+        // '\\\/':
+        // 'abc/*' does not match 'abc/'
+        ? `${p1}[^/]+`
+
+        // 'a*' matches 'a'
+        // 'a*' matches 'aa'
+        : '[^/]*'
+
+      return `${prefix}(?=$|\\/$)`
+    }
+  ],
+]
+
+// A simple cache, because an ignore rule only has only one certain meaning
+const regexCache = Object.create(null)
+
+// @param {pattern}
+const makeRegex = (pattern, ignoreCase) => {
+  let source = regexCache[pattern]
+
+  if (!source) {
+    source = REPLACERS.reduce(
+      (prev, [matcher, replacer]) =>
+        prev.replace(matcher, replacer.bind(pattern)),
+      pattern
+    )
+    regexCache[pattern] = source
+  }
+
+  return ignoreCase
+    ? new RegExp(source, 'i')
+    : new RegExp(source)
+}
+
+const isString = subject => typeof subject === 'string'
+
+// > A blank line matches no files, so it can serve as a separator for readability.
+const checkPattern = pattern => pattern
+  && isString(pattern)
+  && !REGEX_TEST_BLANK_LINE.test(pattern)
+  && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern)
+
+  // > A line starting with # serves as a comment.
+  && pattern.indexOf('#') !== 0
+
+const splitPattern = pattern => pattern.split(REGEX_SPLITALL_CRLF)
+
+class IgnoreRule {
+  constructor (
+    origin,
+    pattern,
+    negative,
+    regex
+  ) {
+    this.origin = origin
+    this.pattern = pattern
+    this.negative = negative
+    this.regex = regex
+  }
+}
+
+const createRule = (pattern, ignoreCase) => {
+  const origin = pattern
+  let negative = false
+
+  // > An optional prefix "!" which negates the pattern;
+  if (pattern.indexOf('!') === 0) {
+    negative = true
+    pattern = pattern.substr(1)
+  }
+
+  pattern = pattern
+  // > Put a backslash ("\") in front of the first "!" for patterns that
+  // >   begin with a literal "!", for example, `"\!important!.txt"`.
+  .replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, '!')
+  // > Put a backslash ("\") in front of the first hash for patterns that
+  // >   begin with a hash.
+  .replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, '#')
+
+  const regex = makeRegex(pattern, ignoreCase)
+
+  return new IgnoreRule(
+    origin,
+    pattern,
+    negative,
+    regex
+  )
+}
+
+const throwError = (message, Ctor) => {
+  throw new Ctor(message)
+}
+
+const checkPath = (path, originalPath, doThrow) => {
+  if (!isString(path)) {
+    return doThrow(
+      `path must be a string, but got \`${originalPath}\``,
+      TypeError
+    )
+  }
+
+  // We don't know if we should ignore EMPTY, so throw
+  if (!path) {
+    return doThrow(`path must not be empty`, TypeError)
+  }
+
+  // Check if it is a relative path
+  if (checkPath.isNotRelative(path)) {
+    const r = '`path.relative()`d'
+    return doThrow(
+      `path should be a ${r} string, but got "${originalPath}"`,
+      RangeError
+    )
+  }
+
+  return true
+}
+
+const isNotRelative = path => REGEX_TEST_INVALID_PATH.test(path)
+
+checkPath.isNotRelative = isNotRelative
+checkPath.convert = p => p
+
+class Ignore {
+  constructor ({
+    ignorecase = true,
+    ignoreCase = ignorecase,
+    allowRelativePaths = false
+  } = {}) {
+    define(this, KEY_IGNORE, true)
+
+    this._rules = []
+    this._ignoreCase = ignoreCase
+    this._allowRelativePaths = allowRelativePaths
+    this._initCache()
+  }
+
+  _initCache () {
+    this._ignoreCache = Object.create(null)
+    this._testCache = Object.create(null)
+  }
+
+  _addPattern (pattern) {
+    // #32
+    if (pattern && pattern[KEY_IGNORE]) {
+      this._rules = this._rules.concat(pattern._rules)
+      this._added = true
+      return
+    }
+
+    if (checkPattern(pattern)) {
+      const rule = createRule(pattern, this._ignoreCase)
+      this._added = true
+      this._rules.push(rule)
+    }
+  }
+
+  // @param {Array<string> | string | Ignore} pattern
+  add (pattern) {
+    this._added = false
+
+    makeArray(
+      isString(pattern)
+        ? splitPattern(pattern)
+        : pattern
+    ).forEach(this._addPattern, this)
+
+    // Some rules have just added to the ignore,
+    // making the behavior changed.
+    if (this._added) {
+      this._initCache()
+    }
+
+    return this
+  }
+
+  // legacy
+  addPattern (pattern) {
+    return this.add(pattern)
+  }
+
+  //          |           ignored : unignored
+  // negative |   0:0   |   0:1   |   1:0   |   1:1
+  // -------- | ------- | ------- | ------- | --------
+  //     0    |  TEST   |  TEST   |  SKIP   |    X
+  //     1    |  TESTIF |  SKIP   |  TEST   |    X
+
+  // - SKIP: always skip
+  // - TEST: always test
+  // - TESTIF: only test if checkUnignored
+  // - X: that never happen
+
+  // @param {boolean} whether should check if the path is unignored,
+  //   setting `checkUnignored` to `false` could reduce additional
+  //   path matching.
+
+  // @returns {TestResult} true if a file is ignored
+  _testOne (path, checkUnignored) {
+    let ignored = false
+    let unignored = false
+
+    this._rules.forEach(rule => {
+      const {negative} = rule
+      if (
+        unignored === negative && ignored !== unignored
+        || negative && !ignored && !unignored && !checkUnignored
+      ) {
+        return
+      }
+
+      const matched = rule.regex.test(path)
+
+      if (matched) {
+        ignored = !negative
+        unignored = negative
+      }
+    })
+
+    return {
+      ignored,
+      unignored
+    }
+  }
+
+  // @returns {TestResult}
+  _test (originalPath, cache, checkUnignored, slices) {
+    const path = originalPath
+      // Supports nullable path
+      && checkPath.convert(originalPath)
+
+    checkPath(
+      path,
+      originalPath,
+      this._allowRelativePaths
+        ? RETURN_FALSE
+        : throwError
+    )
+
+    return this._t(path, cache, checkUnignored, slices)
+  }
+
+  _t (path, cache, checkUnignored, slices) {
+    if (path in cache) {
+      return cache[path]
+    }
+
+    if (!slices) {
+      // path/to/a.js
+      // ['path', 'to', 'a.js']
+      slices = path.split(SLASH)
+    }
+
+    slices.pop()
+
+    // If the path has no parent directory, just test it
+    if (!slices.length) {
+      return cache[path] = this._testOne(path, checkUnignored)
+    }
+
+    const parent = this._t(
+      slices.join(SLASH) + SLASH,
+      cache,
+      checkUnignored,
+      slices
+    )
+
+    // If the path contains a parent directory, check the parent first
+    return cache[path] = parent.ignored
+      // > It is not possible to re-include a file if a parent directory of
+      // >   that file is excluded.
+      ? parent
+      : this._testOne(path, checkUnignored)
+  }
+
+  ignores (path) {
+    return this._test(path, this._ignoreCache, false).ignored
+  }
+
+  createFilter () {
+    return path => !this.ignores(path)
+  }
+
+  filter (paths) {
+    return makeArray(paths).filter(this.createFilter())
+  }
+
+  // @returns {TestResult}
+  test (path) {
+    return this._test(path, this._testCache, true)
+  }
+}
+
+const factory = options => new Ignore(options)
+
+const isPathValid = path =>
+  checkPath(path && checkPath.convert(path), path, RETURN_FALSE)
+
+factory.isPathValid = isPathValid
+
+// Fixes typescript
+factory.default = factory
+
+module.exports = factory
+
+// Windows
+// --------------------------------------------------------------
+/* istanbul ignore if */
+if (
+  // Detect `process` so that it can run in browsers.
+  typeof process !== 'undefined'
+  && (
+    process.env && process.env.IGNORE_TEST_WIN32
+    || process.platform === 'win32'
+  )
+) {
+  /* eslint no-control-regex: "off" */
+  const makePosix = str => /^\\\\\?\\/.test(str)
+  || /["<>|\u0000-\u001F]+/u.test(str)
+    ? str
+    : str.replace(/\\/g, '/')
+
+  checkPath.convert = makePosix
+
+  // 'C:\\foo'     <- 'C:\\foo' has been converted to 'C:/'
+  // 'd:\\foo'
+  const REGIX_IS_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i
+  checkPath.isNotRelative = path =>
+    REGIX_IS_WINDOWS_PATH_ABSOLUTE.test(path)
+    || isNotRelative(path)
+}
Index: frontend/node_modules/ignore/legacy.js
===================================================================
--- frontend/node_modules/ignore/legacy.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/ignore/legacy.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,559 @@
+"use strict";
+
+function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
+function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
+function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
+function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
+function _iterableToArrayLimit(arr, i) { var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"]; if (null != _i) { var _s, _e, _x, _r, _arr = [], _n = !0, _d = !1; try { if (_x = (_i = _i.call(arr)).next, 0 === i) { if (Object(_i) !== _i) return; _n = !1; } else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0); } catch (err) { _d = !0, _e = err; } finally { try { if (!_n && null != _i["return"] && (_r = _i["return"](), Object(_r) !== _r)) return; } finally { if (_d) throw _e; } } return _arr; } }
+function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
+// A simple implementation of make-array
+function makeArray(subject) {
+  return Array.isArray(subject) ? subject : [subject];
+}
+var EMPTY = '';
+var SPACE = ' ';
+var ESCAPE = '\\';
+var REGEX_TEST_BLANK_LINE = /^\s+$/;
+var REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/;
+var REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/;
+var REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/;
+var REGEX_SPLITALL_CRLF = /\r?\n/g;
+// /foo,
+// ./foo,
+// ../foo,
+// .
+// ..
+var REGEX_TEST_INVALID_PATH = /^\.*\/|^\.+$/;
+var SLASH = '/';
+
+// Do not use ternary expression here, since "istanbul ignore next" is buggy
+var TMP_KEY_IGNORE = 'node-ignore';
+/* istanbul ignore else */
+if (typeof Symbol !== 'undefined') {
+  TMP_KEY_IGNORE = Symbol["for"]('node-ignore');
+}
+var KEY_IGNORE = TMP_KEY_IGNORE;
+var define = function define(object, key, value) {
+  return Object.defineProperty(object, key, {
+    value: value
+  });
+};
+var REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g;
+var RETURN_FALSE = function RETURN_FALSE() {
+  return false;
+};
+
+// Sanitize the range of a regular expression
+// The cases are complicated, see test cases for details
+var sanitizeRange = function sanitizeRange(range) {
+  return range.replace(REGEX_REGEXP_RANGE, function (match, from, to) {
+    return from.charCodeAt(0) <= to.charCodeAt(0) ? match
+    // Invalid range (out of order) which is ok for gitignore rules but
+    //   fatal for JavaScript regular expression, so eliminate it.
+    : EMPTY;
+  });
+};
+
+// See fixtures #59
+var cleanRangeBackSlash = function cleanRangeBackSlash(slashes) {
+  var length = slashes.length;
+  return slashes.slice(0, length - length % 2);
+};
+
+// > If the pattern ends with a slash,
+// > it is removed for the purpose of the following description,
+// > but it would only find a match with a directory.
+// > In other words, foo/ will match a directory foo and paths underneath it,
+// > but will not match a regular file or a symbolic link foo
+// >  (this is consistent with the way how pathspec works in general in Git).
+// '`foo/`' will not match regular file '`foo`' or symbolic link '`foo`'
+// -> ignore-rules will not deal with it, because it costs extra `fs.stat` call
+//      you could use option `mark: true` with `glob`
+
+// '`foo/`' should not continue with the '`..`'
+var REPLACERS = [[
+// remove BOM
+// TODO:
+// Other similar zero-width characters?
+/^\uFEFF/, function () {
+  return EMPTY;
+}],
+// > Trailing spaces are ignored unless they are quoted with backslash ("\")
+[
+// (a\ ) -> (a )
+// (a  ) -> (a)
+// (a ) -> (a)
+// (a \ ) -> (a  )
+/((?:\\\\)*?)(\\?\s+)$/, function (_, m1, m2) {
+  return m1 + (m2.indexOf('\\') === 0 ? SPACE : EMPTY);
+}],
+// replace (\ ) with ' '
+// (\ ) -> ' '
+// (\\ ) -> '\\ '
+// (\\\ ) -> '\\ '
+[/(\\+?)\s/g, function (_, m1) {
+  var length = m1.length;
+  return m1.slice(0, length - length % 2) + SPACE;
+}],
+// Escape metacharacters
+// which is written down by users but means special for regular expressions.
+
+// > There are 12 characters with special meanings:
+// > - the backslash \,
+// > - the caret ^,
+// > - the dollar sign $,
+// > - the period or dot .,
+// > - the vertical bar or pipe symbol |,
+// > - the question mark ?,
+// > - the asterisk or star *,
+// > - the plus sign +,
+// > - the opening parenthesis (,
+// > - the closing parenthesis ),
+// > - and the opening square bracket [,
+// > - the opening curly brace {,
+// > These special characters are often called "metacharacters".
+[/[\\$.|*+(){^]/g, function (match) {
+  return "\\".concat(match);
+}], [
+// > a question mark (?) matches a single character
+/(?!\\)\?/g, function () {
+  return '[^/]';
+}],
+// leading slash
+[
+// > A leading slash matches the beginning of the pathname.
+// > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
+// A leading slash matches the beginning of the pathname
+/^\//, function () {
+  return '^';
+}],
+// replace special metacharacter slash after the leading slash
+[/\//g, function () {
+  return '\\/';
+}], [
+// > A leading "**" followed by a slash means match in all directories.
+// > For example, "**/foo" matches file or directory "foo" anywhere,
+// > the same as pattern "foo".
+// > "**/foo/bar" matches file or directory "bar" anywhere that is directly
+// >   under directory "foo".
+// Notice that the '*'s have been replaced as '\\*'
+/^\^*\\\*\\\*\\\//,
+// '**/foo' <-> 'foo'
+function () {
+  return '^(?:.*\\/)?';
+}],
+// starting
+[
+// there will be no leading '/'
+//   (which has been replaced by section "leading slash")
+// If starts with '**', adding a '^' to the regular expression also works
+/^(?=[^^])/, function startingReplacer() {
+  // If has a slash `/` at the beginning or middle
+  return !/\/(?!$)/.test(this)
+  // > Prior to 2.22.1
+  // > If the pattern does not contain a slash /,
+  // >   Git treats it as a shell glob pattern
+  // Actually, if there is only a trailing slash,
+  //   git also treats it as a shell glob pattern
+
+  // After 2.22.1 (compatible but clearer)
+  // > If there is a separator at the beginning or middle (or both)
+  // > of the pattern, then the pattern is relative to the directory
+  // > level of the particular .gitignore file itself.
+  // > Otherwise the pattern may also match at any level below
+  // > the .gitignore level.
+  ? '(?:^|\\/)'
+
+  // > Otherwise, Git treats the pattern as a shell glob suitable for
+  // >   consumption by fnmatch(3)
+  : '^';
+}],
+// two globstars
+[
+// Use lookahead assertions so that we could match more than one `'/**'`
+/\\\/\\\*\\\*(?=\\\/|$)/g,
+// Zero, one or several directories
+// should not use '*', or it will be replaced by the next replacer
+
+// Check if it is not the last `'/**'`
+function (_, index, str) {
+  return index + 6 < str.length
+
+  // case: /**/
+  // > A slash followed by two consecutive asterisks then a slash matches
+  // >   zero or more directories.
+  // > For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on.
+  // '/**/'
+  ? '(?:\\/[^\\/]+)*'
+
+  // case: /**
+  // > A trailing `"/**"` matches everything inside.
+
+  // #21: everything inside but it should not include the current folder
+  : '\\/.+';
+}],
+// normal intermediate wildcards
+[
+// Never replace escaped '*'
+// ignore rule '\*' will match the path '*'
+
+// 'abc.*/' -> go
+// 'abc.*'  -> skip this rule,
+//    coz trailing single wildcard will be handed by [trailing wildcard]
+/(^|[^\\]+)(\\\*)+(?=.+)/g,
+// '*.js' matches '.js'
+// '*.js' doesn't match 'abc'
+function (_, p1, p2) {
+  // 1.
+  // > An asterisk "*" matches anything except a slash.
+  // 2.
+  // > Other consecutive asterisks are considered regular asterisks
+  // > and will match according to the previous rules.
+  var unescaped = p2.replace(/\\\*/g, '[^\\/]*');
+  return p1 + unescaped;
+}], [
+// unescape, revert step 3 except for back slash
+// For example, if a user escape a '\\*',
+// after step 3, the result will be '\\\\\\*'
+/\\\\\\(?=[$.|*+(){^])/g, function () {
+  return ESCAPE;
+}], [
+// '\\\\' -> '\\'
+/\\\\/g, function () {
+  return ESCAPE;
+}], [
+// > The range notation, e.g. [a-zA-Z],
+// > can be used to match one of the characters in a range.
+
+// `\` is escaped by step 3
+/(\\)?\[([^\]/]*?)(\\*)($|\])/g, function (match, leadEscape, range, endEscape, close) {
+  return leadEscape === ESCAPE
+  // '\\[bar]' -> '\\\\[bar\\]'
+  ? "\\[".concat(range).concat(cleanRangeBackSlash(endEscape)).concat(close) : close === ']' ? endEscape.length % 2 === 0
+  // A normal case, and it is a range notation
+  // '[bar]'
+  // '[bar\\\\]'
+  ? "[".concat(sanitizeRange(range)).concat(endEscape, "]") // Invalid range notaton
+  // '[bar\\]' -> '[bar\\\\]'
+  : '[]' : '[]';
+}],
+// ending
+[
+// 'js' will not match 'js.'
+// 'ab' will not match 'abc'
+/(?:[^*])$/,
+// WTF!
+// https://git-scm.com/docs/gitignore
+// changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)
+// which re-fixes #24, #38
+
+// > If there is a separator at the end of the pattern then the pattern
+// > will only match directories, otherwise the pattern can match both
+// > files and directories.
+
+// 'js*' will not match 'a.js'
+// 'js/' will not match 'a.js'
+// 'js' will match 'a.js' and 'a.js/'
+function (match) {
+  return /\/$/.test(match)
+  // foo/ will not match 'foo'
+  ? "".concat(match, "$") // foo matches 'foo' and 'foo/'
+  : "".concat(match, "(?=$|\\/$)");
+}],
+// trailing wildcard
+[/(\^|\\\/)?\\\*$/, function (_, p1) {
+  var prefix = p1
+  // '\^':
+  // '/*' does not match EMPTY
+  // '/*' does not match everything
+
+  // '\\\/':
+  // 'abc/*' does not match 'abc/'
+  ? "".concat(p1, "[^/]+") // 'a*' matches 'a'
+  // 'a*' matches 'aa'
+  : '[^/]*';
+  return "".concat(prefix, "(?=$|\\/$)");
+}]];
+
+// A simple cache, because an ignore rule only has only one certain meaning
+var regexCache = Object.create(null);
+
+// @param {pattern}
+var makeRegex = function makeRegex(pattern, ignoreCase) {
+  var source = regexCache[pattern];
+  if (!source) {
+    source = REPLACERS.reduce(function (prev, _ref) {
+      var _ref2 = _slicedToArray(_ref, 2),
+        matcher = _ref2[0],
+        replacer = _ref2[1];
+      return prev.replace(matcher, replacer.bind(pattern));
+    }, pattern);
+    regexCache[pattern] = source;
+  }
+  return ignoreCase ? new RegExp(source, 'i') : new RegExp(source);
+};
+var isString = function isString(subject) {
+  return typeof subject === 'string';
+};
+
+// > A blank line matches no files, so it can serve as a separator for readability.
+var checkPattern = function checkPattern(pattern) {
+  return pattern && isString(pattern) && !REGEX_TEST_BLANK_LINE.test(pattern) && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern)
+
+  // > A line starting with # serves as a comment.
+  && pattern.indexOf('#') !== 0;
+};
+var splitPattern = function splitPattern(pattern) {
+  return pattern.split(REGEX_SPLITALL_CRLF);
+};
+var IgnoreRule = /*#__PURE__*/_createClass(function IgnoreRule(origin, pattern, negative, regex) {
+  _classCallCheck(this, IgnoreRule);
+  this.origin = origin;
+  this.pattern = pattern;
+  this.negative = negative;
+  this.regex = regex;
+});
+var createRule = function createRule(pattern, ignoreCase) {
+  var origin = pattern;
+  var negative = false;
+
+  // > An optional prefix "!" which negates the pattern;
+  if (pattern.indexOf('!') === 0) {
+    negative = true;
+    pattern = pattern.substr(1);
+  }
+  pattern = pattern
+  // > Put a backslash ("\") in front of the first "!" for patterns that
+  // >   begin with a literal "!", for example, `"\!important!.txt"`.
+  .replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, '!')
+  // > Put a backslash ("\") in front of the first hash for patterns that
+  // >   begin with a hash.
+  .replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, '#');
+  var regex = makeRegex(pattern, ignoreCase);
+  return new IgnoreRule(origin, pattern, negative, regex);
+};
+var throwError = function throwError(message, Ctor) {
+  throw new Ctor(message);
+};
+var checkPath = function checkPath(path, originalPath, doThrow) {
+  if (!isString(path)) {
+    return doThrow("path must be a string, but got `".concat(originalPath, "`"), TypeError);
+  }
+
+  // We don't know if we should ignore EMPTY, so throw
+  if (!path) {
+    return doThrow("path must not be empty", TypeError);
+  }
+
+  // Check if it is a relative path
+  if (checkPath.isNotRelative(path)) {
+    var r = '`path.relative()`d';
+    return doThrow("path should be a ".concat(r, " string, but got \"").concat(originalPath, "\""), RangeError);
+  }
+  return true;
+};
+var isNotRelative = function isNotRelative(path) {
+  return REGEX_TEST_INVALID_PATH.test(path);
+};
+checkPath.isNotRelative = isNotRelative;
+checkPath.convert = function (p) {
+  return p;
+};
+var Ignore = /*#__PURE__*/function () {
+  function Ignore() {
+    var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
+      _ref3$ignorecase = _ref3.ignorecase,
+      ignorecase = _ref3$ignorecase === void 0 ? true : _ref3$ignorecase,
+      _ref3$ignoreCase = _ref3.ignoreCase,
+      ignoreCase = _ref3$ignoreCase === void 0 ? ignorecase : _ref3$ignoreCase,
+      _ref3$allowRelativePa = _ref3.allowRelativePaths,
+      allowRelativePaths = _ref3$allowRelativePa === void 0 ? false : _ref3$allowRelativePa;
+    _classCallCheck(this, Ignore);
+    define(this, KEY_IGNORE, true);
+    this._rules = [];
+    this._ignoreCase = ignoreCase;
+    this._allowRelativePaths = allowRelativePaths;
+    this._initCache();
+  }
+  _createClass(Ignore, [{
+    key: "_initCache",
+    value: function _initCache() {
+      this._ignoreCache = Object.create(null);
+      this._testCache = Object.create(null);
+    }
+  }, {
+    key: "_addPattern",
+    value: function _addPattern(pattern) {
+      // #32
+      if (pattern && pattern[KEY_IGNORE]) {
+        this._rules = this._rules.concat(pattern._rules);
+        this._added = true;
+        return;
+      }
+      if (checkPattern(pattern)) {
+        var rule = createRule(pattern, this._ignoreCase);
+        this._added = true;
+        this._rules.push(rule);
+      }
+    }
+
+    // @param {Array<string> | string | Ignore} pattern
+  }, {
+    key: "add",
+    value: function add(pattern) {
+      this._added = false;
+      makeArray(isString(pattern) ? splitPattern(pattern) : pattern).forEach(this._addPattern, this);
+
+      // Some rules have just added to the ignore,
+      // making the behavior changed.
+      if (this._added) {
+        this._initCache();
+      }
+      return this;
+    }
+
+    // legacy
+  }, {
+    key: "addPattern",
+    value: function addPattern(pattern) {
+      return this.add(pattern);
+    }
+
+    //          |           ignored : unignored
+    // negative |   0:0   |   0:1   |   1:0   |   1:1
+    // -------- | ------- | ------- | ------- | --------
+    //     0    |  TEST   |  TEST   |  SKIP   |    X
+    //     1    |  TESTIF |  SKIP   |  TEST   |    X
+
+    // - SKIP: always skip
+    // - TEST: always test
+    // - TESTIF: only test if checkUnignored
+    // - X: that never happen
+
+    // @param {boolean} whether should check if the path is unignored,
+    //   setting `checkUnignored` to `false` could reduce additional
+    //   path matching.
+
+    // @returns {TestResult} true if a file is ignored
+  }, {
+    key: "_testOne",
+    value: function _testOne(path, checkUnignored) {
+      var ignored = false;
+      var unignored = false;
+      this._rules.forEach(function (rule) {
+        var negative = rule.negative;
+        if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
+          return;
+        }
+        var matched = rule.regex.test(path);
+        if (matched) {
+          ignored = !negative;
+          unignored = negative;
+        }
+      });
+      return {
+        ignored: ignored,
+        unignored: unignored
+      };
+    }
+
+    // @returns {TestResult}
+  }, {
+    key: "_test",
+    value: function _test(originalPath, cache, checkUnignored, slices) {
+      var path = originalPath
+      // Supports nullable path
+      && checkPath.convert(originalPath);
+      checkPath(path, originalPath, this._allowRelativePaths ? RETURN_FALSE : throwError);
+      return this._t(path, cache, checkUnignored, slices);
+    }
+  }, {
+    key: "_t",
+    value: function _t(path, cache, checkUnignored, slices) {
+      if (path in cache) {
+        return cache[path];
+      }
+      if (!slices) {
+        // path/to/a.js
+        // ['path', 'to', 'a.js']
+        slices = path.split(SLASH);
+      }
+      slices.pop();
+
+      // If the path has no parent directory, just test it
+      if (!slices.length) {
+        return cache[path] = this._testOne(path, checkUnignored);
+      }
+      var parent = this._t(slices.join(SLASH) + SLASH, cache, checkUnignored, slices);
+
+      // If the path contains a parent directory, check the parent first
+      return cache[path] = parent.ignored
+      // > It is not possible to re-include a file if a parent directory of
+      // >   that file is excluded.
+      ? parent : this._testOne(path, checkUnignored);
+    }
+  }, {
+    key: "ignores",
+    value: function ignores(path) {
+      return this._test(path, this._ignoreCache, false).ignored;
+    }
+  }, {
+    key: "createFilter",
+    value: function createFilter() {
+      var _this = this;
+      return function (path) {
+        return !_this.ignores(path);
+      };
+    }
+  }, {
+    key: "filter",
+    value: function filter(paths) {
+      return makeArray(paths).filter(this.createFilter());
+    }
+
+    // @returns {TestResult}
+  }, {
+    key: "test",
+    value: function test(path) {
+      return this._test(path, this._testCache, true);
+    }
+  }]);
+  return Ignore;
+}();
+var factory = function factory(options) {
+  return new Ignore(options);
+};
+var isPathValid = function isPathValid(path) {
+  return checkPath(path && checkPath.convert(path), path, RETURN_FALSE);
+};
+factory.isPathValid = isPathValid;
+
+// Fixes typescript
+factory["default"] = factory;
+module.exports = factory;
+
+// Windows
+// --------------------------------------------------------------
+/* istanbul ignore if */
+if (
+// Detect `process` so that it can run in browsers.
+typeof process !== 'undefined' && (process.env && process.env.IGNORE_TEST_WIN32 || process.platform === 'win32')) {
+  /* eslint no-control-regex: "off" */
+  var makePosix = function makePosix(str) {
+    return /^\\\\\?\\/.test(str) || /[\0-\x1F"<>\|]+/.test(str) ? str : str.replace(/\\/g, '/');
+  };
+  checkPath.convert = makePosix;
+
+  // 'C:\\foo'     <- 'C:\\foo' has been converted to 'C:/'
+  // 'd:\\foo'
+  var REGIX_IS_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
+  checkPath.isNotRelative = function (path) {
+    return REGIX_IS_WINDOWS_PATH_ABSOLUTE.test(path) || isNotRelative(path);
+  };
+}
Index: frontend/node_modules/ignore/package.json
===================================================================
--- frontend/node_modules/ignore/package.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/ignore/package.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,74 @@
+{
+  "name": "ignore",
+  "version": "5.3.2",
+  "description": "Ignore is a manager and filter for .gitignore rules, the one used by eslint, gitbook and many others.",
+  "files": [
+    "legacy.js",
+    "index.js",
+    "index.d.ts",
+    "LICENSE-MIT"
+  ],
+  "scripts": {
+    "prepublishOnly": "npm run build",
+    "build": "babel -o legacy.js index.js",
+    "test:lint": "eslint .",
+    "test:tsc": "tsc ./test/ts/simple.ts --lib ES6",
+    "test:ts": "node ./test/ts/simple.js",
+    "tap": "tap --reporter classic",
+    "test:git": "npm run tap test/git-check-ignore.js",
+    "test:ignore": "npm run tap test/ignore.js",
+    "test:ignore:only": "IGNORE_ONLY_IGNORES=1 npm run tap test/ignore.js",
+    "test:others": "npm run tap test/others.js",
+    "test:cases": "npm run tap test/*.js -- --coverage",
+    "test:no-coverage": "npm run tap test/*.js -- --no-check-coverage",
+    "test:only": "npm run test:lint && npm run test:tsc && npm run test:ts && npm run test:cases",
+    "test": "npm run test:only",
+    "test:win32": "IGNORE_TEST_WIN32=1 npm run test",
+    "report": "tap --coverage-report=html",
+    "posttest": "npm run report && codecov"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git@github.com:kaelzhang/node-ignore.git"
+  },
+  "keywords": [
+    "ignore",
+    ".gitignore",
+    "gitignore",
+    "npmignore",
+    "rules",
+    "manager",
+    "filter",
+    "regexp",
+    "regex",
+    "fnmatch",
+    "glob",
+    "asterisks",
+    "regular-expression"
+  ],
+  "author": "kael",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/kaelzhang/node-ignore/issues"
+  },
+  "devDependencies": {
+    "@babel/cli": "^7.22.9",
+    "@babel/core": "^7.22.9",
+    "@babel/preset-env": "^7.22.9",
+    "codecov": "^3.8.2",
+    "debug": "^4.3.4",
+    "eslint": "^8.46.0",
+    "eslint-config-ostai": "^3.0.0",
+    "eslint-plugin-import": "^2.28.0",
+    "mkdirp": "^3.0.1",
+    "pre-suf": "^1.1.1",
+    "rimraf": "^6.0.1",
+    "spawn-sync": "^2.0.0",
+    "tap": "^16.3.9",
+    "tmp": "0.2.3",
+    "typescript": "^5.1.6"
+  },
+  "engines": {
+    "node": ">= 4"
+  }
+}
