Enabling Profiling and small bug-fixes

This commit is contained in:
Martin Karkowski 2022-03-18 09:06:45 +01:00
parent 2cd0ff596f
commit bb89d6b3b5
14 changed files with 337 additions and 179 deletions

View File

@ -5,8 +5,8 @@ SETLOCAL
echo Compiling Backend
REM Add the Node Options for SSL these are requrired since Node v.17
set NODE_OPTIONS=--openssl-legacy-provider
call $env:NODE_OPTIONS="--openssl-legacy-provider"
REM set NODE_OPTIONS=--openssl-legacy-provider
REM call $env:NODE_OPTIONS="--openssl-legacy-provider"
(npm run-script prettier-format) && (

View File

@ -4,4 +4,32 @@ cd "%DIR%"
if not "%1"=="am_admin" (powershell start -verb runas '%0' am_admin & exit /b)
npm link
(npm link) && (
node contribute/toLinkBrowser.js
cp ./package.json ./build/package.json
cd ./build
(npm link) && (
cd "%DIR%"
node contribute/toNodejs.js
) || (
cd "%DIR%"
node contribute/toNodejs.js
)
) || (
node contribute/toLinkBrowser.js
cp ./package.json ./build/package.json
cd ./build
(npm link) && (
cd "%DIR%"
node contribute/toNodejs.js
) || (
cd "%DIR%"
node contribute/toNodejs.js
)
)

View File

@ -1 +1 @@
1.0.21
1.0.24

View File

@ -0,0 +1,28 @@
/**
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @create date 2022-01-18 13:42:41
* @modify date 2022-03-18 08:56:20
* @desc [description]
*/
const { readFileSync, writeFileSync } = require("fs");
const version = readFileSync("./contribute/VERSION", { encoding: "utf-8" });
const package = JSON.parse(readFileSync("./package.json", { encoding: "utf-8" }));
package.description = "NoPE Runtime for the Browser";
delete package.main;
delete package.browser;
delete package.bin;
package.name = "nope-browser"
package.browser = "nope.js";
package.main = "nope.js";
package.version = version;
package.files = [
"*",
];
writeFileSync("./package.json", JSON.stringify(package, undefined, 2), { encoding: "utf-8" });

27
helpers/convert_ticks.js Normal file
View File

@ -0,0 +1,27 @@
/**
* to run the
* https://nodejs.org/en/docs/guides/simple-profiling/
*/
const listFiles = require('../dist-nodejs/helpers/fileMethods').listFiles;
const { exec } = require("child_process");
const { promisify } = require("util")
const execAsync = promisify(exec);
async function main() {
const tick_files = await listFiles("./", "-v8.log")
const promises = [];
for (const file in tick_files) {
console.log(`converting ${file}`)
promises.push(
execAsync(`node --prof-process ${file} > ${file}.txt`)
)
}
await Promise.all(promises)
}
main().catch(console.error)

View File

@ -25,6 +25,7 @@ import { generateLogfilePath, useLogFile } from "../logger/fileLogging";
import { getNopeLogger } from "../logger/getLogger";
import { LoggerLevel, LoggerLevels } from "../logger/nopeLogger";
import { setGlobalLoggerLevel } from "../logger/setGlobalLoggerLevel";
import { recordCPUProfile } from "../profiling/index.nodejs";
import {
INopeINopeConnectivityTimeOptions,
ValidDefaultSelectors,
@ -60,6 +61,8 @@ export interface RunArgs {
forceSelectors: boolean;
// The Id to use.
id: string;
// Flag to enable profiling. Defaults to false.
profile: boolean;
}
export const DEFAULT_SETTINGS: RunArgs = {
@ -78,6 +81,7 @@ export const DEFAULT_SETTINGS: RunArgs = {
forceUsingSelectors: false,
logToFile: false,
id: generateId(),
profile: false,
};
/**
@ -214,6 +218,13 @@ export async function readInArgs(
dest: "communicationLogLevel",
});
parser.add_argument("--profile", {
help: "Flag to enable Profiling",
action: "append",
nargs: "?",
dest: "profile",
});
const args: RunArgs = parser.parse_args();
if (args.params === "not-provided") {
@ -221,6 +232,7 @@ export async function readInArgs(
}
args.skipLoadingConfig = Array.isArray(args.skipLoadingConfig);
args.profile = Array.isArray(args.profile);
args.logToFile = Array.isArray(args.logToFile);
args.forceUsingSelectors = Array.isArray(args.forceUsingSelectors);
@ -243,6 +255,8 @@ export async function runNopeBackend(
const args = Object.assign(_defaultSettings, _args);
const closeCallbacks = [];
try {
// Try to read in the default config file.
opts = JSON.parse(
@ -256,7 +270,7 @@ export async function runNopeBackend(
if (args.logToFile) {
const fileName = generateLogfilePath("run");
useLogFile(fileName, 10);
closeCallbacks.push(useLogFile(fileName, 10));
}
if (LoggerLevels.includes(args.log)) {
@ -266,6 +280,11 @@ export async function runNopeBackend(
// Define a Logger
const logger = getNopeLogger("starter");
if (args.profile) {
logger.warn("Enabled Profiling.");
closeCallbacks.push(recordCPUProfile());
}
if (!Object.getOwnPropertyNames(validLayers).includes(args.channel)) {
logger.error(
"Invalid Channel. Please use the following values. " +
@ -286,6 +305,46 @@ export async function runNopeBackend(
throw error;
}
let _closing = false;
const _dispose = (reason?, p?) => {
if (_closing) {
return;
}
_closing = true;
if (reason) {
// If there is a reason
logger.error("Unhandled Rejection at: Promise", p, "reason:", reason);
logger.error(reason);
} else {
// We should close the Process:
logger.warn("received 'ctrl+c'. Shutting down the Instances");
}
// Exit the Process
const promises = [];
for (const callback of closeCallbacks) {
try {
promises.push(callback());
} catch (e) {
logger.error("During exiting, an error occourd");
logger.error(e);
}
}
// Wait for all Promises to finish.
Promise.all(promises).then(() => {
process.exit();
});
};
// Subscribe to unhandled Reactions.
process.on("unhandledRejection", (reason, p) => _dispose(reason, p));
process.on("SIGINT", () => _dispose());
process.on("SIGTERM", () => _dispose());
process.on("exit", () => {
logger.info("Completed. Goodbye");
});
// Assign the Default Setting for the Channel.
opts.params = layerDefaultParameters[args.channel];
@ -336,38 +395,34 @@ export async function runNopeBackend(
},
_args.singleton
);
// Add the Dispatcher
closeCallbacks.push(async () => {
await loader.dispatcher.dispose();
});
// If required load all Packages.
if (!args.skipLoadingConfig) {
// Try to load the Modules.
if (args.delay > 0) {
logger.info(`Waiting ${args.delay} [s] to get all information.`);
await sleep(args.delay * 1000);
}
try {
logger.info("loading Packages");
await loadPackages(loader, args.file, args.delay);
} catch (e) {
logger.error("Unable to load the Packages defined in " + args.file);
}
}
} catch (e) {
getNopeLogger("cli", "info").error("failed to load the Packages", e);
throw e;
}
// If required load all Packages.
if (!args.skipLoadingConfig) {
// Try to load the Modules.
if (args.delay > 0) {
logger.info(`Waiting ${args.delay} [s] to get all information.`);
await sleep(args.delay * 1000);
}
try {
logger.info("loading Packages");
await loadPackages(loader, args.file, args.delay);
} catch (e) {
logger.error("Unable to load the Packages defined in " + args.file);
}
}
const _dispose = () => {
// We should close the Process:
logger.warn("received 'ctrl+c'. Shutting down the Instances");
loader.dispatcher.dispose().finally(process.exit);
};
process.on("SIGINT", _dispose);
process.on("SIGTERM", _dispose);
process.on("exit", () => {
logger.info("Completed. Goodbye");
});
return loader;
}
@ -386,16 +441,6 @@ export async function run(
forcedArgs: Partial<RunArgs> = {},
quite = false
) {
// Subscribe to unhandled Reactions.
process.on("unhandledRejection", (reason, p) => {
console.log("Unhandled Rejection at: Promise", p, "reason:", reason);
console.error(reason);
// application specific logging, throwing an error, or other logic here
// Forward the error
throw reason;
});
if (!quite) {
console.log(NOPELOGO);
console.log("\n\n");

View File

@ -7,7 +7,6 @@
*/
import { SPLITCHAR } from "./objectMethods";
export const SEPARATOR = "/";
export const SINGLE_LEVEL_WILDCARD = "+";
export const MULTI_LEVEL_WILDCARD = "#";

View File

@ -10,6 +10,7 @@ import { writeFile } from "fs";
import { join } from "path";
import { createFile } from "../helpers/fileMethods";
import { replaceAll } from "../helpers/stringMethods";
import { sleep } from "../index.browser";
import { getCentralNopeLogger, getNopeLogger } from "./getLogger";
export const CURRENT_DATE = _parsableISOString();
@ -45,7 +46,10 @@ export function generateLogfilePath(name: string): string {
* @param {number} [bufferSize=0] Default Buffer-Size. If > 0 we will write the log with buffering.
* @backend **Only in Nodejs available**
*/
export function useLogFile(pathToFile = DEFAULT_FILE, bufferSize = 0): void {
export function useLogFile(
pathToFile = DEFAULT_FILE,
bufferSize = 100
): () => Promise<void> {
const logger = getCentralNopeLogger();
// Define a function, that will write the content of the Buffer to our
@ -128,20 +132,19 @@ export function useLogFile(pathToFile = DEFAULT_FILE, bufferSize = 0): void {
}
});
if (bufferSize > 0) {
const clearBufferAtEnd = function () {
consoleLogger.info("Shutdown detected! Trying to Write the Buffer");
const clearBufferAtEnd = async () => {
consoleLogger.info("Shutdown detected! Trying to Write the Buffer");
if (readyToWrite) {
// Now if the Data is ready, lets write the
// buffer to the File.
writeBufferToFile(() => process.exit(0));
} else {
setTimeout(clearBufferAtEnd, 50);
}
};
while (!readyToWrite) {
await sleep(50);
}
process.on("SIGINT", clearBufferAtEnd);
process.on("SIGTERM", clearBufferAtEnd);
}
const promise = new Promise<void>((resolve, reject) => {
writeBufferToFile(resolve);
});
await promise;
};
return clearBufferAtEnd;
}

View File

@ -0,0 +1,93 @@
"use strict";
const fs = require("fs");
const v8Profiler = require("v8-profiler-next");
import { join } from "path";
import { replaceAll } from "../helpers/stringMethods";
import { getNopeLogger } from "../index.browser";
import { createFile } from "../index.nodejs";
export const CURRENT_DATE = _parsableISOString();
export const DEFAULT_LOG_LOCATION = join(process.cwd(), "logs");
const DEFAULT_FILE = join(
DEFAULT_LOG_LOCATION,
"cpu_profile_" + CURRENT_DATE + ".cpuprofile"
);
const logger = getNopeLogger("CPU-Profiler");
function _parsableISOString(date = new Date()) {
let isoString = date.toISOString();
isoString = replaceAll(isoString, ":", "-");
isoString = replaceAll(isoString, ".", "-");
return isoString;
}
/**
* Generates a Log-File Path based on the given name with the following format:
* /logs/{name}_{date}.log
*
* @export
* @param {string} name Name of the File.
* @return {string}
* @backend **Only in Nodejs available**
*/
export function generateLogfilePath(name: string): string {
return join(
DEFAULT_LOG_LOCATION,
name + "_" + _parsableISOString() + ".cpuprofile"
);
}
export function recordCPUProfile(pathToFile = DEFAULT_FILE) {
const title = "cpu-profile";
// set generateType 1 to generate new format for cpuprofile
// to be compatible with cpuprofile parsing in vscode.
v8Profiler.setGenerateType(1);
// ex. 5 mins cpu profile
v8Profiler.startProfiling(title, true);
let stopped = false;
const stopProfiling = async () => {
if (stopped) {
return;
}
stopped = true;
const profile = v8Profiler.stopProfiling(title);
const promise = new Promise((resolve, reject) => {
profile.export(function (error, result) {
if (error) {
reject(error);
}
resolve(result);
});
});
const result: any = await promise;
// if it doesn't have the extension .cpuprofile then
// chrome's profiler tool won't like it.
// examine the profile:
// Navigate to chrome://inspect
// Click Open dedicated DevTools for Node
// Select the profiler tab
// Load your file
logger.info(
"Please open google chrome and open chrome://inspect and load the file",
pathToFile
);
await createFile(pathToFile, result);
// Clear the Profile.
profile.delete();
};
return stopProfiling;
}

View File

@ -1,9 +1,6 @@
/**
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @create date 2021-11-12 12:25:30
* @modify date 2022-01-06 09:54:44
* @desc [description]
*/
import { memoize } from "lodash";
@ -45,7 +42,9 @@ type TMatchting<O extends INopeTopic = INopeTopic> = {
};
export class PubSubSystemBase<
AD extends IEventAdditionalData = IEventAdditionalData,
AD extends IEventAdditionalData & {
pubSubUpdate?: boolean;
} = IEventAdditionalData,
I extends INopeEventEmitter<
unknown,
unknown,
@ -106,9 +105,12 @@ export class PubSubSystemBase<
subTopic: string | false;
pubTopic: string | false;
callback?: IEventCallback<unknown, AD>;
observer?: INopeObserver;
}
>();
protected _emittersToObservers = new Map<O, INopeObserver>();
protected _matched = new Map<string, TMatchting>();
protected _generateEmitterType: () => I;
@ -183,11 +185,19 @@ export class PubSubSystemBase<
// Define a callback, which will be used to forward
// the data into the system:
let observer: INopeObserver = undefined;
let callback: IEventCallback<unknown, AD> = undefined;
if (pubTopic) {
const _this = this;
callback = (content, opts) => {
// Internal Data-Update of the pub-sub-system
// we wont push the data again. Otherwise, we
// risk an recursive endloop.
if (opts.pubSubUpdate) {
return;
}
// We use this callback to forward the data into the system:
_this._pushData(pubTopic as string, content, opts as AD);
};
@ -199,6 +209,7 @@ export class PubSubSystemBase<
pubTopic,
subTopic,
callback,
observer,
});
// Update the Matching Rules.
@ -206,9 +217,12 @@ export class PubSubSystemBase<
if (callback) {
// If necessary. Add the Callback.
emitter.subscribe(callback, {
observer = emitter.subscribe(callback, {
skipCurrent: !this._sendCurrentDataOnSubscription,
});
// Now lets store our binding.
this._emittersToObservers.set(emitter as unknown as O, observer);
}
// Now, if required, add the Data to the emitter.
@ -495,7 +509,7 @@ export class PubSubSystemBase<
protected _notify(
topic: string,
options: Partial<AD>,
_exclusiveEmitter: O = null
_emitter: O = null
): void {
// Check whether a Matching exists for this
// Topic, if not add it.
@ -511,10 +525,12 @@ export class PubSubSystemBase<
_emitters,
] of referenceToMatch.dataPull.entries()) {
for (const _emitter of _emitters) {
// Get a new copy for every element.
// Get a new copy for every emitter.
const data = this._pullData(_pathToPull, null);
if (_exclusiveEmitter !== null && _emitter !== _exclusiveEmitter) {
// Only if we want to notify an exclusive emitter we
// have to continue, if our emitter isnt matched.
if (_emitter !== null && _emitter === _emitter) {
continue;
}
// Iterate through all Subscribers
@ -532,7 +548,7 @@ export class PubSubSystemBase<
// Get a new copy for every element.
const data = this._pullData(_pattern, null);
if (_exclusiveEmitter !== null && _emitter !== _exclusiveEmitter) {
if (_emitter !== null && _emitter !== _emitter) {
continue;
}
// Iterate through all Subscribers
@ -578,6 +594,9 @@ export class PubSubSystemBase<
): void {
const _options = this._updateOptions(options);
// Force the Update to be true.
_options.pubSubUpdate = true;
if (containsWildcards(path)) {
throw 'The Path contains wildcards. Please use the method "patternbasedPullData" instead';
} else if (path === "") {

View File

@ -1,9 +1,6 @@
/**
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @create date 2021-11-12 17:33:12
* @modify date 2022-01-06 08:32:34
* @desc [description]
*/
import { INopeDescriptor } from "./nopeDescriptor.interface";
@ -22,6 +19,9 @@ export interface ITopicSetContentOptions extends IEventAdditionalData {
topic: string;
}
/**
* The Topic Type.
*/
export type INopeTopic<T = any, S = T, G = T> = INopeEventEmitter<
T,
S,

30
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "nope",
"version": "1.0.17",
"version": "1.0.24",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nope",
"version": "1.0.17",
"version": "1.0.24",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
@ -33,6 +33,7 @@
"ts-morph": "^13.0.2",
"typescript-json-schema": "^0.52.0",
"uuid": "^8.3.2",
"v8-profiler-next": "^1.5.1",
"websocket-stream": "^5.5.2"
},
"bin": {
@ -6620,9 +6621,7 @@
"node_modules/nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"dev": true,
"optional": true
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ=="
},
"node_modules/nanomatch": {
"version": "1.2.13",
@ -17240,6 +17239,15 @@
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"dev": true
},
"node_modules/v8-profiler-next": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/v8-profiler-next/-/v8-profiler-next-1.5.1.tgz",
"integrity": "sha512-7gnfJ3x7zN3gzmVs69OvJBNze5dLCIeY2Necy+IzomWmprSCRsBbCn5GfwAJHkWJxzoex3gyRexxvYGlgR93yg==",
"hasInstallScript": true,
"dependencies": {
"nan": "^2.14.1"
}
},
"node_modules/validate-npm-package-name": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz",
@ -23506,9 +23514,7 @@
"nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"dev": true,
"optional": true
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ=="
},
"nanomatch": {
"version": "1.2.13",
@ -31533,6 +31539,14 @@
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"dev": true
},
"v8-profiler-next": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/v8-profiler-next/-/v8-profiler-next-1.5.1.tgz",
"integrity": "sha512-7gnfJ3x7zN3gzmVs69OvJBNze5dLCIeY2Necy+IzomWmprSCRsBbCn5GfwAJHkWJxzoex3gyRexxvYGlgR93yg==",
"requires": {
"nan": "^2.14.1"
}
},
"validate-npm-package-name": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "nope",
"version": "1.0.21",
"version": "1.0.24",
"description": "NoPE Runtime for Nodejs. For Browser-Support please use nope-browser",
"files": [
"dist-nodejs/**/*",
@ -63,6 +63,7 @@
"ts-morph": "^13.0.2",
"typescript-json-schema": "^0.52.0",
"uuid": "^8.3.2",
"v8-profiler-next": "^1.5.1",
"websocket-stream": "^5.5.2"
},
"devDependencies": {

View File

@ -1,99 +0,0 @@
{
"name": "nope",
"version": "1.0.3",
"description": "NoPE Runtime for Nodejs. For Browser-Support please use nope-browser",
"browser": "build/nope.js",
"main": "dist-nodejs/index.nodejs.js",
"files": [
"build/**/*",
"dist-nodejs/**/*",
"lib/**/*",
"bin/*"
],
"bin": {
"nope-js": "./bin/nope"
},
"scripts": {
"test": "mocha",
"compile-nodejs": "tsc -p ./tsconfig.json",
"compile": "tsc -p ./tsconfig.browser.json",
"build": "npx webpack -c webpack-typescript.config.js",
"doc": "npx jsdoc ./dist/**/* -d docs",
"dev": "NODE_OPTIONS='--inspect' next dev",
"start": "node ./dist/lib/cli/nope.js",
"prettier-format": "run-script-os",
"prettier-format:win32": "prettier \"./lib/**/*.ts\" --write",
"prettier-format:darwin:linux": "prettier 'lib/**/*.ts' --write",
"prettier-format:default": "prettier 'lib/**/*.ts' --write",
"prettier-watch": "run-script-os",
"prettier-watch:win32": "onchange \"lib/**/*.ts\" -- prettier --write {{changed}}",
"prettier-watch:darwin:linux": "onchange 'lib/**/*.ts' -- prettier --write {{changed}}",
"prettier-watch:default": "onchange 'lib/**/*.ts' -- prettier --write {{changed}}"
},
"mocha": {
"reporter": "spec",
"spec": "dist/**/*.spec.js"
},
"repository": {
"type": "git",
"url": "git+https://git.zema.de/tfs/ZISS/_git/nope-js"
},
"keywords": [],
"author": "Martin Karkowski",
"license": "MIT",
"bugs": {
"url": "https://git.zema.de/tfs/ZISS/_git/nope-js/issues"
},
"homepage": "https://git.zema.de/tfs/ZISS/_git/nope-js#readme",
"dependencies": {
"async": "^3.2.2",
"comment-parser": "^1.3.0",
"cors": "^2.8.5",
"handlebars": "^4.7.7",
"inquirer": "^8.2.0",
"inquirer-fuzzy-path": "^2.3.0",
"inquirer-search-list": "^1.2.6",
"inversify": "^6.0.1",
"js-logger": "^1.6.1",
"lodash": "^4.17.21",
"mathjs": "^10.0.2",
"mqtt": "^4.3.4",
"mqtt-pattern": "^1.2.0",
"next": "^12.0.7",
"npm": "^8.3.0",
"npx": "^10.2.2",
"reflect-metadata": "^0.1.13",
"run-script-os": "^1.1.6",
"rxjs": "^7.5.1",
"socket.io": "^4.4.1",
"socket.io-client": "^4.4.1",
"ts-morph": "^13.0.2",
"typescript-json-schema": "^0.52.0",
"uuid": "^8.3.2",
"websocket-stream": "^5.5.2"
},
"devDependencies": {
"@babel/preset-typescript": "^7.16.7",
"@types/async": "^3.2.12",
"@types/chai": "^4.3.0",
"@types/lodash": "^4.14.178",
"@types/node": "^17.0.8",
"@types/socket.io": "^3.0.1",
"@types/socket.io-client": "^1.4.36",
"chai": "^4.3.4",
"dts-bundle": "^0.7.3",
"dts-bundle-webpack": "^1.0.2",
"eslint": "^8.6.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-prettier": "^4.0.0",
"mocha": "^9.1.3",
"npm-check-updates": "^12.1.0",
"onchange": "^7.1.0",
"prettier": "2.5.1",
"typedoc": "^0.22.10",
"typescript": "^4.5.4",
"webpack": "^4.46.0",
"webpack-cli": "^4.8.0"
}
}