diff --git a/frontends/election-manager/.eslintignore b/frontends/election-manager/.eslintignore index 5e4306738b..f4c038b7e2 100644 --- a/frontends/election-manager/.eslintignore +++ b/frontends/election-manager/.eslintignore @@ -6,3 +6,5 @@ /prodserver /src/**/*.js *.d.ts +*.config.js +*.config.ts diff --git a/frontends/election-manager/index.html b/frontends/election-manager/index.html new file mode 100644 index 0000000000..5eaaa968e5 --- /dev/null +++ b/frontends/election-manager/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + VotingWorks VxAdmin + + + +
+ + + + \ No newline at end of file diff --git a/frontends/election-manager/package.json b/frontends/election-manager/package.json index 62b477b3b8..90c24a8083 100644 --- a/frontends/election-manager/package.json +++ b/frontends/election-manager/package.json @@ -10,12 +10,14 @@ "scripts": { "type-check": "tsc --build", "build": "tsc --build && react-scripts build", + "build:vite": "vite build", "build:watch": "tsc --build --watch", "eject": "react-scripts eject", "format": "prettier '**/*.+(css|graphql|json|less|md|mdx|sass|scss|yaml|yml)' --write", "lint": "pnpm type-check && eslint . && pnpm stylelint:run", "lint:fix": "pnpm type-check && eslint . --fix && pnpm stylelint:run:fix", "start": "react-scripts start", + "start:vite": "vite", "stylelint:run": "stylelint 'src/**/*.{js,jsx,ts,tsx}' && stylelint 'src/**/*.css' --config .stylelintrc-css.js", "stylelint:run:fix": "stylelint 'src/**/*.{js,jsx,ts,tsx}' --fix && stylelint 'src/**/*.css' --config .stylelintrc-css.js --fix", "test": "is-ci test:coverage test:watch", @@ -92,13 +94,17 @@ "@votingworks/types": "workspace:*", "@votingworks/ui": "workspace:*", "@votingworks/utils": "workspace:*", + "@zip.js/zip.js": "^2.4.12", "array-unique": "^0.3.2", + "assert": "^2.0.0", "base64-js": "^1.3.1", + "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", "canvas": "2.9.1", "dashify": "^2.0.0", "debug": "^4.3.2", "dompurify": "^2.0.12", + "events": "^3.3.0", "fetch-mock": "^9.10.7", "history": "^4.10.1", "http-proxy-middleware": "1.0.6", @@ -111,6 +117,7 @@ "node-fetch": "^2.6.0", "normalize.css": "^8.0.1", "pagedjs": "^0.1.40", + "path": "^0.12.7", "pdfjs-dist": "2.4.456", "pluralize": "^8.0.0", "react": "^17.0.1", @@ -119,9 +126,12 @@ "react-router-dom": "^5.2.0", "react-scripts": "4.0.1", "react-textarea-autosize": "^8.2.0", + "setimmediate": "^1.0.5", + "stream-browserify": "^3.0.0", "styled-components": "^5.2.1", "typescript": "4.6.3", "use-interval": "^1.2.1", + "util": "^0.12.4", "zip-stream": "^3.0.1", "zod": "3.14.4" }, @@ -129,6 +139,7 @@ "@codemod/parser": "^1.0.6", "@testing-library/jest-dom": "^5.16.4", "@types/base64-js": "^1.3.0", + "@types/connect": "^3.4.35", "@types/debug": "^4.1.6", "@types/history": "^4.7.8", "@types/kiosk-browser": "workspace:*", @@ -154,6 +165,7 @@ "eslint-plugin-react": "^7.18.3", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-vx": "workspace:*", + "express": "^4.18.1", "is-ci-cli": "^2.1.2", "jest": "^27.3.1", "jest-environment-jsdom-sixteen": "^1.0.3", @@ -166,7 +178,8 @@ "stylelint-config-palantir": "^4.0.1", "stylelint-config-prettier": "^8.0.1", "stylelint-config-styled-components": "^0.1.1", - "stylelint-processor-styled-components": "^1.10.0" + "stylelint-processor-styled-components": "^1.10.0", + "vite": "^2.9.9" }, "vx": { "isBundled": true, diff --git a/frontends/election-manager/prodserver/setupProxy.js b/frontends/election-manager/prodserver/setupProxy.js index d12b47fc35..bbe931f96a 100644 --- a/frontends/election-manager/prodserver/setupProxy.js +++ b/frontends/election-manager/prodserver/setupProxy.js @@ -11,16 +11,22 @@ const express = require('express'); const { createProxyMiddleware: proxy } = require('http-proxy-middleware'); const { dirname, join } = require('path'); +/** + * @param {import('connect').Server} app + */ module.exports = function (app) { app.use(proxy('/card', { target: 'http://localhost:3001/' })); app.use(proxy('/convert', { target: 'http://localhost:3003/' })); app.use(proxy('/admin', { target: 'http://localhost:3004/' })); - app.get('/machine-config', (req, res) => { - res.json({ - machineId: process.env.VX_MACHINE_ID || '0000', - codeVersion: process.env.VX_CODE_VERSION || 'dev', - }); + app.use('/machine-config', (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.end( + JSON.stringify({ + machineId: process.env.VX_MACHINE_ID || '0000', + codeVersion: process.env.VX_CODE_VERSION || 'dev', + }) + ); }); const pdfjsDistBuildPath = dirname( diff --git a/frontends/election-manager/src/index.tsx b/frontends/election-manager/src/index.tsx index 91b2f68c48..1a91e29b78 100644 --- a/frontends/election-manager/src/index.tsx +++ b/frontends/election-manager/src/index.tsx @@ -1,3 +1,4 @@ +import './polyfills'; import React from 'react'; import ReactDom from 'react-dom'; import './i18n'; diff --git a/frontends/election-manager/src/polyfills.ts b/frontends/election-manager/src/polyfills.ts new file mode 100644 index 0000000000..de592f1022 --- /dev/null +++ b/frontends/election-manager/src/polyfills.ts @@ -0,0 +1,13 @@ +/** + * Provides polyfills needed for this application and its dependencies. + */ + +/* istanbul ignore file */ +import { Buffer } from 'buffer'; +import 'setimmediate'; + +globalThis.global = globalThis; +globalThis.Buffer = Buffer; +globalThis.process ??= {} as unknown as typeof process; + +process.nextTick = setImmediate; diff --git a/frontends/election-manager/src/stubs/glob.ts b/frontends/election-manager/src/stubs/glob.ts new file mode 100644 index 0000000000..fc3e9c48ca --- /dev/null +++ b/frontends/election-manager/src/stubs/glob.ts @@ -0,0 +1,4 @@ +// This file exists to serve as a stub for the `glob` module. +// See `vite.config.ts` under `resolve.alias` for the configuration. + +export {}; diff --git a/frontends/election-manager/src/utils/downloadable_archive.test.ts b/frontends/election-manager/src/utils/downloadable_archive.test.ts index 7dc9e8f02b..25f5082270 100644 --- a/frontends/election-manager/src/utils/downloadable_archive.test.ts +++ b/frontends/election-manager/src/utils/downloadable_archive.test.ts @@ -1,11 +1,10 @@ import { fakeKiosk } from '@votingworks/test-utils'; -import { Buffer } from 'buffer'; import { fakeFileWriter } from '../../test/helpers/fake_file_writer'; import { DownloadableArchive } from './downloadable_archive'; // https://en.wikipedia.org/wiki/List_of_file_signatures -const ZIP_MAGIC_BYTES = Buffer.of(0x50, 0x4b, 0x03, 0x04); -const EMPTY_ZIP_MAGIC_BYTES = Buffer.of(0x50, 0x4b, 0x05, 0x06); +const ZIP_MAGIC_BYTES = [0x50, 0x4b, 0x03, 0x04]; +const EMPTY_ZIP_MAGIC_BYTES = [0x50, 0x4b, 0x05, 0x06]; test('file prompt fails', async () => { const kiosk = fakeKiosk(); @@ -39,9 +38,8 @@ test('empty zip file when user is prompted for file location', async () => { await archive.end(); expect(fileWriter.chunks).not.toHaveLength(0); - const firstChunk = fileWriter.chunks[0] as Buffer; - expect(firstChunk).toBeInstanceOf(Buffer); - expect(firstChunk.slice(0, EMPTY_ZIP_MAGIC_BYTES.length)).toEqual( + const firstChunk = fileWriter.chunks[0] as Uint8Array; + expect(Array.from(firstChunk.slice(0, EMPTY_ZIP_MAGIC_BYTES.length))).toEqual( EMPTY_ZIP_MAGIC_BYTES ); }); @@ -63,9 +61,8 @@ test('empty zip file when file is saved directly and passes path to kiosk proper }); expect(kiosk.writeFile).toHaveBeenCalledWith('/path/to/folder/file.zip'); - const firstChunk = fileWriter.chunks[0] as Buffer; - expect(firstChunk).toBeInstanceOf(Buffer); - expect(firstChunk.slice(0, EMPTY_ZIP_MAGIC_BYTES.length)).toEqual( + const firstChunk = fileWriter.chunks[0] as Uint8Array; + expect(Array.from(firstChunk.slice(0, EMPTY_ZIP_MAGIC_BYTES.length))).toEqual( EMPTY_ZIP_MAGIC_BYTES ); }); @@ -83,9 +80,10 @@ test('zip file containing a file', async () => { await archive.end(); expect(fileWriter.chunks).not.toHaveLength(0); - const firstChunk = fileWriter.chunks[0] as Buffer; - expect(firstChunk).toBeInstanceOf(Buffer); - expect(firstChunk.slice(0, ZIP_MAGIC_BYTES.length)).toEqual(ZIP_MAGIC_BYTES); + const firstChunk = fileWriter.chunks[0] as Uint8Array; + expect(Array.from(firstChunk.slice(0, ZIP_MAGIC_BYTES.length))).toEqual( + ZIP_MAGIC_BYTES + ); }); test('passes options to kiosk.saveAs', async () => { diff --git a/frontends/election-manager/src/utils/downloadable_archive.ts b/frontends/election-manager/src/utils/downloadable_archive.ts index 6675533995..891a4fb211 100644 --- a/frontends/election-manager/src/utils/downloadable_archive.ts +++ b/frontends/election-manager/src/utils/downloadable_archive.ts @@ -1,6 +1,35 @@ -import { assert, deferred } from '@votingworks/utils'; -import ZipStream from 'zip-stream'; +/* eslint-disable max-classes-per-file */ +import { assert } from '@votingworks/utils'; +import { Buffer } from 'buffer'; import path from 'path'; +import { configure, Uint8ArrayReader, ZipWriter, Writer } from '@zip.js/zip.js'; + +configure({ useWebWorkers: false }); + +/** + * Forwards data from a `ZipWriter` to a kiosk-browser file writer. + */ +class KioskBrowserZipFileWriter extends Writer { + constructor(private readonly fileWriter: KioskBrowser.FileWriter) { + super(); + } + + /** + * Called whenever there is new data to write to the zip file. + */ + async writeUint8Array(array: Uint8Array): Promise { + await super.writeUint8Array(array); + await this.fileWriter.write(array); + } + + /** + * This function is required by the ZipWriter interface, but we ignore its + * return value. It is called when closing the zip file. + */ + async getData(): Promise { + return Promise.resolve(Uint8Array.of()); + } +} /** * Provides support for downloading a Zip archive of files. Requires @@ -8,8 +37,7 @@ import path from 'path'; * that the executing host is allowed to use the `saveAs` API. */ export class DownloadableArchive { - private zip?: ZipStream; - private endPromise?: Promise; + private writer?: ZipWriter; constructor(private readonly kiosk = window.kiosk) {} @@ -30,13 +58,7 @@ export class DownloadableArchive { throw new Error('could not begin download; no file was chosen'); } - let endResolve: () => void; - this.endPromise = new Promise((resolve) => { - endResolve = resolve; - }); - this.zip = new ZipStream() - .on('data', (chunk) => fileWriter.write(chunk)) - .on('end', () => fileWriter.end().then(endResolve)); + this.prepareZip(fileWriter); } /** @@ -57,42 +79,38 @@ export class DownloadableArchive { throw new Error('could not begin download; an error occurred'); } - const { promise: endPromise, resolve: endResolve } = deferred(); - this.endPromise = endPromise; - this.zip = new ZipStream() - .on('data', (chunk) => fileWriter.write(chunk)) - .on('end', () => fileWriter.end().then(endResolve)); + this.prepareZip(fileWriter); + } + + /** + * Prepares the zip archive for writing to the given file writer. + */ + private prepareZip(fileWriter: KioskBrowser.FileWriter): void { + this.writer = new ZipWriter(new KioskBrowserZipFileWriter(fileWriter)); } /** * Writes a file to the archive, resolves when complete. */ - async file( - name: string, - data: Parameters[0] - ): Promise { - const { zip } = this; + async file(name: string, data: string | Buffer): Promise { + const { writer } = this; - if (!zip) { + if (!writer) { throw new Error('cannot call file() before begin()'); } - return new Promise((resolve, reject) => { - zip.entry(data, { name }, (err) => (err ? reject(err) : resolve())); - }); + await writer.add(name, new Uint8ArrayReader(Buffer.from(data))); } /** * Finishes the zip archive and ends the download. */ async end(): Promise { - if (!this.zip) { + if (!this.writer) { throw new Error('cannot call end() before begin()'); } - this.zip.finalize(); - await this.endPromise; - this.zip = undefined; - this.endPromise = undefined; + await this.writer.close(); + this.writer = undefined; } } diff --git a/frontends/election-manager/vite.config.ts b/frontends/election-manager/vite.config.ts new file mode 100644 index 0000000000..5bb2487822 --- /dev/null +++ b/frontends/election-manager/vite.config.ts @@ -0,0 +1,101 @@ +import { join } from 'path'; +import { defineConfig, loadEnv } from 'vite'; +import { getWorkspacePackageInfo } from '../../script/src/validate-monorepo/pnpm'; +import setupProxy from './prodserver/setupProxy'; + +export default defineConfig(async (env) => { + const workspacePackages = await getWorkspacePackageInfo( + join(__dirname, '../..') + ); + + const envPrefix = 'REACT_APP_'; + const dotenvValues = loadEnv(env.mode, __dirname, envPrefix); + const processEnvDefines = Object.entries(dotenvValues).reduce< + Record + >( + (acc, [key, value]) => ({ + ...acc, + [`process.env.${key}`]: JSON.stringify(value), + }), + {} + ); + + return { + build: { + // Write build files to `build` directory. + outDir: 'build', + + // Do not minify build files. We don't need the space savings and this is + // a minor transparency improvement. + minify: false, + }, + + // Replace some code in Node modules, `#define`-style, to avoid referencing + // Node-only globals like `process`. + define: { + 'process.env.NODE_DEBUG': JSON.stringify(undefined), + 'process.version': JSON.stringify(process.version), + + // TODO: Replace these with the appropriate `import.meta.env` values (#1907). + ...processEnvDefines, + }, + + resolve: { + alias: [ + // Replace NodeJS built-in modules with polyfills. + // + // The trailing slash is important for the ones with the same name. + // Without it, they will be resolved as built-in NodeJS modules. + { find: 'assert', replacement: require.resolve('assert/') }, + { find: 'buffer', replacement: require.resolve('buffer/') }, + { find: 'events', replacement: require.resolve('events/') }, + { find: 'stream', replacement: require.resolve('stream-browserify') }, + { find: 'util', replacement: require.resolve('util/') }, + { find: 'zlib', replacement: require.resolve('browserify-zlib') }, + + // Work around a broken `module` entry in pagedjs's `package.json`. + // https://github.com/vitejs/vite/issues/1488 + { + find: 'pagedjs', + replacement: require.resolve('pagedjs/dist/paged.esm'), + }, + + // Work around an internet curmudgeon. + // Problem: https://github.com/isaacs/node-glob/pull/374 + // Fix: https://github.com/isaacs/node-glob/pull/479 + { find: 'glob', replacement: join(__dirname, './src/stubs/glob.ts') }, + + // Create aliases for all workspace packages, i.e. + // + // { + // '@votingworks/types': '…/libs/types/src/index.ts', + // '@votingworks/utils': '…/libs/utils/src/index.ts', + // … + // } + // + // This allows re-mapping imports for workspace packages to their + // TypeScript source code rather than the built JavaScript. + ...Array.from(workspacePackages.values()).reduce( + (aliases, { path, name, source }) => + !source + ? aliases + : [...aliases, { find: name, replacement: join(path, source) }], + [] + ), + ], + }, + + plugins: [ + // Setup the proxy to local services, e.g. `smartcards`. + { + name: 'development-proxy', + configureServer: (app) => { + setupProxy(app.middlewares); + }, + }, + ], + + // Pass some environment variables to the client in `import.meta.env`. + envPrefix, + }; +}); diff --git a/libs/ballot-interpreter-vx/package.json b/libs/ballot-interpreter-vx/package.json index d1ff09e90c..2cd7a9f4e3 100644 --- a/libs/ballot-interpreter-vx/package.json +++ b/libs/ballot-interpreter-vx/package.json @@ -53,6 +53,7 @@ "node-quirc": "^2.2.1", "randombytes": "^2.1.0", "table": "^6.0.3", + "util": "^0.12.4", "uuid": "^8.3.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b2024dc3f..61ec54fd83 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -734,13 +734,17 @@ importers: '@votingworks/types': link:../../libs/types '@votingworks/ui': link:../../libs/ui '@votingworks/utils': link:../../libs/utils + '@zip.js/zip.js': 2.4.12 array-unique: 0.3.2 + assert: 2.0.0 base64-js: 1.5.1 + browserify-zlib: 0.2.0 buffer: 6.0.3 canvas: 2.9.1 dashify: 2.0.0 debug: 4.3.2 dompurify: 2.2.6 + events: 3.3.0 fetch-mock: 9.11.0 history: 4.10.1 http-proxy-middleware: 1.0.6 @@ -753,6 +757,7 @@ importers: node-fetch: 2.6.1 normalize.css: 8.0.1 pagedjs: 0.1.43 + path: 0.12.7 pdfjs-dist: 2.4.456 pluralize: 8.0.0 react: 17.0.1 @@ -761,15 +766,19 @@ importers: react-router-dom: 5.2.0_react@17.0.1 react-scripts: 4.0.1_typescript@4.6.3 react-textarea-autosize: 8.3.0_e8f6b04531727420b27210e589e7d031 + setimmediate: 1.0.5 + stream-browserify: 3.0.0 styled-components: 5.2.1_react-dom@17.0.1+react@17.0.1 typescript: 4.6.3 use-interval: 1.3.0_react@17.0.1 + util: 0.12.4 zip-stream: 3.0.1 zod: 3.14.4 devDependencies: '@codemod/parser': 1.1.0 '@testing-library/jest-dom': 5.16.4 '@types/base64-js': 1.3.0 + '@types/connect': 3.4.35 '@types/debug': 4.1.6 '@types/history': 4.7.8 '@types/kiosk-browser': link:../../libs/@types/kiosk-browser @@ -795,6 +804,7 @@ importers: eslint-plugin-react: 7.22.0_eslint@7.17.0 eslint-plugin-react-hooks: 4.2.0_eslint@7.17.0 eslint-plugin-vx: link:../../libs/eslint-plugin-vx + express: 4.18.1 is-ci-cli: 2.1.2 jest: 27.5.1_canvas@2.9.1 jest-environment-jsdom-sixteen: 1.0.3_canvas@2.9.1 @@ -808,6 +818,7 @@ importers: stylelint-config-prettier: 8.0.2_stylelint@13.8.0 stylelint-config-styled-components: 0.1.1 stylelint-processor-styled-components: 1.10.0 + vite: 2.9.9 specifiers: '@codemod/parser': ^1.0.6 '@testing-library/jest-dom': ^5.16.4 @@ -815,6 +826,7 @@ importers: '@testing-library/user-event': ^12.6.0 '@types/array-unique': ^0.3.0 '@types/base64-js': ^1.3.0 + '@types/connect': ^3.4.35 '@types/dashify': ^1.0.0 '@types/debug': ^4.1.6 '@types/dompurify': ^2.0.3 @@ -844,8 +856,11 @@ importers: '@votingworks/types': workspace:* '@votingworks/ui': workspace:* '@votingworks/utils': workspace:* + '@zip.js/zip.js': ^2.4.12 array-unique: ^0.3.2 + assert: ^2.0.0 base64-js: ^1.3.1 + browserify-zlib: ^0.2.0 buffer: ^6.0.3 canvas: 2.9.1 dashify: ^2.0.0 @@ -865,6 +880,8 @@ importers: eslint-plugin-react: ^7.18.3 eslint-plugin-react-hooks: ^4.2.0 eslint-plugin-vx: workspace:* + events: ^3.3.0 + express: ^4.18.1 fetch-mock: ^9.10.7 history: ^4.10.1 http-proxy-middleware: 1.0.6 @@ -881,6 +898,7 @@ importers: node-fetch: ^2.6.0 normalize.css: ^8.0.1 pagedjs: ^0.1.40 + path: ^0.12.7 pdfjs-dist: 2.4.456 pluralize: ^8.0.0 prettier: ^2.6.2 @@ -892,7 +910,9 @@ importers: react-scripts: 4.0.1 react-test-renderer: ^16.13.1 react-textarea-autosize: ^8.2.0 + setimmediate: ^1.0.5 sort-package-json: ^1.50.0 + stream-browserify: ^3.0.0 styled-components: ^5.2.1 stylelint: ^13.1.0 stylelint-config-palantir: ^4.0.1 @@ -901,6 +921,8 @@ importers: stylelint-processor-styled-components: ^1.10.0 typescript: 4.6.3 use-interval: ^1.2.1 + util: ^0.12.4 + vite: ^2.9.9 zip-stream: ^3.0.1 zod: 3.14.4 frontends/election-manager/prodserver: @@ -1404,6 +1426,7 @@ importers: node-quirc: 2.3.0 randombytes: 2.1.0 table: 6.0.7 + util: 0.12.4 uuid: 8.3.2 devDependencies: '@types/benchmark': 1.0.33 @@ -1494,6 +1517,7 @@ importers: tmp: ^0.2.1 ts-jest: ^27.0.7 typescript: 4.6.3 + util: ^0.12.4 uuid: ^8.3.1 libs/cdf-schema-builder: dependencies: @@ -2387,6 +2411,11 @@ importers: ts-jest: ^27.0.7 typescript: 4.6.3 zod: 3.14.4 + libs/zip.js: + dependencies: + '@zip.js/zip.js': 2.4.12 + specifiers: + '@zip.js/zip.js': ^2.4.12 script: dependencies: resolve-from: 5.0.0 @@ -8766,7 +8795,7 @@ packages: integrity: sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== /@types/connect/3.4.35: dependencies: - '@types/node': 17.0.36 + '@types/node': 16.11.29 dev: true resolution: integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== @@ -11260,6 +11289,10 @@ packages: /@xtuc/long/4.2.2: resolution: integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + /@zip.js/zip.js/2.4.12: + dev: false + resolution: + integrity: sha512-Y9VTILBi067CUNyaCTeLXxAVbB547EdUfAJZndfpnqYm8r2blVmHAqvggPYw2p89rTCvJAhDXlGkY4cv0+JYmA== /abab/2.0.6: resolution: integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== @@ -11766,6 +11799,15 @@ packages: util: 0.10.3 resolution: integrity: sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== + /assert/2.0.0: + dependencies: + es6-object-assign: 1.1.0 + is-nan: 1.3.2 + object-is: 1.1.5 + util: 0.12.4 + dev: false + resolution: + integrity: sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A== /assign-symbols/1.0.0: engines: node: '>=0.10.0' @@ -14920,6 +14962,10 @@ packages: es6-symbol: 3.1.3 resolution: integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + /es6-object-assign/1.1.0: + dev: false + resolution: + integrity: sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw== /es6-symbol/3.1.3: dependencies: d: 1.0.1 @@ -17737,7 +17783,7 @@ packages: '@typescript-eslint/eslint-plugin': 5.21.0_94a944f85573090af1491a822dcbe40b '@typescript-eslint/utils': 5.22.0_eslint@7.19.0+typescript@4.6.3 eslint: 7.19.0 - jest: 27.3.1 + jest: 27.3.1_canvas@2.9.1 dev: true engines: node: ^12.22.0 || ^14.17.0 || >=16.0.0 @@ -19583,6 +19629,44 @@ packages: node: '>= 0.10.0' resolution: integrity: sha512-EJEXxiTQJS3lIPrU1AE2vRuT7X7E+0KBbpm5GSoK524yl0K8X+er8zS2P14E64eqsVNoWbMCT7MpmQ+ErAhgRg== + /express/4.18.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.0 + content-disposition: 0.5.4 + content-type: 1.0.4 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.10.3 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + dev: true + engines: + node: '>= 0.10.0' + resolution: + integrity: sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== /ext/1.4.0: dependencies: type: 2.1.0 @@ -21150,7 +21234,7 @@ packages: /immediate/3.0.6: dev: false resolution: - integrity: sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== /immer/8.0.1: dev: false resolution: @@ -21637,6 +21721,15 @@ packages: dev: false resolution: integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + /is-nan/1.3.2: + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + dev: false + engines: + node: '>= 0.4' + resolution: + integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== /is-negative-zero/2.0.1: engines: node: '>= 0.4' @@ -26167,6 +26260,15 @@ packages: node: '>= 0.4' resolution: integrity: sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg== + /object-is/1.1.5: + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + dev: false + engines: + node: '>= 0.4' + resolution: + integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== /object-keys/1.1.1: engines: node: '>= 0.4' @@ -26660,7 +26762,7 @@ packages: integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== /path-to-regexp/0.1.7: resolution: - integrity: sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== /path-to-regexp/1.8.0: dependencies: isarray: 0.0.1 @@ -26690,6 +26792,13 @@ packages: node: '>=8' resolution: integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + /path/0.12.7: + dependencies: + process: 0.11.10 + util: 0.10.4 + dev: false + resolution: + integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q== /pause-stream/0.0.11: dependencies: through: 2.3.8 @@ -27818,7 +27927,7 @@ packages: engines: node: '>= 0.6.0' resolution: - integrity: sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== /progress/2.0.3: engines: node: '>=0.4.0' @@ -29826,6 +29935,13 @@ packages: readable-stream: 2.3.7 resolution: integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== + /stream-browserify/3.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: false + resolution: + integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== /stream-combiner/0.0.4: dependencies: duplexer: 0.1.2 @@ -31767,6 +31883,12 @@ packages: inherits: 2.0.1 resolution: integrity: sha1-evsa/lCAUkZInj23/g7TeTNqwPk= + /util/0.10.4: + dependencies: + inherits: 2.0.3 + dev: false + resolution: + integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== /util/0.11.1: dependencies: inherits: 2.0.3