Index: frontend/node_modules/loader-utils/lib/getCurrentRequest.js
===================================================================
--- frontend/node_modules/loader-utils/lib/getCurrentRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/getCurrentRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,16 @@
+'use strict';
+
+function getCurrentRequest(loaderContext) {
+  if (loaderContext.currentRequest) {
+    return loaderContext.currentRequest;
+  }
+
+  const request = loaderContext.loaders
+    .slice(loaderContext.loaderIndex)
+    .map((obj) => obj.request)
+    .concat([loaderContext.resource]);
+
+  return request.join('!');
+}
+
+module.exports = getCurrentRequest;
Index: frontend/node_modules/loader-utils/lib/getHashDigest.js
===================================================================
--- frontend/node_modules/loader-utils/lib/getHashDigest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/getHashDigest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,91 @@
+'use strict';
+
+const baseEncodeTables = {
+  26: 'abcdefghijklmnopqrstuvwxyz',
+  32: '123456789abcdefghjkmnpqrstuvwxyz', // no 0lio
+  36: '0123456789abcdefghijklmnopqrstuvwxyz',
+  49: 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', // no lIO
+  52: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
+  58: '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', // no 0lIO
+  62: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
+  64: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_',
+};
+
+function encodeBufferToBase(buffer, base) {
+  const encodeTable = baseEncodeTables[base];
+  if (!encodeTable) {
+    throw new Error('Unknown encoding base' + base);
+  }
+
+  const readLength = buffer.length;
+  const Big = require('big.js');
+
+  Big.RM = Big.DP = 0;
+  let b = new Big(0);
+
+  for (let i = readLength - 1; i >= 0; i--) {
+    b = b.times(256).plus(buffer[i]);
+  }
+
+  let output = '';
+  while (b.gt(0)) {
+    output = encodeTable[b.mod(base)] + output;
+    b = b.div(base);
+  }
+
+  Big.DP = 20;
+  Big.RM = 1;
+
+  return output;
+}
+
+let createMd4 = undefined;
+let BatchedHash = undefined;
+
+function getHashDigest(buffer, hashType, digestType, maxLength) {
+  hashType = hashType || 'md4';
+  maxLength = maxLength || 9999;
+
+  let hash;
+
+  try {
+    hash = require('crypto').createHash(hashType);
+  } catch (error) {
+    if (error.code === 'ERR_OSSL_EVP_UNSUPPORTED' && hashType === 'md4') {
+      if (createMd4 === undefined) {
+        createMd4 = require('./hash/md4');
+
+        if (BatchedHash === undefined) {
+          BatchedHash = require('./hash/BatchedHash');
+        }
+      }
+
+      hash = new BatchedHash(createMd4());
+    }
+
+    if (!hash) {
+      throw error;
+    }
+  }
+
+  hash.update(buffer);
+
+  if (
+    digestType === 'base26' ||
+    digestType === 'base32' ||
+    digestType === 'base36' ||
+    digestType === 'base49' ||
+    digestType === 'base52' ||
+    digestType === 'base58' ||
+    digestType === 'base62'
+  ) {
+    return encodeBufferToBase(hash.digest(), digestType.substr(4)).substr(
+      0,
+      maxLength
+    );
+  } else {
+    return hash.digest(digestType || 'hex').substr(0, maxLength);
+  }
+}
+
+module.exports = getHashDigest;
Index: frontend/node_modules/loader-utils/lib/getOptions.js
===================================================================
--- frontend/node_modules/loader-utils/lib/getOptions.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/getOptions.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,20 @@
+'use strict';
+
+const parseQuery = require('./parseQuery');
+
+function getOptions(loaderContext) {
+  const query = loaderContext.query;
+
+  if (typeof query === 'string' && query !== '') {
+    return parseQuery(loaderContext.query);
+  }
+
+  if (!query || typeof query !== 'object') {
+    // Not object-like queries are not supported.
+    return {};
+  }
+
+  return query;
+}
+
+module.exports = getOptions;
Index: frontend/node_modules/loader-utils/lib/getRemainingRequest.js
===================================================================
--- frontend/node_modules/loader-utils/lib/getRemainingRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/getRemainingRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,16 @@
+'use strict';
+
+function getRemainingRequest(loaderContext) {
+  if (loaderContext.remainingRequest) {
+    return loaderContext.remainingRequest;
+  }
+
+  const request = loaderContext.loaders
+    .slice(loaderContext.loaderIndex + 1)
+    .map((obj) => obj.request)
+    .concat([loaderContext.resource]);
+
+  return request.join('!');
+}
+
+module.exports = getRemainingRequest;
Index: frontend/node_modules/loader-utils/lib/hash/BatchedHash.js
===================================================================
--- frontend/node_modules/loader-utils/lib/hash/BatchedHash.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/hash/BatchedHash.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,64 @@
+const MAX_SHORT_STRING = require('./wasm-hash').MAX_SHORT_STRING;
+
+class BatchedHash {
+  constructor(hash) {
+    this.string = undefined;
+    this.encoding = undefined;
+    this.hash = hash;
+  }
+
+  /**
+   * Update hash {@link https://nodejs.org/api/crypto.html#crypto_hash_update_data_inputencoding}
+   * @param {string|Buffer} data data
+   * @param {string=} inputEncoding data encoding
+   * @returns {this} updated hash
+   */
+  update(data, inputEncoding) {
+    if (this.string !== undefined) {
+      if (
+        typeof data === 'string' &&
+        inputEncoding === this.encoding &&
+        this.string.length + data.length < MAX_SHORT_STRING
+      ) {
+        this.string += data;
+
+        return this;
+      }
+
+      this.hash.update(this.string, this.encoding);
+      this.string = undefined;
+    }
+
+    if (typeof data === 'string') {
+      if (
+        data.length < MAX_SHORT_STRING &&
+        // base64 encoding is not valid since it may contain padding chars
+        (!inputEncoding || !inputEncoding.startsWith('ba'))
+      ) {
+        this.string = data;
+        this.encoding = inputEncoding;
+      } else {
+        this.hash.update(data, inputEncoding);
+      }
+    } else {
+      this.hash.update(data);
+    }
+
+    return this;
+  }
+
+  /**
+   * Calculates the digest {@link https://nodejs.org/api/crypto.html#crypto_hash_digest_encoding}
+   * @param {string=} encoding encoding of the return value
+   * @returns {string|Buffer} digest
+   */
+  digest(encoding) {
+    if (this.string !== undefined) {
+      this.hash.update(this.string, this.encoding);
+    }
+
+    return this.hash.digest(encoding);
+  }
+}
+
+module.exports = BatchedHash;
Index: frontend/node_modules/loader-utils/lib/hash/md4.js
===================================================================
--- frontend/node_modules/loader-utils/lib/hash/md4.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/hash/md4.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,20 @@
+/*
+	MIT License http://www.opensource.org/licenses/mit-license.php
+	Author Tobias Koppers @sokra
+*/
+
+'use strict';
+
+const create = require('./wasm-hash');
+
+//#region wasm code: md4 (../../../assembly/hash/md4.asm.ts) --initialMemory 1
+const md4 = new WebAssembly.Module(
+  Buffer.from(
+    // 2150 bytes
+    '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',
+    'base64'
+  )
+);
+//#endregion
+
+module.exports = create.bind(null, md4, [], 64, 32);
Index: frontend/node_modules/loader-utils/lib/hash/wasm-hash.js
===================================================================
--- frontend/node_modules/loader-utils/lib/hash/wasm-hash.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/hash/wasm-hash.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,208 @@
+/*
+	MIT License http://www.opensource.org/licenses/mit-license.php
+	Author Tobias Koppers @sokra
+*/
+
+'use strict';
+
+// 65536 is the size of a wasm memory page
+// 64 is the maximum chunk size for every possible wasm hash implementation
+// 4 is the maximum number of bytes per char for string encoding (max is utf-8)
+// ~3 makes sure that it's always a block of 4 chars, so avoid partially encoded bytes for base64
+const MAX_SHORT_STRING = Math.floor((65536 - 64) / 4) & ~3;
+
+class WasmHash {
+  /**
+   * @param {WebAssembly.Instance} instance wasm instance
+   * @param {WebAssembly.Instance[]} instancesPool pool of instances
+   * @param {number} chunkSize size of data chunks passed to wasm
+   * @param {number} digestSize size of digest returned by wasm
+   */
+  constructor(instance, instancesPool, chunkSize, digestSize) {
+    const exports = /** @type {any} */ (instance.exports);
+
+    exports.init();
+
+    this.exports = exports;
+    this.mem = Buffer.from(exports.memory.buffer, 0, 65536);
+    this.buffered = 0;
+    this.instancesPool = instancesPool;
+    this.chunkSize = chunkSize;
+    this.digestSize = digestSize;
+  }
+
+  reset() {
+    this.buffered = 0;
+    this.exports.init();
+  }
+
+  /**
+   * @param {Buffer | string} data data
+   * @param {BufferEncoding=} encoding encoding
+   * @returns {this} itself
+   */
+  update(data, encoding) {
+    if (typeof data === 'string') {
+      while (data.length > MAX_SHORT_STRING) {
+        this._updateWithShortString(data.slice(0, MAX_SHORT_STRING), encoding);
+        data = data.slice(MAX_SHORT_STRING);
+      }
+
+      this._updateWithShortString(data, encoding);
+
+      return this;
+    }
+
+    this._updateWithBuffer(data);
+
+    return this;
+  }
+
+  /**
+   * @param {string} data data
+   * @param {BufferEncoding=} encoding encoding
+   * @returns {void}
+   */
+  _updateWithShortString(data, encoding) {
+    const { exports, buffered, mem, chunkSize } = this;
+
+    let endPos;
+
+    if (data.length < 70) {
+      if (!encoding || encoding === 'utf-8' || encoding === 'utf8') {
+        endPos = buffered;
+        for (let i = 0; i < data.length; i++) {
+          const cc = data.charCodeAt(i);
+
+          if (cc < 0x80) {
+            mem[endPos++] = cc;
+          } else if (cc < 0x800) {
+            mem[endPos] = (cc >> 6) | 0xc0;
+            mem[endPos + 1] = (cc & 0x3f) | 0x80;
+            endPos += 2;
+          } else {
+            // bail-out for weird chars
+            endPos += mem.write(data.slice(i), endPos, encoding);
+            break;
+          }
+        }
+      } else if (encoding === 'latin1') {
+        endPos = buffered;
+
+        for (let i = 0; i < data.length; i++) {
+          const cc = data.charCodeAt(i);
+
+          mem[endPos++] = cc;
+        }
+      } else {
+        endPos = buffered + mem.write(data, buffered, encoding);
+      }
+    } else {
+      endPos = buffered + mem.write(data, buffered, encoding);
+    }
+
+    if (endPos < chunkSize) {
+      this.buffered = endPos;
+    } else {
+      const l = endPos & ~(this.chunkSize - 1);
+
+      exports.update(l);
+
+      const newBuffered = endPos - l;
+
+      this.buffered = newBuffered;
+
+      if (newBuffered > 0) {
+        mem.copyWithin(0, l, endPos);
+      }
+    }
+  }
+
+  /**
+   * @param {Buffer} data data
+   * @returns {void}
+   */
+  _updateWithBuffer(data) {
+    const { exports, buffered, mem } = this;
+    const length = data.length;
+
+    if (buffered + length < this.chunkSize) {
+      data.copy(mem, buffered, 0, length);
+
+      this.buffered += length;
+    } else {
+      const l = (buffered + length) & ~(this.chunkSize - 1);
+
+      if (l > 65536) {
+        let i = 65536 - buffered;
+
+        data.copy(mem, buffered, 0, i);
+        exports.update(65536);
+
+        const stop = l - buffered - 65536;
+
+        while (i < stop) {
+          data.copy(mem, 0, i, i + 65536);
+          exports.update(65536);
+          i += 65536;
+        }
+
+        data.copy(mem, 0, i, l - buffered);
+
+        exports.update(l - buffered - i);
+      } else {
+        data.copy(mem, buffered, 0, l - buffered);
+
+        exports.update(l);
+      }
+
+      const newBuffered = length + buffered - l;
+
+      this.buffered = newBuffered;
+
+      if (newBuffered > 0) {
+        data.copy(mem, 0, length - newBuffered, length);
+      }
+    }
+  }
+
+  digest(type) {
+    const { exports, buffered, mem, digestSize } = this;
+
+    exports.final(buffered);
+
+    this.instancesPool.push(this);
+
+    const hex = mem.toString('latin1', 0, digestSize);
+
+    if (type === 'hex') {
+      return hex;
+    }
+
+    if (type === 'binary' || !type) {
+      return Buffer.from(hex, 'hex');
+    }
+
+    return Buffer.from(hex, 'hex').toString(type);
+  }
+}
+
+const create = (wasmModule, instancesPool, chunkSize, digestSize) => {
+  if (instancesPool.length > 0) {
+    const old = instancesPool.pop();
+
+    old.reset();
+
+    return old;
+  } else {
+    return new WasmHash(
+      new WebAssembly.Instance(wasmModule),
+      instancesPool,
+      chunkSize,
+      digestSize
+    );
+  }
+};
+
+module.exports = create;
+module.exports.MAX_SHORT_STRING = MAX_SHORT_STRING;
Index: frontend/node_modules/loader-utils/lib/index.js
===================================================================
--- frontend/node_modules/loader-utils/lib/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,23 @@
+'use strict';
+
+const getOptions = require('./getOptions');
+const parseQuery = require('./parseQuery');
+const stringifyRequest = require('./stringifyRequest');
+const getRemainingRequest = require('./getRemainingRequest');
+const getCurrentRequest = require('./getCurrentRequest');
+const isUrlRequest = require('./isUrlRequest');
+const urlToRequest = require('./urlToRequest');
+const parseString = require('./parseString');
+const getHashDigest = require('./getHashDigest');
+const interpolateName = require('./interpolateName');
+
+exports.getOptions = getOptions;
+exports.parseQuery = parseQuery;
+exports.stringifyRequest = stringifyRequest;
+exports.getRemainingRequest = getRemainingRequest;
+exports.getCurrentRequest = getCurrentRequest;
+exports.isUrlRequest = isUrlRequest;
+exports.urlToRequest = urlToRequest;
+exports.parseString = parseString;
+exports.getHashDigest = getHashDigest;
+exports.interpolateName = interpolateName;
Index: frontend/node_modules/loader-utils/lib/interpolateName.js
===================================================================
--- frontend/node_modules/loader-utils/lib/interpolateName.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/interpolateName.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,151 @@
+'use strict';
+
+const path = require('path');
+const emojisList = require('emojis-list');
+const getHashDigest = require('./getHashDigest');
+
+const emojiRegex = /[\uD800-\uDFFF]./;
+const emojiList = emojisList.filter((emoji) => emojiRegex.test(emoji));
+const emojiCache = {};
+
+function encodeStringToEmoji(content, length) {
+  if (emojiCache[content]) {
+    return emojiCache[content];
+  }
+
+  length = length || 1;
+
+  const emojis = [];
+
+  do {
+    if (!emojiList.length) {
+      throw new Error('Ran out of emoji');
+    }
+
+    const index = Math.floor(Math.random() * emojiList.length);
+
+    emojis.push(emojiList[index]);
+    emojiList.splice(index, 1);
+  } while (--length > 0);
+
+  const emojiEncoding = emojis.join('');
+
+  emojiCache[content] = emojiEncoding;
+
+  return emojiEncoding;
+}
+
+function interpolateName(loaderContext, name, options) {
+  let filename;
+
+  const hasQuery =
+    loaderContext.resourceQuery && loaderContext.resourceQuery.length > 1;
+
+  if (typeof name === 'function') {
+    filename = name(
+      loaderContext.resourcePath,
+      hasQuery ? loaderContext.resourceQuery : undefined
+    );
+  } else {
+    filename = name || '[hash].[ext]';
+  }
+
+  const context = options.context;
+  const content = options.content;
+  const regExp = options.regExp;
+
+  let ext = 'bin';
+  let basename = 'file';
+  let directory = '';
+  let folder = '';
+  let query = '';
+
+  if (loaderContext.resourcePath) {
+    const parsed = path.parse(loaderContext.resourcePath);
+    let resourcePath = loaderContext.resourcePath;
+
+    if (parsed.ext) {
+      ext = parsed.ext.substr(1);
+    }
+
+    if (parsed.dir) {
+      basename = parsed.name;
+      resourcePath = parsed.dir + path.sep;
+    }
+
+    if (typeof context !== 'undefined') {
+      directory = path
+        .relative(context, resourcePath + '_')
+        .replace(/\\/g, '/')
+        .replace(/\.\.(\/)?/g, '_$1');
+      directory = directory.substr(0, directory.length - 1);
+    } else {
+      directory = resourcePath.replace(/\\/g, '/').replace(/\.\.(\/)?/g, '_$1');
+    }
+
+    if (directory.length === 1) {
+      directory = '';
+    } else if (directory.length > 1) {
+      folder = path.basename(directory);
+    }
+  }
+
+  if (loaderContext.resourceQuery && loaderContext.resourceQuery.length > 1) {
+    query = loaderContext.resourceQuery;
+
+    const hashIdx = query.indexOf('#');
+
+    if (hashIdx >= 0) {
+      query = query.substr(0, hashIdx);
+    }
+  }
+
+  let url = filename;
+
+  if (content) {
+    // Match hash template
+    url = url
+      // `hash` and `contenthash` are same in `loader-utils` context
+      // let's keep `hash` for backward compatibility
+      .replace(
+        /\[(?:([^[:\]]+):)?(?:hash|contenthash)(?::([a-z]+\d*))?(?::(\d+))?\]/gi,
+        (all, hashType, digestType, maxLength) =>
+          getHashDigest(content, hashType, digestType, parseInt(maxLength, 10))
+      )
+      .replace(/\[emoji(?::(\d+))?\]/gi, (all, length) =>
+        encodeStringToEmoji(content, parseInt(length, 10))
+      );
+  }
+
+  url = url
+    .replace(/\[ext\]/gi, () => ext)
+    .replace(/\[name\]/gi, () => basename)
+    .replace(/\[path\]/gi, () => directory)
+    .replace(/\[folder\]/gi, () => folder)
+    .replace(/\[query\]/gi, () => query);
+
+  if (regExp && loaderContext.resourcePath) {
+    const match = loaderContext.resourcePath.match(new RegExp(regExp));
+
+    match &&
+      match.forEach((matched, i) => {
+        url = url.replace(new RegExp('\\[' + i + '\\]', 'ig'), matched);
+      });
+  }
+
+  if (
+    typeof loaderContext.options === 'object' &&
+    typeof loaderContext.options.customInterpolateName === 'function'
+  ) {
+    url = loaderContext.options.customInterpolateName.call(
+      loaderContext,
+      url,
+      name,
+      options
+    );
+  }
+
+  return url;
+}
+
+module.exports = interpolateName;
Index: frontend/node_modules/loader-utils/lib/isUrlRequest.js
===================================================================
--- frontend/node_modules/loader-utils/lib/isUrlRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/isUrlRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,31 @@
+'use strict';
+
+const path = require('path');
+
+function isUrlRequest(url, root) {
+  // An URL is not an request if
+
+  // 1. It's an absolute url and it is not `windows` path like `C:\dir\file`
+  if (/^[a-z][a-z0-9+.-]*:/i.test(url) && !path.win32.isAbsolute(url)) {
+    return false;
+  }
+
+  // 2. It's a protocol-relative
+  if (/^\/\//.test(url)) {
+    return false;
+  }
+
+  // 3. It's some kind of url for a template
+  if (/^[{}[\]#*;,'§$%&(=?`´^°<>]/.test(url)) {
+    return false;
+  }
+
+  // 4. It's also not an request if root isn't set and it's a root-relative url
+  if ((root === undefined || root === false) && /^\//.test(url)) {
+    return false;
+  }
+
+  return true;
+}
+
+module.exports = isUrlRequest;
Index: frontend/node_modules/loader-utils/lib/parseQuery.js
===================================================================
--- frontend/node_modules/loader-utils/lib/parseQuery.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/parseQuery.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,69 @@
+'use strict';
+
+const JSON5 = require('json5');
+
+const specialValues = {
+  null: null,
+  true: true,
+  false: false,
+};
+
+function parseQuery(query) {
+  if (query.substr(0, 1) !== '?') {
+    throw new Error(
+      "A valid query string passed to parseQuery should begin with '?'"
+    );
+  }
+
+  query = query.substr(1);
+
+  if (!query) {
+    return {};
+  }
+
+  if (query.substr(0, 1) === '{' && query.substr(-1) === '}') {
+    return JSON5.parse(query);
+  }
+
+  const queryArgs = query.split(/[,&]/g);
+  const result = Object.create(null);
+
+  queryArgs.forEach((arg) => {
+    const idx = arg.indexOf('=');
+
+    if (idx >= 0) {
+      let name = arg.substr(0, idx);
+      let value = decodeURIComponent(arg.substr(idx + 1));
+
+      // eslint-disable-next-line no-prototype-builtins
+      if (specialValues.hasOwnProperty(value)) {
+        value = specialValues[value];
+      }
+
+      if (name.substr(-2) === '[]') {
+        name = decodeURIComponent(name.substr(0, name.length - 2));
+
+        if (!Array.isArray(result[name])) {
+          result[name] = [];
+        }
+
+        result[name].push(value);
+      } else {
+        name = decodeURIComponent(name);
+        result[name] = value;
+      }
+    } else {
+      if (arg.substr(0, 1) === '-') {
+        result[decodeURIComponent(arg.substr(1))] = false;
+      } else if (arg.substr(0, 1) === '+') {
+        result[decodeURIComponent(arg.substr(1))] = true;
+      } else {
+        result[decodeURIComponent(arg)] = true;
+      }
+    }
+  });
+
+  return result;
+}
+
+module.exports = parseQuery;
Index: frontend/node_modules/loader-utils/lib/parseString.js
===================================================================
--- frontend/node_modules/loader-utils/lib/parseString.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/parseString.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,23 @@
+'use strict';
+
+function parseString(str) {
+  try {
+    if (str[0] === '"') {
+      return JSON.parse(str);
+    }
+
+    if (str[0] === "'" && str.substr(str.length - 1) === "'") {
+      return parseString(
+        str
+          .replace(/\\.|"/g, (x) => (x === '"' ? '\\"' : x))
+          .replace(/^'|'$/g, '"')
+      );
+    }
+
+    return JSON.parse('"' + str + '"');
+  } catch (e) {
+    return str;
+  }
+}
+
+module.exports = parseString;
Index: frontend/node_modules/loader-utils/lib/stringifyRequest.js
===================================================================
--- frontend/node_modules/loader-utils/lib/stringifyRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/stringifyRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,51 @@
+'use strict';
+
+const path = require('path');
+
+const matchRelativePath = /^\.\.?[/\\]/;
+
+function isAbsolutePath(str) {
+  return path.posix.isAbsolute(str) || path.win32.isAbsolute(str);
+}
+
+function isRelativePath(str) {
+  return matchRelativePath.test(str);
+}
+
+function stringifyRequest(loaderContext, request) {
+  const splitted = request.split('!');
+  const context =
+    loaderContext.context ||
+    (loaderContext.options && loaderContext.options.context);
+
+  return JSON.stringify(
+    splitted
+      .map((part) => {
+        // First, separate singlePath from query, because the query might contain paths again
+        const splittedPart = part.match(/^(.*?)(\?.*)/);
+        const query = splittedPart ? splittedPart[2] : '';
+        let singlePath = splittedPart ? splittedPart[1] : part;
+
+        if (isAbsolutePath(singlePath) && context) {
+          singlePath = path.relative(context, singlePath);
+
+          if (isAbsolutePath(singlePath)) {
+            // If singlePath still matches an absolute path, singlePath was on a different drive than context.
+            // In this case, we leave the path platform-specific without replacing any separators.
+            // @see https://github.com/webpack/loader-utils/pull/14
+            return singlePath + query;
+          }
+
+          if (isRelativePath(singlePath) === false) {
+            // Ensure that the relative path starts at least with ./ otherwise it would be a request into the modules directory (like node_modules).
+            singlePath = './' + singlePath;
+          }
+        }
+
+        return singlePath.replace(/\\/g, '/') + query;
+      })
+      .join('!')
+  );
+}
+
+module.exports = stringifyRequest;
Index: frontend/node_modules/loader-utils/lib/urlToRequest.js
===================================================================
--- frontend/node_modules/loader-utils/lib/urlToRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/loader-utils/lib/urlToRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,60 @@
+'use strict';
+
+// we can't use path.win32.isAbsolute because it also matches paths starting with a forward slash
+const matchNativeWin32Path = /^[A-Z]:[/\\]|^\\\\/i;
+
+function urlToRequest(url, root) {
+  // Do not rewrite an empty url
+  if (url === '') {
+    return '';
+  }
+
+  const moduleRequestRegex = /^[^?]*~/;
+  let request;
+
+  if (matchNativeWin32Path.test(url)) {
+    // absolute windows path, keep it
+    request = url;
+  } else if (root !== undefined && root !== false && /^\//.test(url)) {
+    // if root is set and the url is root-relative
+    switch (typeof root) {
+      // 1. root is a string: root is prefixed to the url
+      case 'string':
+        // special case: `~` roots convert to module request
+        if (moduleRequestRegex.test(root)) {
+          request = root.replace(/([^~/])$/, '$1/') + url.slice(1);
+        } else {
+          request = root + url;
+        }
+        break;
+      // 2. root is `true`: absolute paths are allowed
+      //    *nix only, windows-style absolute paths are always allowed as they doesn't start with a `/`
+      case 'boolean':
+        request = url;
+        break;
+      default:
+        throw new Error(
+          "Unexpected parameters to loader-utils 'urlToRequest': url = " +
+            url +
+            ', root = ' +
+            root +
+            '.'
+        );
+    }
+  } else if (/^\.\.?\//.test(url)) {
+    // A relative url stays
+    request = url;
+  } else {
+    // every other url is threaded like a relative url
+    request = './' + url;
+  }
+
+  // A `~` makes the url an module
+  if (moduleRequestRegex.test(request)) {
+    request = request.replace(moduleRequestRegex, '');
+  }
+
+  return request;
+}
+
+module.exports = urlToRequest;
