Merge pull request #29490 from backstage/rugvip/rspack

cli: switch to using Rspack by default
This commit is contained in:
Patrik Oldsberg
2025-08-11 16:06:48 +02:00
committed by GitHub
19 changed files with 417 additions and 227 deletions
+23
View File
@@ -0,0 +1,23 @@
---
'@backstage/cli': minor
---
**BREAKING**: The new app build based on [Rspack](https://rspack.dev/) is now the default, and the `EXPERIMENTAL_RSPACK` flag has been removed. To revert to the old behavior, set the `LEGACY_WEBPACK_BUILD` environment flag and install the following optional dependencies:
```json
{
"dependencies": {
"@module-federation/enhanced": "^0.9.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"esbuild-loader": "^4.0.0",
"eslint-webpack-plugin": "^4.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.0",
"mini-css-extract-plugin": "^2.4.2",
"terser-webpack-plugin": "^5.1.3",
"webpack": "^5.96.0",
"webpack-dev-server": "^5.0.0"
}
}
```
If you do encounter a blocking issue that forces you to use the old WebPack build, please [open an issue](https://github.com/backstage/backstage/issues) explaining the problem. The WebPack build will be removed in a future release.
+3 -29
View File
@@ -35,7 +35,7 @@ test('Should not throw `ResizeObserver loop completed with undelivered notificat
).not.toBeVisible();
});
test('Should resize widgets vertically and horizontally', async ({ page }) => {
test('Should render some home page widgets', async ({ page }) => {
await page.goto('/');
const enterButton = page.getByRole('button', { name: 'Enter' });
@@ -43,32 +43,6 @@ test('Should resize widgets vertically and horizontally', async ({ page }) => {
await enterButton.click();
await page.goto('/home');
await expect(page.getByText('Backstage Example App')).toBeVisible();
// Start editing mode
await page.getByRole('button', { name: /Edit/ }).click();
await expect(page.getByRole('button', { name: /Save/ })).toBeVisible();
// Resize the last installed widget
const widgetElement = await page.locator('.react-grid-item:nth-child(3)');
const defaultWidgetBox = { x: 1, y: 1, width: 1, height: 1 };
const widgetBoxBefore =
(await widgetElement.boundingBox()) ?? defaultWidgetBox;
const widgetResizeHandle = await widgetElement.locator(
'.react-resizable-handle',
);
await widgetResizeHandle.hover();
await page.mouse.down();
await page.mouse.move(widgetBoxBefore.width / 2, widgetBoxBefore.height / 2);
await page.mouse.up();
const widgetBoxAfter =
(await widgetElement.boundingBox()) ?? defaultWidgetBox;
// Ensure that both height and width was reduced
expect(widgetBoxAfter.width).toBeLessThan(widgetBoxBefore.width);
expect(widgetBoxAfter.height).toBeLessThan(widgetBoxBefore.height);
// Exit editing mode
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('button', { name: /Edit/ })).toBeVisible();
await expect(page.getByText('Top Visited')).toBeVisible();
await expect(page.getByText('Recently Visited')).toBeVisible();
});
+44 -19
View File
@@ -63,15 +63,16 @@
"@octokit/graphql-schema": "^13.7.0",
"@octokit/oauth-app": "^4.2.0",
"@octokit/request": "^8.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"@rollup/plugin-commonjs": "^26.0.0",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-yaml": "^4.0.0",
"@rspack/core": "^1.4.11",
"@rspack/dev-server": "^1.1.4",
"@rspack/plugin-react-refresh": "^1.4.3",
"@spotify/eslint-config-base": "^15.0.0",
"@spotify/eslint-config-react": "^15.0.0",
"@spotify/eslint-config-typescript": "^15.0.0",
"@sucrase/webpack-loader": "^2.0.0",
"@swc/core": "^1.3.46",
"@swc/helpers": "^0.5.0",
"@swc/jest": "^0.2.22",
@@ -91,7 +92,6 @@
"css-loader": "^6.5.1",
"ctrlc-windows": "^2.1.0",
"esbuild": "^0.25.0",
"esbuild-loader": "^4.0.0",
"eslint": "^8.6.0",
"eslint-config-prettier": "^9.0.0",
"eslint-formatter-friendly": "^7.0.0",
@@ -102,9 +102,8 @@
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-unused-imports": "^4.1.4",
"eslint-webpack-plugin": "^4.2.0",
"eslint-rspack-plugin": "^4.2.1",
"express": "^4.17.1",
"fork-ts-checker-webpack-plugin": "^9.0.0",
"fs-extra": "^11.2.0",
"git-url-parse": "^15.0.0",
"glob": "^7.1.7",
@@ -120,7 +119,6 @@
"jest-runtime": "^29.0.2",
"json-schema": "^0.4.0",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.4.2",
"minimatch": "^9.0.0",
"node-stdlib-browser": "^1.3.1",
"npm-packlist": "^5.0.0",
@@ -144,12 +142,10 @@
"sucrase": "^3.20.2",
"swc-loader": "^0.2.3",
"tar": "^6.1.12",
"terser-webpack-plugin": "^5.1.3",
"ts-checker-rspack-plugin": "^1.1.5",
"ts-morph": "^24.0.0",
"undici": "^7.2.3",
"util": "^0.12.3",
"webpack": "^5.94.0",
"webpack-dev-server": "^5.0.0",
"yaml": "^2.0.0",
"yargs": "^16.2.0",
"yml-loader": "^2.1.0",
@@ -174,9 +170,7 @@
"@backstage/plugin-scaffolder-node-test-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@backstage/theme": "workspace:^",
"@rspack/core": "^1.3.9",
"@rspack/dev-server": "^1.1.1",
"@rspack/plugin-react-refresh": "^1.4.2",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"@types/cross-spawn": "^6.0.2",
"@types/ejs": "^3.1.3",
"@types/express": "^4.17.6",
@@ -194,22 +188,53 @@
"@types/webpack-sources": "^3.2.3",
"@types/yarnpkg__lockfile": "^1.1.4",
"del": "^8.0.0",
"esbuild-loader": "^4.0.0",
"eslint-webpack-plugin": "^4.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.0",
"mini-css-extract-plugin": "^2.4.2",
"msw": "^1.0.0",
"nodemon": "^3.0.1"
"nodemon": "^3.0.1",
"terser-webpack-plugin": "^5.1.3",
"webpack": "~5.96.0",
"webpack-dev-server": "^5.0.0"
},
"peerDependencies": {
"@rspack/core": "^1.2.8",
"@rspack/dev-server": "^1.0.9",
"@rspack/plugin-react-refresh": "^1.0.0"
"@module-federation/enhanced": "^0.9.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"esbuild-loader": "^4.0.0",
"eslint-webpack-plugin": "^4.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.0",
"mini-css-extract-plugin": "^2.4.2",
"terser-webpack-plugin": "^5.1.3",
"webpack": "~5.96.0",
"webpack-dev-server": "^5.0.0"
},
"peerDependenciesMeta": {
"@rspack/core": {
"@module-federation/enhanced": {
"optional": true
},
"@rspack/dev-server": {
"@pmmmwh/react-refresh-webpack-plugin": {
"optional": true
},
"@rspack/plugin-react-refresh": {
"esbuild-loader": {
"optional": true
},
"eslint-webpack-plugin": {
"optional": true
},
"fork-ts-checker-webpack-plugin": {
"optional": true
},
"mini-css-extract-plugin": {
"optional": true
},
"terser-webpack-plugin": {
"optional": true
},
"webpack": {
"optional": true
},
"webpack-dev-server": {
"optional": true
}
},
@@ -25,8 +25,8 @@ import { isValidUrl } from '../../../lib/urls';
import chalk from 'chalk';
export async function command(opts: OptionValues): Promise<void> {
const rspack = process.env.EXPERIMENTAL_RSPACK
? (require('@rspack/core') as typeof import('@rspack/core').rspack)
const webpack = process.env.LEGACY_WEBPACK_BUILD
? (require('webpack') as typeof import('webpack'))
: undefined;
const role = await findRoleFromCommand(opts);
@@ -44,7 +44,7 @@ export async function command(opts: OptionValues): Promise<void> {
targetDir: paths.targetDir,
configPaths,
writeStats: Boolean(opts.stats),
rspack,
webpack,
});
}
return buildBackend({
@@ -67,7 +67,7 @@ export async function command(opts: OptionValues): Promise<void> {
configPaths: [],
writeStats: Boolean(opts.stats),
isModuleFederationRemote: true,
rspack,
webpack,
});
}
@@ -31,10 +31,9 @@ import { createScriptOptionsParser } from '../../../../lib/optionsParser';
export async function command(opts: OptionValues, cmd: Command): Promise<void> {
let packages = await PackageGraph.listTargetPackages();
const shouldUseRspack = Boolean(process.env.EXPERIMENTAL_RSPACK);
const rspack = shouldUseRspack
? (require('@rspack/core') as typeof import('@rspack/core').rspack)
const webpack = process.env.LEGACY_WEBPACK_BUILD
? (require('webpack') as typeof import('webpack'))
: undefined;
if (opts.since) {
@@ -116,7 +115,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
targetDir: pkg.dir,
configPaths: (buildOptions.config as string[]) ?? [],
writeStats: Boolean(buildOptions.stats),
rspack,
webpack,
});
},
});
@@ -26,11 +26,11 @@ interface BuildAppOptions {
writeStats: boolean;
configPaths: string[];
isModuleFederationRemote?: true;
rspack?: typeof import('@rspack/core').rspack;
webpack?: typeof import('webpack');
}
export async function buildFrontend(options: BuildAppOptions) {
const { targetDir, writeStats, configPaths, rspack } = options;
const { targetDir, writeStats, configPaths, webpack } = options;
const packageJson = (await fs.readJson(
resolvePath(targetDir, 'package.json'),
)) as BackstagePackageJson;
@@ -48,6 +48,6 @@ export async function buildFrontend(options: BuildAppOptions) {
args: configPaths,
fromPackage: packageJson.name,
})),
rspack,
webpack,
});
}
@@ -17,7 +17,7 @@
import yn from 'yn';
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import webpack from 'webpack';
import { rspack, Configuration, MultiStats } from '@rspack/core';
import {
measureFileSizesBeforeBuild,
printFileSizesAfterBuild,
@@ -38,7 +38,7 @@ function applyContextToError(error: string, moduleName: string): string {
}
export async function buildBundle(options: BuildOptions) {
const { statsJsonEnabled, schema: configSchema, rspack } = options;
const { statsJsonEnabled, schema: configSchema, webpack } = options;
const paths = resolveBundlingPaths(options);
const publicPaths = await resolveOptionalBundlingPaths({
@@ -54,7 +54,7 @@ export async function buildBundle(options: BuildOptions) {
getFrontendAppConfigs: () => options.frontendAppConfigs,
};
const configs: webpack.Configuration[] = [];
const configs: Configuration[] = [];
if (options.moduleFederation?.mode === 'remote') {
// Package detection is disabled for remote bundles
@@ -119,13 +119,11 @@ export async function buildBundle(options: BuildOptions) {
);
}
if (rspack) {
console.log(
chalk.yellow(`⚠️ WARNING: Using experimental RSPack bundler.`),
);
if (webpack) {
console.log(chalk.yellow(`⚠️ WARNING: Using legacy WebPack bundler`));
}
const { stats } = await build(configs, isCi, rspack);
const { stats } = await build(configs, isCi, webpack);
if (!stats) {
throw new Error('No stats returned');
@@ -159,34 +157,32 @@ export async function buildBundle(options: BuildOptions) {
}
async function build(
configs: webpack.Configuration[],
configs: Configuration[],
isCi: boolean,
rspack?: typeof import('@rspack/core').rspack,
webpack?: typeof import('webpack'),
) {
const bundler = (rspack ?? webpack) as typeof webpack;
const bundler = (webpack ?? rspack) as typeof rspack;
const stats = await new Promise<webpack.MultiStats | undefined>(
(resolve, reject) => {
bundler(configs, (err, buildStats) => {
if (err) {
if (err.message) {
const { errors } = formatWebpackMessages({
errors: [err.message],
warnings: new Array<string>(),
_showErrors: true,
_showWarnings: true,
});
const stats = await new Promise<MultiStats | undefined>((resolve, reject) => {
bundler(configs, (err, buildStats) => {
if (err) {
if (err.message) {
const { errors } = formatWebpackMessages({
errors: [err.message],
warnings: new Array<string>(),
_showErrors: true,
_showWarnings: true,
});
throw new Error(errors[0]);
} else {
reject(err);
}
throw new Error(errors[0]);
} else {
resolve(buildStats);
reject(err);
}
});
},
);
} else {
resolve(buildStats);
}
});
});
if (!stats) {
throw new Error('Failed to compile: No stats provided');
@@ -16,16 +16,15 @@
import { BundlingOptions, ModuleFederationOptions } from './types';
import { resolve as resolvePath } from 'path';
import webpack from 'webpack';
import { rspack, Configuration } from '@rspack/core';
import { BundlingPaths } from './paths';
import { Config } from '@backstage/config';
import ESLintPlugin from 'eslint-webpack-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import ESLintRspackPlugin from 'eslint-rspack-plugin';
import { TsCheckerRspackPlugin } from 'ts-checker-rspack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack';
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
import ReactRefreshPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
import { paths as cliPaths } from '../../../../lib/paths';
import fs from 'fs-extra';
import { optimization as optimizationConfig } from './optimization';
@@ -113,14 +112,14 @@ async function readBuildInfo() {
export async function createConfig(
paths: BundlingPaths,
options: BundlingOptions,
): Promise<webpack.Configuration> {
): Promise<Configuration> {
const {
checksEnabled,
isDev,
frontendConfig,
moduleFederation,
publicSubPath = '',
rspack,
webpack,
} = options;
const { plugins, loaders } = transforms(options);
@@ -147,27 +146,35 @@ export async function createConfig(
},
} as const;
if (rspack) {
if (webpack) {
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
plugins.push(new ReactRefreshPlugin(refreshOptions));
} else {
const RspackReactRefreshPlugin = require('@rspack/plugin-react-refresh');
plugins.push(new RspackReactRefreshPlugin(refreshOptions));
} else {
plugins.push(new ReactRefreshPlugin(refreshOptions));
}
}
if (checksEnabled) {
const TsCheckerPlugin = webpack
? (require('fork-ts-checker-webpack-plugin') as typeof import('fork-ts-checker-webpack-plugin'))
: TsCheckerRspackPlugin;
const ESLintPlugin = webpack
? (require('eslint-webpack-plugin') as typeof import('eslint-webpack-plugin'))
: ESLintRspackPlugin;
plugins.push(
new ForkTsCheckerWebpackPlugin({
typescript: { configFile: paths.targetTsConfig, memoryLimit: 4096 },
new TsCheckerPlugin({
typescript: { configFile: paths.targetTsConfig, memoryLimit: 8192 },
}),
new ESLintPlugin({
cache: false, // Cache seems broken
context: paths.targetPath,
files: ['**/*.(ts|tsx|mts|cts|js|jsx|mjs|cjs)'],
}),
);
}
const bundler = rspack ? (rspack as unknown as typeof webpack) : webpack;
const bundler = webpack ? (webpack as unknown as typeof rspack) : rspack;
// TODO(blam): process is no longer auto polyfilled by webpack in v5.
// we use the provide plugin to provide this polyfill, but lets look
@@ -190,7 +197,11 @@ export async function createConfig(
config: frontendConfig,
},
};
if (rspack) {
if (webpack) {
// Config injection via index.html doesn't work across reloads with
// WebPack, so we rely on the APP_CONFIG injection instead
plugins.push(new HtmlWebpackPlugin(templateOptions));
} else {
// With Rspack we inject config via index.html, this is both because we
// can't use APP_CONFIG due to the lack of support for runtime values, but
// also because we are able to do it and it lines up better with what the
@@ -204,10 +215,6 @@ export async function createConfig(
options.getFrontendAppConfigs,
),
);
} else {
// Config injection via index.html doesn't work across reloads with
// WebPack, so we rely on the APP_CONFIG injection instead
plugins.push(new HtmlWebpackPlugin(templateOptions));
}
plugins.push(
new HtmlWebpackPlugin({
@@ -227,9 +234,9 @@ export async function createConfig(
if (options.moduleFederation) {
const isRemote = options.moduleFederation?.mode === 'remote';
const AdaptedModuleFederationPlugin = rspack
? (require('@module-federation/enhanced/rspack')
.ModuleFederationPlugin as typeof ModuleFederationPlugin)
const AdaptedModuleFederationPlugin = webpack
? (require('@module-federation/enhanced/webpack')
.ModuleFederationPlugin as unknown as typeof ModuleFederationPlugin)
: ModuleFederationPlugin;
const exposes = options.moduleFederation?.exposes
@@ -304,18 +311,28 @@ export async function createConfig(
const buildInfo = await readBuildInfo();
plugins.push(
new bundler.DefinePlugin({
'process.env.BUILD_INFO': JSON.stringify(buildInfo),
'process.env.APP_CONFIG': rspack
? JSON.stringify([]) // Inject via index.html instead
: bundler.DefinePlugin.runtimeValue(
webpack
? new webpack.DefinePlugin({
'process.env.BUILD_INFO': JSON.stringify(buildInfo),
'process.env.APP_CONFIG': webpack.DefinePlugin.runtimeValue(
() => JSON.stringify(options.getFrontendAppConfigs()),
true,
),
// This allows for conditional imports of react-dom/client, since there's no way
// to check for presence of it in source code without module resolution errors.
'process.env.HAS_REACT_DOM_CLIENT': JSON.stringify(hasReactDomClient()),
}),
// This allows for conditional imports of react-dom/client, since there's no way
// to check for presence of it in source code without module resolution errors.
'process.env.HAS_REACT_DOM_CLIENT': JSON.stringify(
hasReactDomClient(),
),
})
: new bundler.DefinePlugin({
'process.env.BUILD_INFO': JSON.stringify(buildInfo),
'process.env.APP_CONFIG': JSON.stringify([]), // Inject via index.html instead
// This allows for conditional imports of react-dom/client, since there's no way
// to check for presence of it in source code without module resolution errors.
'process.env.HAS_REACT_DOM_CLIENT': JSON.stringify(
hasReactDomClient(),
),
}),
);
if (options.linkedWorkspace) {
@@ -330,9 +347,8 @@ export async function createConfig(
// These files are required by the transpiled code when using React Refresh.
// They need to be excluded to the module scope plugin which ensures that files
// that exist in the package are required.
const reactRefreshFiles = rspack
? []
: [
const reactRefreshFiles = webpack
? [
require.resolve(
'@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js',
),
@@ -340,7 +356,8 @@ export async function createConfig(
'@pmmmwh/react-refresh-webpack-plugin/overlay/index.js',
),
require.resolve('react-refresh'),
];
]
: [];
const mode = isDev ? 'development' : 'production';
const optimization = optimizationConfig(options);
@@ -388,7 +405,7 @@ export async function createConfig(
util: require.resolve('util/'),
},
// FIXME: see also https://github.com/web-infra-dev/rspack/issues/3408
...(!rspack && {
...(webpack && {
plugins: [
new ModuleScopePlugin(
[paths.targetSrc, paths.targetDev],
@@ -421,7 +438,7 @@ export async function createConfig(
},
experiments: {
lazyCompilation: yn(process.env.EXPERIMENTAL_LAZY_COMPILATION),
...(rspack && {
...(!webpack && {
// We're still using `style-loader` for custom `insert` option
css: false,
}),
@@ -16,7 +16,7 @@
import { relative as relativePath } from 'path';
import { getPackages } from '@manypkg/get-packages';
import webpack from 'webpack';
import { rspack } from '@rspack/core';
import { paths } from '../../../../lib/paths';
/**
@@ -26,7 +26,7 @@ import { paths } from '../../../../lib/paths';
* of react and react-dom which are always resolved from the target workspace.
*/
export async function createWorkspaceLinkingPlugins(
bundler: typeof webpack,
bundler: typeof rspack,
workspace: string,
) {
const { packages: linkedPackages, root: linkedRoot } = await getPackages(
@@ -14,19 +14,21 @@
* limitations under the License.
*/
import { WebpackOptionsNormalized } from 'webpack';
import { BundlingOptions } from './types';
const { EsbuildPlugin } = require('esbuild-loader');
import {
SwcJsMinimizerRspackPlugin,
LightningCssMinimizerRspackPlugin,
RspackOptionsNormalized,
} from '@rspack/core';
export const optimization = (
options: BundlingOptions,
): WebpackOptionsNormalized['optimization'] => {
const { isDev, rspack } = options;
): RspackOptionsNormalized['optimization'] => {
const { isDev, webpack } = options;
const MinifyPlugin = rspack
? rspack.SwcJsMinimizerRspackPlugin
: EsbuildPlugin;
const MinifyPlugin = webpack
? require('esbuild-loader').EsbuildPlugin
: SwcJsMinimizerRspackPlugin;
return {
minimize: !isDev,
@@ -42,7 +44,7 @@ export const optimization = (
format: undefined,
include: 'remoteEntry.js',
}),
rspack && new rspack.LightningCssMinimizerRspackPlugin(),
webpack ? undefined : new LightningCssMinimizerRspackPlugin(),
],
runtimeChunk: 'single',
splitChunks: {
@@ -74,7 +76,7 @@ export const optimization = (
priority: 10,
minSize: 100000,
minChunks: 1,
...(!rspack && {
...(webpack && {
maxAsyncRequests: Infinity,
maxInitialRequests: Infinity,
}),
@@ -19,8 +19,8 @@ import chalk from 'chalk';
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import openBrowser from 'react-dev-utils/openBrowser';
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import { rspack } from '@rspack/core';
import { RspackDevServer } from '@rspack/dev-server';
import { paths as libPaths } from '../../../../lib/paths';
import { loadCliConfig } from '../../../config/lib/config';
@@ -55,20 +55,20 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
resolvePath(options.targetDir ?? libPaths.targetDir, 'package.json'),
);
let webpackServer: WebpackDevServer | undefined = undefined;
let devServer: RspackDevServer | undefined = undefined;
let latestFrontendAppConfigs: AppConfig[] = [];
/** Triggers a full reload of all clients */
const triggerReload = () => {
if (webpackServer) {
webpackServer.invalidate();
if (devServer) {
devServer.invalidate();
// For the Rspack server it's not enough to invalidate, we also need to
// tell the browser to reload, which we do with a 'static-changed' message
if (process.env.EXPERIMENTAL_RSPACK) {
webpackServer.sendMessage(
webpackServer.webSocketServer?.clients ?? [],
if (!process.env.LEGACY_WEBPACK_BUILD) {
devServer.sendMessage(
devServer.webSocketServer?.clients ?? [],
'static-changed',
);
}
@@ -122,8 +122,8 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
},
});
const rspack = process.env.EXPERIMENTAL_RSPACK
? (require('@rspack/core') as typeof import('@rspack/core').rspack)
const webpack = process.env.LEGACY_WEBPACK_BUILD
? (require('webpack') as typeof import('webpack'))
: undefined;
const commonConfigOptions = {
@@ -132,7 +132,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
isDev: true,
baseUrl: url,
frontendConfig,
rspack,
webpack,
getFrontendAppConfigs: () => {
return latestFrontendAppConfigs;
},
@@ -144,15 +144,13 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
moduleFederation: options.moduleFederation,
});
const bundler = (rspack ?? webpack) as typeof webpack;
const DevServer: typeof WebpackDevServer = rspack
? require('@rspack/dev-server').RspackDevServer
: WebpackDevServer;
const bundler = (webpack ?? rspack) as typeof rspack;
const DevServer: typeof RspackDevServer = webpack
? require('webpack-dev-server')
: RspackDevServer;
if (rspack) {
console.log(
chalk.yellow(`⚠️ WARNING: Using experimental RSPack dev server.`),
);
if (webpack) {
console.log(chalk.yellow(`⚠️ WARNING: Using legacy WebPack dev server.`));
}
const publicPaths = await resolveOptionalBundlingPaths({
@@ -170,7 +168,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
? bundler([config, await createConfig(publicPaths, commonConfigOptions)])
: bundler(config);
webpackServer = new DevServer(
devServer = new DevServer(
{
hot: !process.env.CI,
devMiddleware: {
@@ -225,8 +223,8 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
);
await new Promise<void>(async (resolve, reject) => {
if (webpackServer) {
webpackServer.startCallback((err?: Error) => {
if (devServer) {
devServer.startCallback((err?: Error) => {
if (err) {
reject(err);
return;
@@ -245,7 +243,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
const waitForExit = async () => {
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.on(signal, () => {
webpackServer?.stop();
devServer?.stop();
// exit instead of resolve. The process is shutting down and resolving a promise here logs an error
process.exit();
});
@@ -14,26 +14,30 @@
* limitations under the License.
*/
import { RuleSetRule, WebpackPluginInstance } from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import {
RuleSetRule,
RspackPluginInstance,
CssExtractRspackPlugin,
WebpackPluginInstance,
} from '@rspack/core';
type Transforms = {
loaders: RuleSetRule[];
plugins: WebpackPluginInstance[];
plugins: Array<RspackPluginInstance | WebpackPluginInstance>;
};
type TransformOptions = {
isDev: boolean;
isBackend?: boolean;
rspack?: typeof import('@rspack/core').rspack;
webpack?: typeof import('webpack').webpack;
};
export const transforms = (options: TransformOptions): Transforms => {
const { isDev, isBackend, rspack } = options;
const { isDev, isBackend, webpack } = options;
const CssExtractRspackPlugin: typeof MiniCssExtractPlugin = rspack
? (rspack.CssExtractRspackPlugin as unknown as typeof MiniCssExtractPlugin)
: MiniCssExtractPlugin;
const CssExtractPlugin: typeof CssExtractRspackPlugin = webpack
? (require('mini-css-extract-plugin') as unknown as typeof CssExtractRspackPlugin)
: CssExtractRspackPlugin;
// This ensures that styles inserted from the style-loader and any
// async style chunks are always given lower priority than JSS styles.
@@ -58,7 +62,9 @@ export const transforms = (options: TransformOptions): Transforms => {
exclude: /node_modules/,
use: [
{
loader: rspack ? 'builtin:swc-loader' : require.resolve('swc-loader'),
loader: webpack
? require.resolve('swc-loader')
: 'builtin:swc-loader',
options: {
jsc: {
target: 'es2022',
@@ -86,7 +92,9 @@ export const transforms = (options: TransformOptions): Transforms => {
exclude: /node_modules/,
use: [
{
loader: rspack ? 'builtin:swc-loader' : require.resolve('swc-loader'),
loader: webpack
? require.resolve('swc-loader')
: 'builtin:swc-loader',
options: {
jsc: {
target: 'es2022',
@@ -161,7 +169,7 @@ export const transforms = (options: TransformOptions): Transforms => {
insert: insertBeforeJssStyles,
},
}
: CssExtractRspackPlugin.loader,
: CssExtractPlugin.loader,
{
loader: require.resolve('css-loader'),
options: {
@@ -172,11 +180,11 @@ export const transforms = (options: TransformOptions): Transforms => {
},
];
const plugins = new Array<WebpackPluginInstance>();
const plugins = new Array<RspackPluginInstance | WebpackPluginInstance>();
if (!isDev) {
plugins.push(
new CssExtractRspackPlugin({
new CssExtractPlugin({
filename: 'static/[name].[contenthash:8].css',
chunkFilename: 'static/[name].[id].[contenthash:8].css',
insert: insertBeforeJssStyles, // Only applies to async chunks
@@ -45,7 +45,7 @@ export type BundlingOptions = {
// An external linked workspace to include in the bundling
linkedWorkspace?: string;
moduleFederation?: ModuleFederationOptions;
rspack?: typeof import('@rspack/core').rspack;
webpack?: typeof import('webpack');
};
export type ServeOptions = BundlingPathsOptions & {
@@ -69,7 +69,7 @@ export type BuildOptions = BundlingPathsOptions & {
frontendAppConfigs: AppConfig[];
fullConfig: Config;
moduleFederation?: ModuleFederationOptions;
rspack?: typeof import('@rspack/core').rspack;
webpack?: typeof import('webpack');
};
export type BackendBundlingOptions = {
@@ -79,5 +79,5 @@ export type BackendBundlingOptions = {
inspectEnabled: boolean;
inspectBrkEnabled: boolean;
require?: string;
rspack?: typeof import('@rspack/core').rspack;
webpack?: typeof import('webpack');
};
+1 -3
View File
@@ -58,8 +58,6 @@ declare module 'react-dev-utils/ModuleScopePlugin' {
}
declare module 'react-dev-utils/FileSizeReporter' {
import webpack = require('webpack');
export interface OpaqueFileSizes {
root: string;
sizes: Record<string, number>;
@@ -81,7 +79,7 @@ declare module 'react-dev-utils/FileSizeReporter' {
* the main bundle or a chunk exceeds the specified size (in bytes).
*/
export function printFileSizesAfterBuild(
webpackStats: webpack.Stats,
stats: import('@rspack/core').Stats,
previousFileSizes: OpaqueFileSizes,
buildFolder: string,
maxBundleGzipSize?: number,
@@ -32,11 +32,17 @@ export default defineConfig({
? []
: [
{
command: 'yarn start',
command: 'yarn start app',
port: 3000,
reuseExistingServer: true,
timeout: 60_000,
},
{
command: 'yarn start backend',
port: 7007,
reuseExistingServer: true,
timeout: 60_000,
},
],
forbidOnly: !!process.env.CI,
@@ -32,11 +32,17 @@ export default defineConfig({
? []
: [
{
command: 'yarn start',
command: 'yarn start app',
port: 3000,
reuseExistingServer: true,
timeout: 60_000,
},
{
command: 'yarn start backend',
port: 7007,
reuseExistingServer: true,
timeout: 60_000,
},
],
forbidOnly: !!process.env.CI,
+1 -1
View File
@@ -221,7 +221,7 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) {
*/
async function pinYarnVersion(dir: string) {
const yarnRc = await fs.readFile(paths.resolveOwnRoot('.yarnrc.yml'), 'utf8');
const yarnRcLines = yarnRc.split('\n');
const yarnRcLines = yarnRc.split(/\r?\n/);
const yarnPathLine = yarnRcLines.find(line => line.startsWith('yarnPath:'));
if (!yarnPathLine) {
throw new Error(`Unable to find 'yarnPath' in ${yarnRc}`);
+7 -8
View File
@@ -32,18 +32,17 @@ export default defineConfig({
? []
: [
{
command: 'yarn start',
command: 'yarn start example-app',
port: 3000,
reuseExistingServer: true,
timeout: 60_000,
},
// TODO: Before encouraging e2e tests for backend we'll want to provide better utilities for mocking auth
// {
// command: 'yarn start-backend',
// port: 7007,
// reuseExistingServer: true,
// timeout: 60_000,
// },
{
command: 'yarn start example-backend',
port: 7007,
reuseExistingServer: true,
timeout: 60_000,
},
],
forbidOnly: !!process.env.CI,
+176 -37
View File
@@ -1896,14 +1896,14 @@ __metadata:
languageName: node
linkType: hard
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0, @babel/code-frame@npm:^7.26.2, @babel/code-frame@npm:^7.8.3":
version: 7.26.2
resolution: "@babel/code-frame@npm:7.26.2"
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0, @babel/code-frame@npm:^7.26.2, @babel/code-frame@npm:^7.27.1, @babel/code-frame@npm:^7.8.3":
version: 7.27.1
resolution: "@babel/code-frame@npm:7.27.1"
dependencies:
"@babel/helper-validator-identifier": "npm:^7.25.9"
"@babel/helper-validator-identifier": "npm:^7.27.1"
js-tokens: "npm:^4.0.0"
picocolors: "npm:^1.0.0"
checksum: 10/db2c2122af79d31ca916755331bb4bac96feb2b334cdaca5097a6b467fdd41963b89b14b6836a14f083de7ff887fc78fa1b3c10b14e743d33e12dbfe5ee3d223
picocolors: "npm:^1.1.1"
checksum: 10/721b8a6e360a1fa0f1c9fe7351ae6c874828e119183688b533c477aa378f1010f37cc9afbfc4722c686d1f5cdd00da02eab4ba7278a0c504fa0d7a321dcd4fdf
languageName: node
linkType: hard
@@ -2976,13 +2976,12 @@ __metadata:
"@rollup/plugin-json": "npm:^6.0.0"
"@rollup/plugin-node-resolve": "npm:^15.0.0"
"@rollup/plugin-yaml": "npm:^4.0.0"
"@rspack/core": "npm:^1.3.9"
"@rspack/dev-server": "npm:^1.1.1"
"@rspack/plugin-react-refresh": "npm:^1.4.2"
"@rspack/core": "npm:^1.4.11"
"@rspack/dev-server": "npm:^1.1.4"
"@rspack/plugin-react-refresh": "npm:^1.4.3"
"@spotify/eslint-config-base": "npm:^15.0.0"
"@spotify/eslint-config-react": "npm:^15.0.0"
"@spotify/eslint-config-typescript": "npm:^15.0.0"
"@sucrase/webpack-loader": "npm:^2.0.0"
"@swc/core": "npm:^1.3.46"
"@swc/helpers": "npm:^0.5.0"
"@swc/jest": "npm:^0.2.22"
@@ -3030,6 +3029,7 @@ __metadata:
eslint-plugin-react: "npm:^7.37.2"
eslint-plugin-react-hooks: "npm:^5.0.0"
eslint-plugin-unused-imports: "npm:^4.1.4"
eslint-rspack-plugin: "npm:^4.2.1"
eslint-webpack-plugin: "npm:^4.2.0"
express: "npm:^4.17.1"
fork-ts-checker-webpack-plugin: "npm:^9.0.0"
@@ -3075,10 +3075,11 @@ __metadata:
swc-loader: "npm:^0.2.3"
tar: "npm:^6.1.12"
terser-webpack-plugin: "npm:^5.1.3"
ts-checker-rspack-plugin: "npm:^1.1.5"
ts-morph: "npm:^24.0.0"
undici: "npm:^7.2.3"
util: "npm:^0.12.3"
webpack: "npm:^5.94.0"
webpack: "npm:~5.96.0"
webpack-dev-server: "npm:^5.0.0"
yaml: "npm:^2.0.0"
yargs: "npm:^16.2.0"
@@ -3087,15 +3088,33 @@ __metadata:
zod: "npm:^3.22.4"
zod-validation-error: "npm:^3.4.0"
peerDependencies:
"@rspack/core": ^1.2.8
"@rspack/dev-server": ^1.0.9
"@rspack/plugin-react-refresh": ^1.0.0
"@module-federation/enhanced": ^0.9.0
"@pmmmwh/react-refresh-webpack-plugin": ^0.5.7
esbuild-loader: ^4.0.0
eslint-webpack-plugin: ^4.2.0
fork-ts-checker-webpack-plugin: ^9.0.0
mini-css-extract-plugin: ^2.4.2
terser-webpack-plugin: ^5.1.3
webpack: ~5.96.0
webpack-dev-server: ^5.0.0
peerDependenciesMeta:
"@rspack/core":
"@module-federation/enhanced":
optional: true
"@rspack/dev-server":
"@pmmmwh/react-refresh-webpack-plugin":
optional: true
"@rspack/plugin-react-refresh":
esbuild-loader:
optional: true
eslint-webpack-plugin:
optional: true
fork-ts-checker-webpack-plugin:
optional: true
mini-css-extract-plugin:
optional: true
terser-webpack-plugin:
optional: true
webpack:
optional: true
webpack-dev-server:
optional: true
bin:
backstage-cli: bin/backstage-cli
@@ -10293,6 +10312,73 @@ __metadata:
languageName: node
linkType: hard
"@jsonjoy.com/base64@npm:^1.1.2":
version: 1.1.2
resolution: "@jsonjoy.com/base64@npm:1.1.2"
peerDependencies:
tslib: 2
checksum: 10/d76bb58eff841c090d9bf69a073611ffa73c40a664ccbcea689f65961f57d7b24051269d06b437e4f6204285d6ba92f50f587c5e95c5f9e4f10b36a2ed4cd0c8
languageName: node
linkType: hard
"@jsonjoy.com/buffers@npm:^1.0.0":
version: 1.0.0
resolution: "@jsonjoy.com/buffers@npm:1.0.0"
peerDependencies:
tslib: 2
checksum: 10/3347a16a555398c19265203877b8e325be5facc1d875c0e85db0cc20b53418302257ed5af15bf53b84ff8337bd024a10be25ff3a02ebc54ae8e643577ca29e63
languageName: node
linkType: hard
"@jsonjoy.com/codegen@npm:^1.0.0":
version: 1.0.0
resolution: "@jsonjoy.com/codegen@npm:1.0.0"
peerDependencies:
tslib: 2
checksum: 10/a0afb03d2af4fbc1377c547e507f5db99a25f515d8c4b6b2cef1ff28145ac59fff12b6e1f41f9734cb62ea5619e7f9be1acd0908305d6f4176898ee534ee9a64
languageName: node
linkType: hard
"@jsonjoy.com/json-pack@npm:^1.0.3":
version: 1.10.0
resolution: "@jsonjoy.com/json-pack@npm:1.10.0"
dependencies:
"@jsonjoy.com/base64": "npm:^1.1.2"
"@jsonjoy.com/buffers": "npm:^1.0.0"
"@jsonjoy.com/codegen": "npm:^1.0.0"
"@jsonjoy.com/json-pointer": "npm:^1.0.1"
"@jsonjoy.com/util": "npm:^1.9.0"
hyperdyperid: "npm:^1.2.0"
thingies: "npm:^2.5.0"
peerDependencies:
tslib: 2
checksum: 10/5ad1eb008cf5f681dbc1301f5a7c33ca3a101d22773dbd2a9db4b02e5847c2f8b0c5f074c69f4ced5c24b3dc1dd85ddbc1688f0002fee9416869c579c34c4a8e
languageName: node
linkType: hard
"@jsonjoy.com/json-pointer@npm:^1.0.1":
version: 1.0.1
resolution: "@jsonjoy.com/json-pointer@npm:1.0.1"
dependencies:
"@jsonjoy.com/util": "npm:^1.3.0"
peerDependencies:
tslib: 2
checksum: 10/ff15df95df38677a09fe2155cf4521844c9f0653c77a7090bb6e726d275fb78ab6c3543c4ebdc14aff07e44da08b1545631a63e42e217c18059194957e060ed9
languageName: node
linkType: hard
"@jsonjoy.com/util@npm:^1.3.0, @jsonjoy.com/util@npm:^1.9.0":
version: 1.9.0
resolution: "@jsonjoy.com/util@npm:1.9.0"
dependencies:
"@jsonjoy.com/buffers": "npm:^1.0.0"
"@jsonjoy.com/codegen": "npm:^1.0.0"
peerDependencies:
tslib: 2
checksum: 10/1a6e5301d725a7161b93ff707eb1a954bf4552a2fa96eee9a960d3ae3ed5f993d18b56dcff29e98036341a5968c5d1b2dfe21f76695390e7f0d89b81f24c85e0
languageName: node
linkType: hard
"@juggle/resize-observer@npm:^3.3.1":
version: 3.3.1
resolution: "@juggle/resize-observer@npm:3.3.1"
@@ -16831,7 +16917,7 @@ __metadata:
languageName: node
linkType: hard
"@rspack/core@npm:^1.3.9":
"@rspack/core@npm:^1.4.11":
version: 1.4.11
resolution: "@rspack/core@npm:1.4.11"
dependencies:
@@ -16847,7 +16933,7 @@ __metadata:
languageName: node
linkType: hard
"@rspack/dev-server@npm:^1.1.1":
"@rspack/dev-server@npm:^1.1.4":
version: 1.1.4
resolution: "@rspack/dev-server@npm:1.1.4"
dependencies:
@@ -16862,14 +16948,14 @@ __metadata:
languageName: node
linkType: hard
"@rspack/lite-tapable@npm:1.0.1":
"@rspack/lite-tapable@npm:1.0.1, @rspack/lite-tapable@npm:^1.0.1":
version: 1.0.1
resolution: "@rspack/lite-tapable@npm:1.0.1"
checksum: 10/240b7832965bca5a52d1f03a8539dab5810958ce24b5a670405b2505d81350f10d668f4055648f5918bc18ac033e637bcb7f92189345f0f2f671b546019c2f9e
languageName: node
linkType: hard
"@rspack/plugin-react-refresh@npm:^1.4.2":
"@rspack/plugin-react-refresh@npm:^1.4.3":
version: 1.4.3
resolution: "@rspack/plugin-react-refresh@npm:1.4.3"
dependencies:
@@ -18870,17 +18956,6 @@ __metadata:
languageName: node
linkType: hard
"@sucrase/webpack-loader@npm:^2.0.0":
version: 2.0.0
resolution: "@sucrase/webpack-loader@npm:2.0.0"
dependencies:
loader-utils: "npm:^1.1.0"
peerDependencies:
sucrase: ^3
checksum: 10/16578991b1b888ac5bec5628bd24db9e21651bbbe30de076aece8787f115d8971ac87a20bc75446187c73c3185851ec2233d5b6f18c4a2dd53fbbb1ed4e488b4
languageName: node
linkType: hard
"@swagger-api/apidom-ast@npm:^1.0.0-beta.11":
version: 1.0.0-beta.11
resolution: "@swagger-api/apidom-ast@npm:1.0.0-beta.11"
@@ -29853,6 +29928,21 @@ __metadata:
languageName: node
linkType: hard
"eslint-rspack-plugin@npm:^4.2.1":
version: 4.2.1
resolution: "eslint-rspack-plugin@npm:4.2.1"
dependencies:
"@types/eslint": "npm:^8.56.10"
jest-worker: "npm:^29.7.0"
micromatch: "npm:^4.0.8"
normalize-path: "npm:^3.0.0"
schema-utils: "npm:^4.2.0"
peerDependencies:
eslint: ^8.0.0 || ^9.0.0
checksum: 10/c45639e660d13af63c58f8f80103a033f607b9d635d9315d7d859408134cc35e2e3f981805f7abdd6061ec9d8207cfc7c0d5a097df9ac8c7ccd707ca5aa14583
languageName: node
linkType: hard
"eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1":
version: 5.1.1
resolution: "eslint-scope@npm:5.1.1"
@@ -33252,6 +33342,13 @@ __metadata:
languageName: node
linkType: hard
"hyperdyperid@npm:^1.2.0":
version: 1.2.0
resolution: "hyperdyperid@npm:1.2.0"
checksum: 10/64abb5568ff17aa08ac0175ae55e46e22831c5552be98acdd1692081db0209f36fff58b31432017b4e1772c178962676a2cc3c54e4d5d7f020d7710cec7ad7a6
languageName: node
linkType: hard
"hyphenate-style-name@npm:^1.0.3":
version: 1.0.3
resolution: "hyphenate-style-name@npm:1.0.3"
@@ -37840,12 +37937,15 @@ __metadata:
languageName: node
linkType: hard
"memfs@npm:^4.6.0":
version: 4.7.7
resolution: "memfs@npm:4.7.7"
"memfs@npm:^4.28.0, memfs@npm:^4.6.0":
version: 4.36.0
resolution: "memfs@npm:4.36.0"
dependencies:
"@jsonjoy.com/json-pack": "npm:^1.0.3"
"@jsonjoy.com/util": "npm:^1.3.0"
tree-dump: "npm:^1.0.1"
tslib: "npm:^2.0.0"
checksum: 10/311633e5857c91f41021b43f00eda8d540fed2c2d9e02c780fe78de720cfb55d15ab2d5b5ce9f2576637589b82e84488f1b9ff503563e817ed65200ad24617fb
checksum: 10/4898187c3278bd127340ec3d6c51d1fa77faa50aad71a8a1e4b7c02abbccd8485dccc7f565291b386b0cc96e5ac06902e4645108fad44e458fe8c1dfee3caf42
languageName: node
linkType: hard
@@ -47372,6 +47472,15 @@ __metadata:
languageName: node
linkType: hard
"thingies@npm:^2.5.0":
version: 2.5.0
resolution: "thingies@npm:2.5.0"
peerDependencies:
tslib: ^2
checksum: 10/b8b028a474aab798ff12ad5a5648306059976d3e23870327ae4ef640012953314b1d7f208dd5a8e9ebaeee6dd1cdb05503bb829699ee8f36514c37f771f2f035
languageName: node
linkType: hard
"throttle-debounce@npm:^3.0.1":
version: 3.0.1
resolution: "throttle-debounce@npm:3.0.1"
@@ -47671,6 +47780,15 @@ __metadata:
languageName: node
linkType: hard
"tree-dump@npm:^1.0.1":
version: 1.0.3
resolution: "tree-dump@npm:1.0.3"
peerDependencies:
tslib: 2
checksum: 10/cf382e61cfb5e3ff8f03425b5bc1923e8f0e385b3a02f43d9d0a32d09da9984477e0f2a7698628662263d1d3f1af17e33486c77ff454978f0f9f07fb5d1fe9a2
languageName: node
linkType: hard
"tree-kill@npm:^1.2.2":
version: 1.2.2
resolution: "tree-kill@npm:1.2.2"
@@ -47767,6 +47885,27 @@ __metadata:
languageName: node
linkType: hard
"ts-checker-rspack-plugin@npm:^1.1.5":
version: 1.1.5
resolution: "ts-checker-rspack-plugin@npm:1.1.5"
dependencies:
"@babel/code-frame": "npm:^7.27.1"
"@rspack/lite-tapable": "npm:^1.0.1"
chokidar: "npm:^3.6.0"
is-glob: "npm:^4.0.3"
memfs: "npm:^4.28.0"
minimatch: "npm:^9.0.5"
picocolors: "npm:^1.1.1"
peerDependencies:
"@rspack/core": ^1.0.0
typescript: ">=3.8.0"
peerDependenciesMeta:
"@rspack/core":
optional: true
checksum: 10/d43f87a5860f3b8f528bf8f43c78c2d958a18fa484d791a204047fc5672134fd689665021ee25b07f9f9d8c807cd291978f58b789bf531b4704a9a4e3ad84735
languageName: node
linkType: hard
"ts-dedent@npm:^2.0.0, ts-dedent@npm:^2.2.0":
version: 2.2.0
resolution: "ts-dedent@npm:2.2.0"
@@ -49468,7 +49607,7 @@ __metadata:
languageName: node
linkType: hard
"webpack@npm:5, webpack@npm:^5, webpack@npm:^5.94.0":
"webpack@npm:5, webpack@npm:^5, webpack@npm:~5.96.0":
version: 5.96.1
resolution: "webpack@npm:5.96.1"
dependencies: