Merge pull request #1238 from spotify/shmidt-i/backend-hmr-2

Backend HMR
This commit is contained in:
Ivan Shmidt
2020-06-16 11:40:46 +02:00
committed by GitHub
32 changed files with 427 additions and 57 deletions
+4 -1
View File
@@ -2,7 +2,7 @@
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.1.1-alpha.8",
"main": "dist",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
"publishConfig": {
@@ -32,6 +32,7 @@
"express": "^4.17.1",
"helmet": "^3.22.0",
"morgan": "^1.10.0",
"stoppable": "^1.1.0",
"winston": "^3.2.1"
},
"devDependencies": {
@@ -41,7 +42,9 @@
"@types/express": "^4.17.6",
"@types/http-errors": "^1.6.3",
"@types/morgan": "^1.9.0",
"@types/stoppable": "^1.1.0",
"@types/supertest": "^2.0.8",
"@types/webpack-env": "^1.15.2",
"@types/yaml": "^1.9.7",
"get-port": "^5.1.1",
"http-errors": "^1.7.3",
+92
View File
@@ -0,0 +1,92 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This function allows devs to cleanup
* ongoing effects when module gets hot-reloaded
* Useful for cleaning intervals, timers, requests etc
* @example
* ```ts
* const intervalId = setInterval(doStuff, 1000);
* useHotCleanup(module, () => clearInterval(intervalId));
* ```
* @param _module Reference to the current module where you invoke the fn
* @param cancelEffect Fn that cleans up the ongoing effects
*/
export function useHotCleanup(_module: NodeModule, cancelEffect: () => void) {
if (_module.hot) {
_module.hot.addDisposeHandler(() => {
cancelEffect();
});
}
}
/**
* This function allows devs to preserve
* some value between hot-reloads.
* Useful for stateful parts of the backend
* @example
* ```ts
* const db = useHotMemoize(module, () => createDB(dbParams));
* ```
* @param _module Reference to the current module where you invoke the fn
* @param valueFactory Fn that returns the value you want to memoize
* @warning Don't use inside conditionals or loops,
* same rules as for hooks apply (https://reactjs.org/docs/hooks-rules.html)
*/
export function useHotMemoize<T>(
_module: NodeModule,
valueFactory: () => T,
): T {
const CURRENT_HOT_MEMOIZE_INDEX_KEY = 'backstage.io/hmr-memoize-key';
if (!_module.hot) {
// Just return value straight away
return valueFactory();
}
if (_module.hot && typeof _module.hot.data === 'undefined') {
// First run, init the module data
_module.hot.data = {
[CURRENT_HOT_MEMOIZE_INDEX_KEY]: 0,
};
}
// Let's store data per module based on the order of the code invocation
const index = _module.hot.data?.[CURRENT_HOT_MEMOIZE_INDEX_KEY];
// Increasing the counter after each call
_module.hot.data[CURRENT_HOT_MEMOIZE_INDEX_KEY] += 1;
const prevValue = _module.hot.data?.[index];
const createDisposeHandler = (value: any) => (data: {
[key: number]: any;
[indexKey: string]: number;
}) => {
// Preserving the value through the HMR process
data[index] = value;
// Decreasing the counter after each handler
data[CURRENT_HOT_MEMOIZE_INDEX_KEY] = index - 1;
};
if (prevValue) {
_module.hot!.addDisposeHandler(createDisposeHandler(prevValue));
return prevValue;
}
const newValue = valueFactory();
_module.hot.addDisposeHandler(createDisposeHandler(newValue));
return newValue;
}
+1
View File
@@ -18,3 +18,4 @@ export * from './errors';
export * from './logging';
export * from './middleware';
export * from './service';
export * from './hot';
@@ -19,6 +19,7 @@ import cors from 'cors';
import express, { Router } from 'express';
import helmet from 'helmet';
import { Server } from 'http';
import stoppable from 'stoppable';
import { Logger } from 'winston';
import { getRootLogger } from '../logging';
import {
@@ -27,6 +28,7 @@ import {
requestLoggingHandler,
} from '../middleware';
import { ServiceBuilder } from './types';
import { useHotCleanup } from '../hot';
const DEFAULT_PORT = 7000;
@@ -35,9 +37,14 @@ export class ServiceBuilderImpl implements ServiceBuilder {
private logger: Logger | undefined;
private corsOptions: cors.CorsOptions | undefined;
private routers: [string, Router][];
constructor() {
/**
* Reference to the module where builder is created
* Needed for the HMR
*/
private module: NodeModule;
constructor(module: NodeModule) {
this.routers = [];
this.module = module;
}
setPort(port: number): ServiceBuilder {
@@ -82,9 +89,19 @@ export class ServiceBuilderImpl implements ServiceBuilder {
logger.error(`Failed to start up on port ${port}, ${e}`);
reject(e);
});
const server = app.listen(port, () => {
logger.info(`Listening on port ${port}`);
});
const server = stoppable(
app.listen(port, () => {
logger.info(`Listening on port ${port}`);
}),
0,
);
useHotCleanup(this.module, () =>
server.stop((e: any) => {
if (e) console.error(e);
}),
);
resolve(server);
});
}
@@ -19,6 +19,6 @@ import { ServiceBuilderImpl } from './ServiceBuilderImpl';
/**
* Creates a new service builder.
*/
export function createServiceBuilder() {
return new ServiceBuilderImpl();
export function createServiceBuilder(_module: NodeModule) {
return new ServiceBuilderImpl(_module);
}
+1 -1
View File
@@ -9,6 +9,6 @@
"target": "ES2019",
"module": "commonjs",
"esModuleInterop": true,
"types": ["node", "jest"]
"types": ["node", "jest", "webpack-env"]
}
}
+1 -1
View File
@@ -10,7 +10,7 @@
},
"scripts": {
"build": "tsc",
"start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess \\\"cross-env NODE_ENV=development nodemon -r esm\\\"",
"start": "backstage-cli backend:dev",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"clean": "backstage-cli clean",
+16 -6
View File
@@ -22,7 +22,11 @@
* Happy hacking!
*/
import { createServiceBuilder, getRootLogger } from '@backstage/backend-common';
import {
createServiceBuilder,
getRootLogger,
useHotMemoize,
} from '@backstage/backend-common';
import knex from 'knex';
import auth from './plugins/auth';
import catalog from './plugins/catalog';
@@ -45,19 +49,24 @@ function createEnv(plugin: string): PluginEnvironment {
}
async function main() {
const service = createServiceBuilder()
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
const identityEnv = useHotMemoize(module, () => createEnv('identity'));
const service = createServiceBuilder(module)
.enableCors({
origin: 'http://localhost:3000',
credentials: true,
})
.addRouter('/catalog', await catalog(createEnv('catalog')))
.addRouter('/scaffolder', await scaffolder(createEnv('scaffolder')))
.addRouter('/catalog', await catalog(catalogEnv))
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
.addRouter(
'/sentry',
await sentry(getRootLogger().child({ type: 'plugin', plugin: 'sentry' })),
)
.addRouter('/auth', await auth(createEnv('auth')))
.addRouter('/identity', await identity(createEnv('identity')));
.addRouter('/auth', await auth(authEnv))
.addRouter('/identity', await identity(identityEnv));
await service.start().catch(err => {
console.log(err);
@@ -65,6 +74,7 @@ async function main() {
});
}
module.hot?.accept();
main().catch(error => {
console.error(`Backend failed to start up, ${error}`);
process.exit(1);
+5 -1
View File
@@ -24,6 +24,7 @@ import {
runPeriodically,
} from '@backstage/plugin-catalog-backend';
import { PluginEnvironment } from '../types';
import { useHotCleanup } from '@backstage/backend-common';
export default async function createPlugin({
logger,
@@ -41,7 +42,10 @@ export default async function createPlugin({
logger,
);
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000);
useHotCleanup(
module,
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000),
);
return await createRouter({
entitiesCatalog,
+1 -1
View File
@@ -10,6 +10,6 @@
"module": "commonjs",
"esModuleInterop": true,
"lib": ["es2019", "dom"],
"types": ["node", "jest"]
"types": ["node", "jest", "webpack-env"]
}
}
+1 -1
View File
@@ -32,6 +32,6 @@
"strictPropertyInitialization": true,
"stripInternal": true,
"target": "ES2019",
"types": ["node", "jest"]
"types": ["node", "jest", "webpack-env"]
}
}
+5
View File
@@ -39,6 +39,9 @@
"@rollup/plugin-node-resolve": "^7.1.1",
"@spotify/eslint-config": "^7.0.1",
"@sucrase/webpack-loader": "^2.0.0",
"@types/start-server-webpack-plugin": "^2.2.0",
"@types/webpack-node-externals": "^1.7.1",
"@types/webpack-env": "^1.15.2",
"bfj": "^7.0.2",
"chalk": "^4.0.0",
"chokidar": "^3.3.1",
@@ -72,6 +75,7 @@
"rollup-plugin-peer-deps-external": "^2.2.2",
"rollup-plugin-postcss": "^3.1.1",
"rollup-plugin-typescript2": "^0.26.0",
"start-server-webpack-plugin": "^2.2.5",
"style-loader": "^1.2.1",
"sucrase": "^3.14.1",
"tar": "^6.0.1",
@@ -81,6 +85,7 @@
"url-loader": "^4.1.0",
"webpack": "^4.41.6",
"webpack-dev-server": "^3.10.3",
"webpack-node-externals": "^1.7.2",
"yaml": "^1.10.0",
"yml-loader": "^2.1.0",
"yn": "^4.0.0"
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
import { Command } from 'commander';
import { serveBackend } from '../../lib/bundler/backend';
export default async (cmd: Command) => {
const appConfigs = await loadConfig();
const waitForExit = await serveBackend({
entry: 'src/index',
checksEnabled: cmd.check,
config: ConfigReader.fromConfigs(appConfigs),
appConfigs,
});
await waitForExit();
};
+6
View File
@@ -41,6 +41,12 @@ const main = (argv: string[]) => {
.option('--check', 'Enable type checking and linting')
.action(lazyAction(() => import('./commands/app/serve'), 'default'));
program
.command('backend:dev')
.description('Start local development server with HMR for the backend')
.option('--check', 'Enable type checking and linting')
.action(lazyAction(() => import('./commands/backend/dev'), 'default'));
program
.command('app:diff')
.option('--check', 'Fail if changes are required')
+56
View File
@@ -0,0 +1,56 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import webpack from 'webpack';
import { createBackendConfig } from './config';
import { resolveBundlingPaths } from './paths';
import { ServeOptions } from './types';
export async function serveBackend(options: ServeOptions) {
const paths = resolveBundlingPaths(options);
const config = createBackendConfig(paths, {
...options,
isDev: true,
});
const compiler = webpack(config);
const watcher = compiler.watch(
{
poll: true,
},
(err: Error) => {
if (err) {
console.error(err);
} else console.log('Build succeeded');
},
);
const waitForExit = async () => {
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.on(signal, () => {
watcher.close(() => console.log('Stopped watcher'));
// exit instead of resolve. The process is shutting down and resolving a promise here logs an error
process.exit();
});
}
// Block indefinitely and wait for the interrupt signal
return new Promise(() => {});
};
return waitForExit;
}
+105 -15
View File
@@ -14,15 +14,17 @@
* limitations under the License.
*/
import webpack from 'webpack';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
import StartServerPlugin from 'start-server-webpack-plugin';
import webpack from 'webpack';
import nodeExternals from 'webpack-node-externals';
import { optimization } from './optimization';
import { Config } from '@backstage/config';
import { BundlingPaths } from './paths';
import { transforms } from './transforms';
import { optimization } from './optimization';
import { BundlingOptions } from './types';
import { BundlingOptions, BackendBundlingOptions } from './types';
// import checkRequiredFiles from 'react-dev-utils/checkRequiredFiles';
// import ModuleNotFoundPlugin from 'react-dev-utils/ModuleNotFoundPlugin';
// import errorOverlayMiddleware from 'react-dev-utils/errorOverlayMiddleware';
@@ -93,6 +95,17 @@ export function createConfig(
return {
mode: isDev ? 'development' : 'production',
profile: false,
node: {
module: 'empty',
dgram: 'empty',
dns: 'mock',
fs: 'empty',
http2: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
optimization: optimization(options),
bail: false,
performance: {
hints: false, // we check the gzip size instead
@@ -124,17 +137,94 @@ export function createConfig(
? '[name].chunk.js'
: '[name].[chunkhash:8].chunk.js',
},
optimization: optimization(options),
plugins,
node: {
module: 'empty',
dgram: 'empty',
dns: 'mock',
fs: 'empty',
http2: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
};
}
export function createBackendConfig(
paths: BundlingPaths,
options: BackendBundlingOptions,
): webpack.Configuration {
const { checksEnabled, isDev } = options;
const { loaders } = transforms(options);
return {
mode: isDev ? 'development' : 'production',
profile: false,
...(isDev
? {
watch: true,
watchOptions: {
ignored: [/node_modules\/(?!\@backstage)/],
},
}
: {}),
externals: [
nodeExternals({
modulesDir: paths.rootNodeModules,
whitelist: ['webpack/hot/poll?100', /\@backstage\/.*/],
}),
nodeExternals({
modulesDir: paths.targetNodeModules,
whitelist: ['webpack/hot/poll?100', /\@backstage\/.*/],
}),
],
target: 'node' as const,
node: {
__dirname: true,
__filename: true,
global: true,
},
bail: false,
performance: {
hints: false, // we check the gzip size instead
},
devtool: isDev ? 'cheap-module-eval-source-map' : 'source-map',
context: paths.targetPath,
entry: ['webpack/hot/poll?100', paths.targetEntry],
resolve: {
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
mainFields: ['main:src', 'browser', 'module', 'main'],
modules: [paths.targetNodeModules, paths.rootNodeModules],
plugins: [
new ModuleScopePlugin(
[paths.targetSrc, paths.targetDev],
[paths.targetPackageJson],
),
],
alias: {
'react-dom': '@hot-loader/react-dom',
},
},
module: {
rules: loaders,
},
output: {
path: paths.targetDist,
filename: isDev ? '[name].js' : '[name].[hash:8].js',
chunkFilename: isDev
? '[name].chunk.js'
: '[name].[chunkhash:8].chunk.js',
},
plugins: [
new StartServerPlugin('main.js'),
new webpack.HotModuleReplacementPlugin(),
...(checksEnabled
? [
new ForkTsCheckerWebpackPlugin({
tsconfig: paths.targetTsConfig,
eslint: true,
eslintOptions: {
parserOptions: {
project: paths.targetTsConfig,
tsconfigRootDir: paths.targetPath,
},
},
reportFiles: ['**', '!**/__tests__/**', '!**/?(*.)(spec|test).*'],
}),
]
: []),
],
};
}
+1
View File
@@ -14,5 +14,6 @@
* limitations under the License.
*/
export { serveBackend } from './backend';
export { buildBundle } from './bundle';
export { serveBundle } from './server';
+2
View File
@@ -60,6 +60,8 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) {
targetTsConfig: paths.resolveTargetRoot('tsconfig.json'),
targetNodeModules: paths.resolveTarget('node_modules'),
targetPackageJson: paths.resolveTarget('package.json'),
rootNodeModules: paths.resolveTargetRoot('node_modules'),
root: paths.targetRoot,
};
}
+5 -1
View File
@@ -30,7 +30,11 @@ export async function serveBundle(options: ServeOptions) {
const paths = resolveBundlingPaths(options);
const pkgPath = paths.targetPackageJson;
const pkg = await fs.readJson(pkgPath);
const config = createConfig(paths, { ...options, isDev: true, baseUrl: url });
const config = createConfig(paths, {
...options,
isDev: true,
baseUrl: url,
});
const compiler = webpack(config);
const server = new WebpackDevServer(compiler, {
+4 -2
View File
@@ -16,14 +16,16 @@
import webpack, { Module, Plugin } from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { BundlingOptions } from './types';
import { BundlingOptions, BackendBundlingOptions } from './types';
type Transforms = {
loaders: Module['rules'];
plugins: Plugin[];
};
export const transforms = (options: BundlingOptions): Transforms => {
export const transforms = (
options: BundlingOptions | BackendBundlingOptions,
): Transforms => {
const { isDev } = options;
const loaders = [
+2
View File
@@ -25,6 +25,8 @@ export type BundlingOptions = {
baseUrl: URL;
};
export type BackendBundlingOptions = Omit<BundlingOptions, 'baseUrl'>;
export type ServeOptions = BundlingPathsOptions & {
checksEnabled: boolean;
config: Config;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-auth-backend",
"version": "0.1.1-alpha.8",
"main": "dist",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
+1 -1
View File
@@ -10,6 +10,6 @@
"module": "commonjs",
"esModuleInterop": true,
"lib": ["es2019"],
"types": ["node", "jest"]
"types": ["node", "jest", "webpack-env"]
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend",
"version": "0.1.1-alpha.8",
"main": "dist",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
@@ -54,7 +54,7 @@ export async function startStandaloneServer(
higherOrderOperation,
logger,
});
const service = createServiceBuilder()
const service = createServiceBuilder(module)
.enableCors({ origin: 'http://localhost:3000' })
.addRouter('/catalog', router);
return await service.start().catch(err => {
@@ -62,3 +62,5 @@ export async function startStandaloneServer(
process.exit(1);
});
}
module.hot?.accept();
+1 -1
View File
@@ -11,6 +11,6 @@
"esModuleInterop": true,
"allowJs": true,
"lib": ["es2019"],
"types": ["node", "jest"]
"types": ["node", "jest", "webpack-env"]
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-identity-backend",
"version": "0.1.1-alpha.8",
"main": "dist",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
+1 -1
View File
@@ -10,6 +10,6 @@
"module": "commonjs",
"esModuleInterop": true,
"lib": ["es2019"],
"types": ["node", "jest"]
"types": ["node", "jest", "webpack-env"]
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-scaffolder-backend",
"version": "0.1.1-alpha.8",
"main": "dist",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-sentry-backend",
"version": "0.1.1-alpha.8",
"main": "dist",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
+1 -1
View File
@@ -10,6 +10,6 @@
"module": "commonjs",
"esModuleInterop": true,
"lib": ["es2019"],
"types": ["node", "jest"]
"types": ["node", "jest", "webpack-env"]
}
}
+51 -10
View File
@@ -4020,6 +4020,20 @@
resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
"@types/start-server-webpack-plugin@^2.2.0":
version "2.2.0"
resolved "https://registry.npmjs.org/@types/start-server-webpack-plugin/-/start-server-webpack-plugin-2.2.0.tgz#a7c4595c715eda083d92ca1ea184d97db3d8fc7b"
integrity sha512-TFiZWMPuiMR/utvjk6ENi0HPtQl38HnPMYfJqm04ztpzITHzTCXt7T7LyXnP9eTwg4lLQkmRUaFy04iEyjoJmw==
dependencies:
"@types/webpack" "*"
"@types/stoppable@^1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.0.tgz#a5fa6a48120b109ca9233eed05c67c50bc4f3b91"
integrity sha512-BRR23Q9CJduH7AM6mk4JRttd8XyFkb4qIPZu4mdLF+VoP+wcjIxIWIKiBbN78NBbEuynrAyMPtzOHnIp2B/JPQ==
dependencies:
"@types/node" "*"
"@types/styled-jsx@^2.2.8":
version "2.2.8"
resolved "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234"
@@ -4143,6 +4157,18 @@
resolved "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.15.1.tgz#c8e84705e08eed430b5e15b39c65b0944e4d1422"
integrity sha512-eWN5ElDTeBc5lRDh95SqA8x18D0ll2pWudU3uWiyfsRmIZcmUXpEsxPU+7+BsdCrO2vfLRC629u/MmjbmF+2tA==
"@types/webpack-env@^1.15.2":
version "1.15.2"
resolved "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a"
integrity sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ==
"@types/webpack-node-externals@^1.7.1":
version "1.7.1"
resolved "https://registry.npmjs.org/@types/webpack-node-externals/-/webpack-node-externals-1.7.1.tgz#73d0d7ae0e98cfbd69b7443388302cd69217494a"
integrity sha512-kbO2gYPKvMb5j1KOgnIuUH52CKul9Ud4b10J5n+JX8oHmgu86hYpBVfrV4bMDe5lhCaO64h8QrKz7WnRZzqkbA==
dependencies:
"@types/webpack" "*"
"@types/webpack-sources@*":
version "0.1.6"
resolved "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.6.tgz#3d21dfc2ec0ad0c77758e79362426a9ba7d7cbcb"
@@ -9932,7 +9958,7 @@ he@^1.2.0:
resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
headers-utils@^1.1.3, headers-utils@^1.1.9:
headers-utils@^1.1.9, headers-utils@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/headers-utils/-/headers-utils-1.2.0.tgz#5e10d1bc9d2bccf789547afca5b991a3167241e8"
integrity sha512-4/BMXcWrJErw7JpM87gF8MNEXcIMLzepYZjNRv/P9ctgupl2Ywa3u1PgHtNhSRq84bHH9Ndlkdy7bSi+bZ9I9A==
@@ -13169,9 +13195,9 @@ ms@^2.0.0, ms@^2.1.1:
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
msw@^0.19.0:
version "0.19.0"
resolved "https://registry.npmjs.org/msw/-/msw-0.19.0.tgz#fd37015787d40db82d243a2853be66c466675e72"
integrity sha512-1TpmJzJ+afBWTRNJYoeW8KwLQbCVlvvhw2u/eRuIYfel+bPqcut5NaSgo+Bi4C0Q/7M5wza00w1GEuOXQu6FCA==
version "0.19.3"
resolved "https://registry.npmjs.org/msw/-/msw-0.19.3.tgz#88f39edbd37313bff15a0e7cd00c71406cbdbbae"
integrity sha512-HYLnyrCDDPP72GG/CeHPhBjHsZFYkz36rJLXDWccZWNA24gYjgrcp9iVqqitk2cI6NAJwsupRml9GkfpJBB74w==
dependencies:
"@open-draft/until" "^1.0.0"
"@types/cookie" "^0.3.3"
@@ -13180,7 +13206,7 @@ msw@^0.19.0:
graphql "^15.0.0"
headers-utils "^1.1.9"
node-match-path "^0.4.2"
node-request-interceptor "^0.2.4"
node-request-interceptor "^0.2.5"
statuses "^2.0.0"
yargs "^15.3.1"
@@ -13436,13 +13462,13 @@ node-releases@^1.1.29, node-releases@^1.1.52:
dependencies:
semver "^6.3.0"
node-request-interceptor@^0.2.4:
version "0.2.4"
resolved "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.2.4.tgz#f03a1b874823d0bea311a14280227707be946298"
integrity sha512-/htjDLmygBczT5qYPaSxfAEtMkc0LGuH6jqAP1o+TKfQh6yQfFyTtac25cpY8+pb4EawHljCLUN7dCeed9SdPA==
node-request-interceptor@^0.2.5:
version "0.2.6"
resolved "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.2.6.tgz#541278d7033bb6a8befb5dd793f83428cf6446a2"
integrity sha512-aJW1tPSM7nzuZFRe+C/KSz22GJO3CVFMxHHmMGX8Z+tjP7TCIVbzeckLFVfJG68BdVgrdOOP7Ejc57ag820eyA==
dependencies:
debug "^4.1.1"
headers-utils "^1.1.3"
headers-utils "^1.2.0"
nodemon@^2.0.2:
version "2.0.4"
@@ -17299,6 +17325,11 @@ start-server-and-test@^1.10.11:
ps-tree "1.2.0"
wait-on "4.0.0"
start-server-webpack-plugin@^2.2.5:
version "2.2.5"
resolved "https://registry.npmjs.org/start-server-webpack-plugin/-/start-server-webpack-plugin-2.2.5.tgz#4a2838759b0f36acd11b0b2f5f196f289ae29d31"
integrity sha512-DRCkciwCJoCFZ+wt3wWMkR1M2mpVhJbUKFXqhK3FWyIUKYb42NnocH5sMwqgo+nPNHupqNwK/v8lgfBbr2NKdg==
state-toggle@^1.0.0:
version "1.0.3"
resolved "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe"
@@ -17327,6 +17358,11 @@ stealthy-require@^1.1.1:
resolved "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
stoppable@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b"
integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==
store2@^2.7.1:
version "2.10.0"
resolved "https://registry.npmjs.org/store2/-/store2-2.10.0.tgz#46b82bb91878daf1b0d56dec2f1d41e54d5103cf"
@@ -19067,6 +19103,11 @@ webpack-log@^2.0.0:
ansi-colors "^3.0.0"
uuid "^3.3.2"
webpack-node-externals@^1.7.2:
version "1.7.2"
resolved "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-1.7.2.tgz#6e1ee79ac67c070402ba700ef033a9b8d52ac4e3"
integrity sha512-ajerHZ+BJKeCLviLUUmnyd5B4RavLF76uv3cs6KNuO8W+HuQaEs0y0L7o40NQxdPy5w0pcv8Ew7yPUAQG0UdCg==
webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
version "1.4.3"
resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"