1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-05 23:53:21 +00:00

Upgrade the jest test suite to match the clients (#379)

* Upgrade the jest test suite to match the clients

* Update makeStaticByteArray
This commit is contained in:
Oscar Hinton
2023-12-12 13:45:12 +01:00
committed by GitHub
parent 36ab2953b5
commit 1546cc2012
24 changed files with 379 additions and 107 deletions

27
jest.config.js Normal file
View File

@@ -0,0 +1,27 @@
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
/** @type {import('jest').Config} */
module.exports = {
reporters: ["default", "jest-junit"],
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/",
}),
projects: [
"<rootDir>/jslib/angular/jest.config.js",
"<rootDir>/jslib/common/jest.config.js",
"<rootDir>/jslib/electron/jest.config.js",
"<rootDir>/jslib/node/jest.config.js",
],
// Workaround for a memory leak that crashes tests in CI:
// https://github.com/facebook/jest/issues/9430#issuecomment-1149882002
// Also anecdotally improves performance when run locally
maxWorkers: 3,
};

View File

@@ -1,16 +1,15 @@
const { pathsToModuleNameMapper } = require("ts-jest/utils");
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
const { compilerOptions } = require("../shared/tsconfig.libs");
const sharedConfig = require("../shared/jest.config.angular");
/** @type {import('jest').Config} */
module.exports = {
name: "angular",
displayName: "angular tests",
...sharedConfig,
displayName: "libs/angular tests",
preset: "jest-preset-angular",
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/",
}),

View File

@@ -0,0 +1,28 @@
import { webcrypto } from "crypto";
import "jest-preset-angular/setup-jest";
Object.defineProperty(window, "CSS", { value: null });
Object.defineProperty(window, "getComputedStyle", {
value: () => {
return {
display: "none",
appearance: ["-webkit-appearance"],
};
},
});
Object.defineProperty(document, "doctype", {
value: "<!DOCTYPE html>",
});
Object.defineProperty(document.body.style, "transform", {
value: () => {
return {
enumerable: true,
configurable: true,
};
},
});
Object.defineProperty(window, "crypto", {
value: webcrypto,
});

View File

@@ -1,17 +1,16 @@
const { pathsToModuleNameMapper } = require("ts-jest/utils");
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
const { compilerOptions } = require("../shared/tsconfig.libs");
const sharedConfig = require("../shared/jest.config.ts");
/** @type {import('jest').Config} */
module.exports = {
name: "common",
displayName: "common jslib tests",
...sharedConfig,
displayName: "libs/common tests",
preset: "ts-jest",
testEnvironment: "jsdom",
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/",
}),

View File

@@ -0,0 +1 @@
export * from "./matchers";

View File

@@ -0,0 +1 @@
export * from "./to-equal-buffer";

View File

@@ -0,0 +1,25 @@
import { makeStaticByteArray } from "../utils";
describe("toEqualBuffer custom matcher", () => {
it("matches identical ArrayBuffers", () => {
const array = makeStaticByteArray(10);
expect(array.buffer).toEqualBuffer(array.buffer);
});
it("matches an identical ArrayBuffer and Uint8Array", () => {
const array = makeStaticByteArray(10);
expect(array.buffer).toEqualBuffer(array);
});
it("doesn't match different ArrayBuffers", () => {
const array1 = makeStaticByteArray(10);
const array2 = makeStaticByteArray(10, 11);
expect(array1.buffer).not.toEqualBuffer(array2.buffer);
});
it("doesn't match a different ArrayBuffer and Uint8Array", () => {
const array1 = makeStaticByteArray(10);
const array2 = makeStaticByteArray(10, 11);
expect(array1.buffer).not.toEqualBuffer(array2);
});
});

View File

@@ -0,0 +1,31 @@
/**
* The inbuilt toEqual() matcher will always return TRUE when provided with 2 ArrayBuffers.
* This is because an ArrayBuffer must be wrapped in a new Uint8Array to be accessible.
* This custom matcher will automatically instantiate a new Uint8Array on the received value
* (and optionally, the expected value) and then call toEqual() on the resulting Uint8Arrays.
*/
export const toEqualBuffer: jest.CustomMatcher = function (
received: ArrayBuffer | Uint8Array,
expected: ArrayBuffer | Uint8Array,
) {
received = new Uint8Array(received);
expected = new Uint8Array(expected);
if (this.equals(received, expected)) {
return {
message: () => `expected
${received}
not to match
${expected}`,
pass: true,
};
}
return {
message: () => `expected
${received}
to match
${expected}`,
pass: false,
};
};

View File

@@ -28,10 +28,10 @@ export function mockEnc(s: string): EncString {
return mock;
}
export function makeStaticByteArray(length: number) {
export function makeStaticByteArray(length: number, start = 0) {
const arr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
arr[i] = i;
arr[i] = start + i;
}
return arr;
}

View File

@@ -0,0 +1,17 @@
import { webcrypto } from "crypto";
import { toEqualBuffer } from "./spec";
Object.defineProperty(window, "crypto", {
value: webcrypto,
});
// Add custom matchers
expect.extend({
toEqualBuffer: toEqualBuffer,
});
export interface CustomMatchers<R = unknown> {
toEqualBuffer(expected: Uint8Array | ArrayBuffer): R;
}

View File

@@ -1,15 +1,16 @@
const { pathsToModuleNameMapper } = require("ts-jest/utils");
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
const { compilerOptions } = require("../shared/tsconfig.libs");
const sharedConfig = require("../shared/jest.config.ts");
/** @type {import('jest').Config} */
module.exports = {
...sharedConfig,
displayName: "libs/electron tests",
preset: "ts-jest",
testEnvironment: "jsdom",
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
testEnvironment: "node",
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/",
}),

View File

View File

@@ -1,18 +0,0 @@
const { pathsToModuleNameMapper } = require("ts-jest/utils");
const { compilerOptions } = require("./tsconfig");
module.exports = {
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/",
}),
projects: [
"<rootDir>/angular/jest.config.js",
"<rootDir>/common/jest.config.js",
"<rootDir>/electron/jest.config.js",
"<rootDir>/node/jest.config.js",
],
};

View File

@@ -1,14 +1,16 @@
const { pathsToModuleNameMapper } = require("ts-jest/utils");
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
const { compilerOptions } = require("../shared/tsconfig.libs");
const sharedConfig = require("../shared/jest.config.ts");
/** @type {import('jest').Config} */
module.exports = {
...sharedConfig,
displayName: "libs/node tests",
preset: "ts-jest",
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
testEnvironment: "node",
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/",
}),

0
jslib/node/test.setup.ts Normal file
View File

View File

@@ -1,13 +0,0 @@
{
"name": "@bitwarden/jslib",
"version": "0.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/jslib",
"version": "0.0.0",
"license": "GPL-3.0"
}
}
}

View File

@@ -1,22 +0,0 @@
{
"name": "@bitwarden/jslib",
"version": "0.0.0",
"description": "Common code used across Bitwarden JavaScript projects.",
"keywords": [
"bitwarden"
],
"author": "Bitwarden Inc.",
"homepage": "https://bitwarden.com",
"repository": {
"type": "git",
"url": "https://github.com/bitwarden/jslib"
},
"license": "GPL-3.0",
"scripts": {
"clean": "rimraf dist/**/*",
"test": "jest",
"test:watch": "jest --watch",
"test:watch:all": "jest --watchAll"
},
"dependencies": {}
}

View File

@@ -0,0 +1,36 @@
import * as ts from "typescript";
// Custom Typescript AST transformer for use with ts-jest / jest-preset-angular
// Removes specified ES2020 syntax from source code, as node does not support it yet
// Reference: https://kulshekhar.github.io/ts-jest/docs/getting-started/options/astTransformers
// Use this tool to understand how we identify and filter AST nodes: https://ts-ast-viewer.com/
/**
* Remember to increase the version whenever transformer's content is changed. This is to inform Jest to not reuse
* the previous cache which contains old transformer's content
*/
export const version = 1;
export const name = "bit-es2020-transformer";
// Returns true for 'import.meta' statements
const isImportMetaStatement = (node: ts.Node) =>
ts.isPropertyAccessExpression(node) &&
ts.isMetaProperty(node.expression) &&
node.expression.keywordToken === ts.SyntaxKind.ImportKeyword;
export const factory = function (/*opts?: Opts*/) {
function visitor(ctx: ts.TransformationContext, sf: ts.SourceFile) {
const visitor: ts.Visitor = (node: ts.Node): ts.VisitResult<any> => {
if (isImportMetaStatement(node)) {
return null;
}
// Continue searching child nodes
return ts.visitEachChild(node, visitor, ctx);
};
return visitor;
}
return (ctx: ts.TransformationContext): ts.Transformer<any> => {
return (sf: ts.SourceFile) => ts.visitNode(sf, visitor(ctx, sf));
};
};

View File

@@ -0,0 +1,32 @@
/* eslint-env node */
/* eslint-disable @typescript-eslint/no-var-requires */
const { defaultTransformerOptions } = require("jest-preset-angular/presets");
/** @type {import('jest').Config} */
module.exports = {
testMatch: ["**/+(*.)+(spec).+(ts)"],
// Workaround for a memory leak that crashes tests in CI:
// https://github.com/facebook/jest/issues/9430#issuecomment-1149882002
// Also anecdotally improves performance when run locally
maxWorkers: 3,
transform: {
"^.+\\.(ts|js|mjs|svg)$": [
"jest-preset-angular",
{
...defaultTransformerOptions,
// Jest does not use tsconfig.spec.json by default
tsconfig: "<rootDir>/tsconfig.spec.json",
// Further workaround for memory leak, recommended here:
// https://github.com/kulshekhar/ts-jest/issues/1967#issuecomment-697494014
// Makes tests run faster and reduces size/rate of leak, but loses typechecking on test code
// See https://bitwarden.atlassian.net/browse/EC-497 for more info
isolatedModules: true,
astTransformers: {
before: ["<rootDir>/../../jslib/shared/es2020-transformer.ts"],
},
},
],
},
};

View File

@@ -0,0 +1,29 @@
/* eslint-env node */
/** @type {import('jest').Config} */
module.exports = {
testMatch: ["**/+(*.)+(spec).+(ts)"],
// Workaround for a memory leak that crashes tests in CI:
// https://github.com/facebook/jest/issues/9430#issuecomment-1149882002
// Also anecdotally improves performance when run locally
maxWorkers: 3,
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
// Jest does not use tsconfig.spec.json by default
tsconfig: "<rootDir>/tsconfig.spec.json",
// Further workaround for memory leak, recommended here:
// https://github.com/kulshekhar/ts-jest/issues/1967#issuecomment-697494014
// Makes tests run faster and reduces size/rate of leak, but loses typechecking on test code
// See https://bitwarden.atlassian.net/browse/EC-497 for more info
isolatedModules: true,
astTransformers: {
before: ["<rootDir>/../../jslib/shared/es2020-transformer.ts"],
},
},
],
},
};

View File

@@ -0,0 +1,22 @@
import JSDOMEnvironment from "jest-environment-jsdom";
/**
* https://github.com/jsdom/jsdom/issues/3363#issuecomment-1467894943
* Adds nodes structuredClone implementation to the global object of jsdom.
* use by either adding this file to the testEnvironment property of jest config
* or by adding the following to the top spec file:
*
* ```
* /**
* * @jest-environment ../shared/test.environment.ts
* *\/
* ```
*/
export default class FixJSDOMEnvironment extends JSDOMEnvironment {
constructor(...args: ConstructorParameters<typeof JSDOMEnvironment>) {
super(...args);
// FIXME https://github.com/jsdom/jsdom/issues/3363
this.global.structuredClone = structuredClone;
}
}

View File

@@ -0,0 +1,12 @@
{
"extends": "./tsconfig",
"compilerOptions": {
"paths": {
"tldjs": ["../common/src/misc/tldjs.noop"],
"jslib-common/*": ["../common/src/*"],
"jslib-angular/*": ["../angular/src/*"],
"jslib-electron/*": ["../electron/src/*"],
"jslib-node/*": ["../node/src/*"]
}
}
}

77
package-lock.json generated
View File

@@ -96,6 +96,7 @@
"html-webpack-plugin": "5.5.3",
"husky": "8.0.3",
"jest": "29.6.4",
"jest-junit": "^16.0.0",
"jest-preset-angular": "13.1.1",
"jsdom": "22.1.0",
"lint-staged": "12.5.0",
@@ -111,6 +112,7 @@
"sass": "1.66.1",
"sass-loader": "12.6.0",
"tapable": "1.1.3",
"ts-jest": "^29.1.1",
"ts-loader": "9.4.4",
"ts-node": "10.9.1",
"tsconfig-paths": "3.14.2",
@@ -13448,6 +13450,33 @@
"fsevents": "^2.3.2"
}
},
"node_modules/jest-junit": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz",
"integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==",
"dev": true,
"dependencies": {
"mkdirp": "^1.0.4",
"strip-ansi": "^6.0.1",
"uuid": "^8.3.2",
"xml": "^1.0.1"
},
"engines": {
"node": ">=10.12.0"
}
},
"node_modules/jest-junit/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true,
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jest-leak-detector": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.3.tgz",
@@ -20089,9 +20118,9 @@
}
},
"node_modules/ts-jest": {
"version": "29.1.0",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz",
"integrity": "sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==",
"version": "29.1.1",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
"integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==",
"dev": true,
"dependencies": {
"bs-logger": "0.x",
@@ -20100,7 +20129,7 @@
"json5": "^2.2.3",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"semver": "^7.5.3",
"yargs-parser": "^21.0.1"
},
"bin": {
@@ -21474,6 +21503,12 @@
}
}
},
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
"dev": true
},
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
@@ -31483,6 +31518,26 @@
"walker": "^1.0.8"
}
},
"jest-junit": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz",
"integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==",
"dev": true,
"requires": {
"mkdirp": "^1.0.4",
"strip-ansi": "^6.0.1",
"uuid": "^8.3.2",
"xml": "^1.0.1"
},
"dependencies": {
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
}
}
},
"jest-leak-detector": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.3.tgz",
@@ -36519,9 +36574,9 @@
}
},
"ts-jest": {
"version": "29.1.0",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz",
"integrity": "sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==",
"version": "29.1.1",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
"integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==",
"dev": true,
"requires": {
"bs-logger": "0.x",
@@ -36530,7 +36585,7 @@
"json5": "^2.2.3",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"semver": "^7.5.3",
"yargs-parser": "^21.0.1"
}
},
@@ -37486,6 +37541,12 @@
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"requires": {}
},
"xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
"dev": true
},
"xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",

View File

@@ -62,9 +62,9 @@
"publish:win": "npm run build:dist && npm run clean:dist && electron-builder --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
"prettier": "prettier --write .",
"prepare": "husky install",
"test:jslib": "jest",
"test:jslib:watch": "jest --watch",
"test:jslib:watch:all": "jest --watchAll"
"test": "jest",
"test:watch": "jest --watch",
"test:watch:all": "jest --watchAll"
},
"devDependencies": {
"@angular-devkit/build-angular": "15.2.9",
@@ -78,9 +78,9 @@
"@types/ldapjs": "2.2.5",
"@types/lowdb": "1.0.11",
"@types/lunr": "2.3.4",
"@types/node": "18.17.12",
"@types/node-fetch": "2.6.4",
"@types/node-forge": "1.3.4",
"@types/node": "18.17.12",
"@types/papaparse": "5.3.8",
"@types/proper-lockfile": "4.1.2",
"@types/tldjs": "2.3.1",
@@ -93,6 +93,7 @@
"copy-webpack-plugin": "11.0.0",
"cross-env": "7.0.3",
"css-loader": "6.8.1",
"electron": "18.3.15",
"electron-builder": "24.6.3",
"electron-log": "4.4.8",
"electron-notarize": "1.2.2",
@@ -100,17 +101,17 @@
"electron-reload": "1.5.0",
"electron-store": "8.1.0",
"electron-updater": "5.3.0",
"electron": "18.3.15",
"eslint": "8.48.0",
"eslint-config-prettier": "8.10.0",
"eslint-import-resolver-typescript": "2.7.1",
"eslint-plugin-import": "2.28.1",
"eslint": "8.48.0",
"form-data": "4.0.0",
"html-loader": "3.1.2",
"html-webpack-plugin": "5.5.3",
"husky": "8.0.3",
"jest-preset-angular": "13.1.1",
"jest": "29.6.4",
"jest-junit": "^16.0.0",
"jest-preset-angular": "13.1.1",
"jsdom": "22.1.0",
"lint-staged": "12.5.0",
"mini-css-extract-plugin": "2.7.6",
@@ -122,21 +123,22 @@
"prettier": "2.8.8",
"rimraf": "3.0.2",
"rxjs": "7.8.1",
"sass-loader": "12.6.0",
"sass": "1.66.1",
"sass-loader": "12.6.0",
"tapable": "1.1.3",
"ts-jest": "^29.1.1",
"ts-loader": "9.4.4",
"ts-node": "10.9.1",
"tsconfig-paths-webpack-plugin": "4.1.0",
"tsconfig-paths": "3.14.2",
"tsconfig-paths-webpack-plugin": "4.1.0",
"ttypescript": "1.5.15",
"typemoq": "2.1.0",
"typescript-transform-paths": "2.2.4",
"typescript": "4.9.5",
"typescript-transform-paths": "2.2.4",
"webpack": "5.88.2",
"webpack-cli": "5.1.4",
"webpack-merge": "5.9.0",
"webpack-node-externals": "3.0.0",
"webpack": "5.88.2",
"zone.js": "0.13.1"
},
"dependencies": {