feat(angular): add convert-to-rspack generator (#29860)

## Current Behavior
Nx currently does not offer an automated method for switching from an
Angular Webpack build to an Angular Rspack build.

## Expected Behavior
Provide a generator `convert-to-rspack` in `@nx/angular` that will allow
conversion from an Angular Webpack build to an Angular Rspack build.

Usage: `nx g convert-to-rspack --project=myapp`

## TODO
- [x] handle more builder options
- [x] take existing custom webpack configs and migrate into the rspack
config that is created
This commit is contained in:
Colum Ferry 2025-03-14 17:11:21 +00:00 committed by GitHub
parent f40873ffbe
commit 0082081d5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1503 additions and 7 deletions

View File

@ -7314,6 +7314,14 @@
"isExternal": false,
"disableCollapsible": false
},
{
"id": "convert-to-rspack",
"path": "/nx-api/angular/generators/convert-to-rspack",
"name": "convert-to-rspack",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "directive",
"path": "/nx-api/angular/generators/directive",

View File

@ -208,6 +208,15 @@
"path": "/nx-api/angular/generators/convert-to-application-executor",
"type": "generator"
},
"/nx-api/angular/generators/convert-to-rspack": {
"description": "Converts Angular Webpack projects to use Rspack.",
"file": "generated/packages/angular/generators/convert-to-rspack.json",
"hidden": false,
"name": "convert-to-rspack",
"originalFilePath": "/packages/angular/src/generators/convert-to-rspack/schema.json",
"path": "/nx-api/angular/generators/convert-to-rspack",
"type": "generator"
},
"/nx-api/angular/generators/directive": {
"description": "Generate an Angular directive.",
"file": "generated/packages/angular/generators/directive.json",

View File

@ -203,6 +203,15 @@
"path": "angular/generators/convert-to-application-executor",
"type": "generator"
},
{
"description": "Converts Angular Webpack projects to use Rspack.",
"file": "generated/packages/angular/generators/convert-to-rspack.json",
"hidden": false,
"name": "convert-to-rspack",
"originalFilePath": "/packages/angular/src/generators/convert-to-rspack/schema.json",
"path": "angular/generators/convert-to-rspack",
"type": "generator"
},
{
"description": "Generate an Angular directive.",
"file": "generated/packages/angular/generators/directive.json",

View File

@ -0,0 +1,38 @@
{
"name": "convert-to-rspack",
"factory": "./src/generators/convert-to-rspack/convert-to-rspack",
"schema": {
"$schema": "https://json-schema.org/schema",
"$id": "GeneratorNxApp",
"title": "Creates an Angular application.",
"description": "Creates an Angular application.",
"type": "object",
"cli": "nx",
"properties": {
"project": {
"type": "string",
"aliases": ["name", "projectName"],
"description": "Project for which to convert to rspack.",
"$default": { "$source": "argv", "index": 0 },
"x-priority": "important"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"skipInstall": {
"description": "Skip installing dependencies.",
"type": "boolean",
"default": false
}
},
"presets": []
},
"description": "Converts Angular Webpack projects to use Rspack.",
"implementation": "/packages/angular/src/generators/convert-to-rspack/convert-to-rspack.ts",
"aliases": [],
"hidden": false,
"path": "/packages/angular/src/generators/convert-to-rspack/schema.json",
"type": "generator"
}

View File

@ -366,6 +366,7 @@
- [component-story](/nx-api/angular/generators/component-story)
- [component-test](/nx-api/angular/generators/component-test)
- [convert-to-application-executor](/nx-api/angular/generators/convert-to-application-executor)
- [convert-to-rspack](/nx-api/angular/generators/convert-to-rspack)
- [directive](/nx-api/angular/generators/directive)
- [federate-module](/nx-api/angular/generators/federate-module)
- [init](/nx-api/angular/generators/init)

View File

@ -144,3 +144,25 @@ describe('Move Angular Project', () => {
expect(lib2File).toContain(`extends ${newModule}`);
});
});
describe('Convert Angular Webpack Project to Rspack', () => {
let proj: string;
let app1: string;
beforeAll(() => {
proj = newProject({ packages: ['@nx/angular'] });
app1 = uniq('app1');
runCLI(
`generate @nx/angular:app ${app1} --bundler=webpack --no-interactive`
);
});
afterAll(() => cleanupProject());
it('should convert an Angular Webpack project to Rspack', async () => {
runCLI(`generate @nx/angular:convert-to-rspack --project=${app1}`);
const buildOutput = runCLI(`build ${app1}`);
expect(buildOutput).toContain('rspack build');
expect(buildOutput).toContain('browser compiled');
});
});

View File

@ -38,6 +38,11 @@
"schema": "./src/generators/convert-to-application-executor/schema.json",
"description": "Converts projects to use the `@nx/angular:application` executor or the `@angular-devkit/build-angular:application` builder."
},
"convert-to-rspack": {
"factory": "./src/generators/convert-to-rspack/convert-to-rspack",
"schema": "./src/generators/convert-to-rspack/schema.json",
"description": "Converts Angular Webpack projects to use Rspack."
},
"directive": {
"factory": "./src/generators/directive/directive",
"schema": "./src/generators/directive/schema.json",

View File

@ -50,6 +50,7 @@
"dependencies": {
"@phenomnomnominal/tsquery": "~5.0.1",
"@typescript-eslint/type-utils": "^8.0.0",
"enquirer": "~2.3.6",
"picocolors": "^1.1.0",
"magic-string": "~0.30.2",
"minimatch": "9.0.3",
@ -60,6 +61,7 @@
"@nx/js": "file:../js",
"@nx/eslint": "file:../eslint",
"@nx/webpack": "file:../webpack",
"@nx/rspack": "file:../rspack",
"@nx/module-federation": "file:../module-federation",
"@nx/web": "file:../web",
"@nx/workspace": "file:../workspace",

View File

@ -0,0 +1,487 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { convertToRspack } from './convert-to-rspack';
import {
addProjectConfiguration,
readJson,
readNxJson,
readProjectConfiguration,
updateJson,
writeJson,
} from '@nx/devkit';
import * as _configUtils from '@nx/devkit/src/utils/config-utils';
jest.mock('@nx/devkit/src/utils/config-utils', () => ({
...jest.requireActual('@nx/devkit/src/utils/config-utils'),
loadConfigFile: jest.fn().mockImplementation(async (path) => {
return () => {
return {};
};
}),
}));
describe('convert-to-rspack', () => {
it('should convert a basic angular webpack application to rspack', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
polyfills: ['tslib'], // zone.js is not in nx repo's node_modules so simulating it with a package that is
tsConfig: 'apps/app/tsconfig.app.json',
assets: [
'apps/app/src/favicon.ico',
'apps/app/src/assets',
{ input: 'apps/app/public', glob: '**/*' },
],
styles: ['apps/app/src/styles.scss'],
scripts: [],
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
updateJson(tree, 'package.json', (json) => {
json.scripts ??= {};
json.scripts.build = 'nx build';
return json;
});
// ACT
await convertToRspack(tree, { project: 'app' });
// ASSERT
const updatedProject = readProjectConfiguration(tree, 'app');
const pkgJson = readJson(tree, 'package.json');
const nxJson = readNxJson(tree);
expect(tree.read('apps/app/rspack.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { createConfig } from '@nx/angular-rspack';
export default createConfig({
options: {
root: __dirname,
outputPath: {
base: '../../dist/apps/app',
},
index: './src/index.html',
browser: './src/main.ts',
polyfills: ['tslib'],
tsConfig: './tsconfig.app.json',
assets: [
'./src/favicon.ico',
'./src/assets',
{
input: './public',
glob: '**/*',
},
],
styles: ['./src/styles.scss'],
scripts: [],
},
});
"
`);
expect(pkgJson.devDependencies['@nx/angular-rspack']).toBeDefined();
expect(
nxJson.plugins.find((p) =>
typeof p === 'string' ? false : p.plugin === '@nx/rspack/plugin'
)
).toBeDefined();
expect(pkgJson.scripts?.build).toBeUndefined();
expect(updatedProject.targets.build).not.toBeDefined();
expect(updatedProject.targets.serve).not.toBeDefined();
});
it('should normalize paths to libs in workspace correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
polyfills: ['tslib', 'apps/app/src/polyfills.ts'],
tsConfig: 'apps/app/tsconfig.app.json',
assets: ['libs/mylib/src/favicon.ico'],
styles: ['apps/app/src/styles.scss'],
scripts: [],
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
updateJson(tree, 'package.json', (json) => {
json.scripts ??= {};
json.scripts.build = 'nx build';
return json;
});
tree.write('libs/mylib/src/favicon.ico', 'libs/mylib/src/favicon.ico');
// ACT
await convertToRspack(tree, { project: 'app' });
// ASSERT
const updatedProject = readProjectConfiguration(tree, 'app');
const pkgJson = readJson(tree, 'package.json');
const nxJson = readNxJson(tree);
expect(tree.read('apps/app/rspack.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { createConfig } from '@nx/angular-rspack';
export default createConfig({
options: {
root: __dirname,
outputPath: {
base: '../../dist/apps/app',
},
index: './src/index.html',
browser: './src/main.ts',
polyfills: ['tslib', './src/polyfills.ts'],
tsConfig: './tsconfig.app.json',
assets: ['../../libs/mylib/src/favicon.ico'],
styles: ['./src/styles.scss'],
scripts: [],
},
});
"
`);
expect(pkgJson.devDependencies['@nx/angular-rspack']).toBeDefined();
expect(
nxJson.plugins.find((p) =>
typeof p === 'string' ? false : p.plugin === '@nx/rspack/plugin'
)
).toBeDefined();
expect(pkgJson.scripts?.build).toBeUndefined();
expect(updatedProject.targets.build).not.toBeDefined();
expect(updatedProject.targets.serve).not.toBeDefined();
});
it('should convert a basic angular webpack application with configurations to rspack', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
polyfills: ['tslib'],
tsConfig: 'apps/app/tsconfig.app.json',
assets: [
'apps/app/src/favicon.ico',
'apps/app/src/assets',
{ input: 'apps/app/public', glob: '**/*' },
],
styles: ['apps/app/src/styles.scss'],
scripts: [],
},
configurations: {
production: {
outputPath: 'dist/apps/app-prod',
index: 'apps/app/src/index.prod.html',
main: 'apps/app/src/main.prod.ts',
tsConfig: 'apps/app/tsconfig.prod.json',
},
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
// ACT
await convertToRspack(tree, { project: 'app' });
// ASSERT
const updatedProject = readProjectConfiguration(tree, 'app');
const pkgJson = readJson(tree, 'package.json');
const nxJson = readNxJson(tree);
expect(tree.read('apps/app/rspack.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { createConfig } from '@nx/angular-rspack';
export default createConfig(
{
options: {
root: __dirname,
outputPath: {
base: '../../dist/apps/app',
},
index: './src/index.html',
browser: './src/main.ts',
polyfills: ['tslib'],
tsConfig: './tsconfig.app.json',
assets: [
'./src/favicon.ico',
'./src/assets',
{
input: './public',
glob: '**/*',
},
],
styles: ['./src/styles.scss'],
scripts: [],
},
},
{
production: {
options: {
outputPath: {
base: '../../dist/apps/app-prod',
},
index: './src/index.prod.html',
browser: './src/main.prod.ts',
tsConfig: './tsconfig.prod.json',
},
},
}
);
"
`);
expect(pkgJson.devDependencies['@nx/angular-rspack']).toBeDefined();
expect(
nxJson.plugins.find((p) =>
typeof p === 'string' ? false : p.plugin === '@nx/rspack/plugin'
)
).toBeDefined();
expect(updatedProject.targets.build).not.toBeDefined();
expect(updatedProject.targets.serve).not.toBeDefined();
});
it('should convert an angular webpack application with custom webpack config function to rspack', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@nx/angular:webpack-browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
polyfills: ['tslib'],
tsConfig: 'apps/app/tsconfig.app.json',
assets: [
'apps/app/src/favicon.ico',
'apps/app/src/assets',
{ input: 'apps/app/public', glob: '**/*' },
],
styles: ['apps/app/src/styles.scss'],
scripts: [],
customWebpackConfig: {
path: 'apps/app/webpack.config.js',
},
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
tree.write(
'apps/app/module-federation.config.js',
`
module.exports = {
name: 'app',
exposes: {
'./app': './src/app/index.ts',
},
remotes: ['remote1', 'remote2'],
};
`
);
tree.write(
'apps/app/webpack.config.js',
`
const { withModuleFederation } = require('@nx/module-federation/angular');
const config = require('./module-federation.config');
module.exports = withModuleFederation(config, { dts: false });
`
);
// ACT
await convertToRspack(tree, { project: 'app' });
// ASSERT
const updatedProject = readProjectConfiguration(tree, 'app');
expect(tree.read('apps/app/rspack.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { createConfig } from '@nx/angular-rspack';
import baseWebpackConfig from './webpack.config';
import webpackMerge from 'webpack-merge';
const baseConfig = createConfig({
options: {
root: __dirname,
outputPath: {
base: '../../dist/apps/app',
},
index: './src/index.html',
browser: './src/main.ts',
polyfills: ['tslib'],
tsConfig: './tsconfig.app.json',
assets: [
'./src/favicon.ico',
'./src/assets',
{
input: './public',
glob: '**/*',
},
],
styles: ['./src/styles.scss'],
scripts: [],
},
});
export default webpackMerge(baseConfig[0], baseWebpackConfig);
"
`);
expect(tree.read('apps/app/webpack.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"const { NxModuleFederationPlugin } = require('@nx/module-federation/rspack');
const config = require('./module-federation.config');
module.exports = {
plugins: [
new NxModuleFederationPlugin(
{ config },
{
dts: false,
}
),
],
};
"
`);
});
it('should convert an angular webpack application with custom webpack config to rspack', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@nx/angular:webpack-browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
polyfills: ['tslib'],
tsConfig: 'apps/app/tsconfig.app.json',
assets: [
'apps/app/src/favicon.ico',
'apps/app/src/assets',
{ input: 'apps/app/public', glob: '**/*' },
],
styles: ['apps/app/src/styles.scss'],
scripts: [],
customWebpackConfig: {
path: 'apps/app/webpack.config.js',
},
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
tree.write(
'apps/app/module-federation.config.js',
`
module.exports = {
name: 'app',
exposes: {
'./app': './src/app/index.ts',
},
remotes: ['remote1', 'remote2'],
};
`
);
tree.write('apps/app/webpack.config.js', ``);
jest
.spyOn(_configUtils, 'loadConfigFile')
.mockImplementation(async (path) => {
return {
default: {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
},
};
});
// ACT
await convertToRspack(tree, { project: 'app' });
// ASSERT
const updatedProject = readProjectConfiguration(tree, 'app');
expect(tree.read('apps/app/rspack.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { createConfig } from '@nx/angular-rspack';
import baseWebpackConfig from './webpack.config';
import webpackMerge from 'webpack-merge';
const baseConfig = createConfig({
options: {
root: __dirname,
outputPath: {
base: '../../dist/apps/app',
},
index: './src/index.html',
browser: './src/main.ts',
polyfills: ['tslib'],
tsConfig: './tsconfig.app.json',
assets: [
'./src/favicon.ico',
'./src/assets',
{
input: './public',
glob: '**/*',
},
],
styles: ['./src/styles.scss'],
scripts: [],
},
});
export default webpackMerge(baseConfig[0], baseWebpackConfig);
"
`);
});
});

View File

@ -0,0 +1,466 @@
import {
type Tree,
readProjectConfiguration,
addDependenciesToPackageJson,
formatFiles,
GeneratorCallback,
runTasksInSerial,
ensurePackage,
updateProjectConfiguration,
workspaceRoot,
joinPathFragments,
readJson,
writeJson,
} from '@nx/devkit';
import type { ConvertToRspackSchema } from './schema';
import { angularRspackVersion, nxVersion } from '../../utils/versions';
import { createConfig } from './lib/create-config';
import { getCustomWebpackConfig } from './lib/get-custom-webpack-config';
import { updateTsconfig } from './lib/update-tsconfig';
import { validateSupportedBuildExecutor } from './lib/validate-supported-executor';
import { join } from 'path/posix';
import { relative } from 'path';
import { existsSync } from 'fs';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import { prompt } from 'enquirer';
const SUPPORTED_EXECUTORS = [
'@angular-devkit/build-angular:browser',
'@angular-devkit/build-angular:dev-server',
'@nx/angular:webpack-browser',
'@nx/angular:dev-server',
'@nx/angular:module-federation-dev-server',
];
const RENAMED_OPTIONS = {
main: 'browser',
ngswConfigPath: 'serviceWorker',
};
const REMOVED_OPTIONS = [
'publicHost',
'disableHostCheck',
'resourcesOutputPath',
'routesFile',
'routes',
'discoverRoutes',
'appModuleBundle',
'inputIndexPath',
'outputIndexPath',
'buildOptimizer',
'deployUrl',
'buildTarget',
'browserTarget',
];
function normalizeFromProjectRoot(
tree: Tree,
path: string,
projectRoot: string
) {
if (projectRoot === '.') {
if (!path.startsWith('./')) {
return `./${path}`;
} else {
return path;
}
} else if (path.startsWith(projectRoot)) {
return path.replace(projectRoot, '.');
} else if (!path.startsWith('./')) {
if (tree.exists(path)) {
const pathWithWorkspaceRoot = joinPathFragments(workspaceRoot, path);
const projectRootWithWorkspaceRoot = joinPathFragments(
workspaceRoot,
projectRoot
);
return relative(projectRootWithWorkspaceRoot, pathWithWorkspaceRoot);
}
return `./${path}`;
}
return path;
}
const defaultNormalizer = (tree: Tree, path: string, root: string) =>
normalizeFromProjectRoot(tree, path, root);
const PATH_NORMALIZER = {
index: (
tree: Tree,
path: string | { input: string; output: string },
root: string
) => {
if (typeof path === 'string') {
return normalizeFromProjectRoot(tree, path, root);
}
return {
input: normalizeFromProjectRoot(tree, path.input, root),
output: path.output ?? 'index.html',
};
},
indexHtmlTransformer: defaultNormalizer,
main: defaultNormalizer,
server: defaultNormalizer,
tsConfig: defaultNormalizer,
outputPath: (tree: Tree, path: string, root: string) => {
const relativePathFromWorkspaceRoot = relative(
joinPathFragments(workspaceRoot, root),
workspaceRoot
);
return joinPathFragments(relativePathFromWorkspaceRoot, path);
},
proxyConfig: defaultNormalizer,
polyfills: (tree: Tree, paths: string | string[], root: string) => {
const normalizedPaths: string[] = [];
const normalizeFn = (path: string) => {
try {
const resolvedPath = require.resolve(path, {
paths: [join(workspaceRoot, 'node_modules')],
});
normalizedPaths.push(path);
} catch {
normalizedPaths.push(normalizeFromProjectRoot(tree, path, root));
}
};
if (typeof paths === 'string') {
normalizeFn(paths);
} else {
for (const path of paths) {
normalizeFn(path);
}
}
return normalizedPaths;
},
styles: (
tree: Tree,
paths: Array<
string | { input: string; bundleName: string; inject: boolean }
>,
root: string
) => {
const normalizedPaths: Array<
string | { input: string; bundleName: string; inject: boolean }
> = [];
for (const path of paths) {
if (typeof path === 'string') {
normalizedPaths.push(normalizeFromProjectRoot(tree, path, root));
} else {
normalizedPaths.push({
input: normalizeFromProjectRoot(tree, path.input, root),
bundleName: path.bundleName,
inject: path.inject ?? true,
});
}
}
return normalizedPaths;
},
scripts: (
tree: Tree,
paths: Array<
string | { input: string; bundleName: string; inject: boolean }
>,
root: string
) => {
const normalizedPaths: Array<
string | { input: string; bundleName: string; inject: boolean }
> = [];
for (const path of paths) {
if (typeof path === 'string') {
normalizedPaths.push(normalizeFromProjectRoot(tree, path, root));
} else {
normalizedPaths.push({
input: normalizeFromProjectRoot(tree, path.input, root),
bundleName: path.bundleName,
inject: path.inject ?? true,
});
}
}
return normalizedPaths;
},
assets: (
tree: Tree,
paths: Array<string | { input: string; [key: string]: any }>,
root: string
) => {
const normalizedPaths: Array<
string | { input: string; [key: string]: any }
> = [];
for (const path of paths) {
if (typeof path === 'string') {
normalizedPaths.push(normalizeFromProjectRoot(tree, path, root));
} else {
normalizedPaths.push({
...path,
input: normalizeFromProjectRoot(tree, path.input, root),
});
}
}
return normalizedPaths;
},
fileReplacements: (
tree: Tree,
paths: Array<
{ replace: string; with: string } | { src: string; replaceWith: string }
>,
root: string
) => {
const normalizedPaths: Array<
{ replace: string; with: string } | { src: string; replaceWith: string }
> = [];
for (const path of paths) {
normalizedPaths.push({
replace: normalizeFromProjectRoot(
tree,
'src' in path ? path.src : path.replace,
root
),
with: normalizeFromProjectRoot(
tree,
'replaceWith' in path ? path.replaceWith : path.with,
root
),
});
}
return normalizedPaths;
},
};
function handleBuildTargetOptions(
tree: Tree,
options: Record<string, any>,
newConfigurationOptions: Record<string, any>,
root: string
) {
let customWebpackConfigPath: string | undefined;
if (!options || Object.keys(options).length === 0) {
return customWebpackConfigPath;
}
if (options.customWebpackConfig) {
customWebpackConfigPath = options.customWebpackConfig.path;
delete options.customWebpackConfig;
}
if (options.outputs) {
// handled by the Rspack inference plugin
delete options.outputs;
}
for (const [key, value] of Object.entries(options)) {
let optionName = key;
let optionValue =
key in PATH_NORMALIZER ? PATH_NORMALIZER[key](tree, value, root) : value;
if (REMOVED_OPTIONS.includes(key)) {
continue;
}
if (key in RENAMED_OPTIONS) {
optionName = RENAMED_OPTIONS[key];
}
newConfigurationOptions[optionName] = optionValue;
}
if (typeof newConfigurationOptions.polyfills === 'string') {
newConfigurationOptions.polyfills = [newConfigurationOptions.polyfills];
}
let outputPath = newConfigurationOptions.outputPath;
if (typeof outputPath === 'string') {
if (!/\/browser\/?$/.test(outputPath)) {
console.warn(
`The output location of the browser build has been updated from "${outputPath}" to ` +
`"${join(outputPath, 'browser')}". ` +
'You might need to adjust your deployment pipeline or, as an alternative, ' +
'set outputPath.browser to "" in order to maintain the previous functionality.'
);
} else {
outputPath = outputPath.replace(/\/browser\/?$/, '');
}
newConfigurationOptions['outputPath'] = {
base: outputPath,
};
if (typeof newConfigurationOptions.resourcesOutputPath === 'string') {
const media = newConfigurationOptions.resourcesOutputPath.replaceAll(
'/',
''
);
if (media && media !== 'media') {
newConfigurationOptions['outputPath'] = {
base: outputPath,
media,
};
}
}
}
return customWebpackConfigPath;
}
function handleDevServerTargetOptions(
tree: Tree,
options: Record<string, any>,
newConfigurationOptions: Record<string, any>,
root: string
) {
for (const [key, value] of Object.entries(options)) {
let optionName = key;
let optionValue =
key in PATH_NORMALIZER ? PATH_NORMALIZER[key](tree, value, root) : value;
if (REMOVED_OPTIONS.includes(key)) {
continue;
}
if (key in RENAMED_OPTIONS) {
optionName = RENAMED_OPTIONS[key];
}
newConfigurationOptions[optionName] = optionValue;
}
}
async function getProjectToConvert(tree: Tree) {
const projects = new Set<string>();
for (const executor of SUPPORTED_EXECUTORS) {
forEachExecutorOptions(tree, executor, (_, project) => {
projects.add(project);
});
}
const { project } = await prompt<{ project: string }>({
type: 'select',
name: 'project',
message: 'Which project would you like to convert to rspack?',
choices: Array.from(projects),
});
return project;
}
export async function convertToRspack(
tree: Tree,
schema: ConvertToRspackSchema
) {
let { project: projectName } = schema;
if (!projectName) {
projectName = await getProjectToConvert(tree);
}
const project = readProjectConfiguration(tree, projectName);
const tasks: GeneratorCallback[] = [];
const createConfigOptions: Record<string, any> = {
root: project.root,
};
const configurationOptions: Record<string, Record<string, any>> = {};
const buildTargetNames: string[] = [];
const serveTargetNames: string[] = [];
let customWebpackConfigPath: string | undefined;
validateSupportedBuildExecutor(Object.values(project.targets));
for (const [targetName, target] of Object.entries(project.targets)) {
if (
target.executor === '@angular-devkit/build-angular:browser' ||
target.executor === '@nx/angular:webpack-browser'
) {
customWebpackConfigPath = handleBuildTargetOptions(
tree,
target.options,
createConfigOptions,
project.root
);
if (target.configurations) {
for (const [configurationName, configuration] of Object.entries(
target.configurations
)) {
configurationOptions[configurationName] = {};
handleBuildTargetOptions(
tree,
configuration,
configurationOptions[configurationName],
project.root
);
}
}
buildTargetNames.push(targetName);
} else if (
target.executor === '@angular-devkit/build-angular:dev-server' ||
target.executor === '@nx/angular:dev-server' ||
target.executor === '@nx/angular:module-federation-dev-server'
) {
createConfigOptions.devServer = {};
if (target.options) {
handleDevServerTargetOptions(
tree,
target.options,
createConfigOptions.devServer,
project.root
);
}
if (target.configurations) {
for (const [configurationName, configuration] of Object.entries(
target.configurations
)) {
configurationOptions[configurationName] ??= {};
configurationOptions[configurationName].devServer ??= {};
handleDevServerTargetOptions(
tree,
configuration,
configurationOptions[configurationName].devServer,
project.root
);
}
}
}
serveTargetNames.push(targetName);
}
const customWebpackConfigInfo = customWebpackConfigPath
? await getCustomWebpackConfig(tree, project.root, customWebpackConfigPath)
: undefined;
createConfig(
tree,
createConfigOptions,
configurationOptions,
customWebpackConfigInfo?.normalizedPathToCustomWebpackConfig,
customWebpackConfigInfo?.isWebpackConfigFunction
);
updateTsconfig(tree, project.root);
for (const targetName of [...buildTargetNames, ...serveTargetNames]) {
delete project.targets[targetName];
}
updateProjectConfiguration(tree, projectName, project);
const { rspackInitGenerator } = ensurePackage<typeof import('@nx/rspack')>(
'@nx/rspack',
nxVersion
);
await rspackInitGenerator(tree, {
addPlugin: true,
});
// This is needed to prevent a circular execution of the build target
const rootPkgJson = readJson(tree, 'package.json');
if (rootPkgJson.scripts?.build === 'nx build') {
delete rootPkgJson.scripts.build;
writeJson(tree, 'package.json', rootPkgJson);
}
if (!schema.skipInstall) {
const installTask = addDependenciesToPackageJson(
tree,
{},
{
'@nx/angular-rspack': angularRspackVersion,
}
);
tasks.push(installTask);
}
if (!schema.skipFormat) {
await formatFiles(tree);
}
return runTasksInSerial(...tasks);
}
export default convertToRspack;

View File

@ -0,0 +1,98 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { createConfig } from './create-config';
describe('createConfig', () => {
it('should create a config file', () => {
const tree = createTreeWithEmptyWorkspace();
const opts = {
root: 'root',
index: 'src/index.html',
browser: 'src/main.ts',
tsconfigPath: 'tsconfig.app.json',
polyfills: ['zone.js'],
assets: ['public'],
styles: ['src/styles.css'],
scripts: [],
jit: false,
inlineStylesExtension: 'css',
fileReplacements: [],
hasServer: false,
skipTypeChecking: false,
};
createConfig(tree, opts);
expect(tree.read('root/rspack.config.ts', 'utf-8')).toMatchInlineSnapshot(`
"
import { createConfig }from '@nx/angular-rspack';
export default createConfig({
options: {
root: __dirname,
"index": "src/index.html",
"browser": "src/main.ts",
"tsconfigPath": "tsconfig.app.json",
"polyfills": [
"zone.js"
],
"assets": [
"public"
],
"styles": [
"src/styles.css"
],
"scripts": [],
"jit": false,
"inlineStylesExtension": "css",
"fileReplacements": [],
"hasServer": false,
"skipTypeChecking": false
}
});
"
`);
});
it('should create a config file with configurations', () => {
const tree = createTreeWithEmptyWorkspace();
const opts = {
root: 'root',
index: 'src/index.html',
browser: 'src/main.ts',
};
const configurationOptions = {
production: {
index: 'src/index.prod.html',
browser: 'src/main.prod.ts',
},
};
createConfig(tree, opts, configurationOptions);
expect(tree.read('root/rspack.config.ts', 'utf-8')).toMatchInlineSnapshot(`
"
import { createConfig }from '@nx/angular-rspack';
export default createConfig({
options: {
root: __dirname,
"index": "src/index.html",
"browser": "src/main.ts"
}
}, {
production: {
options: {
"index": "src/index.prod.html",
"browser": "src/main.prod.ts"
}
}});
"
`);
});
});

View File

@ -0,0 +1,62 @@
import { joinPathFragments, type Tree } from '@nx/devkit';
export function createConfig(
tree: Tree,
opts: Record<string, any>,
configurationOptions: Record<string, Record<string, any>> = {},
existingWebpackConfigPath?: string,
isExistingWebpackConfigFunction?: boolean
) {
const { root, ...createConfigOptions } = opts;
const hasConfigurations = Object.keys(configurationOptions).length > 0;
const expandedConfigurationOptions = hasConfigurations
? Object.entries(configurationOptions)
.map(([configurationName, configurationOptions]) => {
return `
${configurationName}: {
options: {
${JSON.stringify(configurationOptions, undefined, 2).slice(1, -1)}
}
}`;
})
.join(',\n')
: '';
const configContents = `
import { createConfig }from '@nx/angular-rspack';
${
existingWebpackConfigPath
? `import baseWebpackConfig from '${existingWebpackConfigPath}';
${
isExistingWebpackConfigFunction
? ''
: `import webpackMerge from 'webpack-merge';`
}`
: ''
}
${
existingWebpackConfigPath ? 'const baseConfig = ' : 'export default '
}createConfig({
options: {
root: __dirname,
${JSON.stringify(createConfigOptions, undefined, 2).slice(1, -1)}
}
}${hasConfigurations ? `, {${expandedConfigurationOptions}}` : ''});
${
existingWebpackConfigPath
? `
export default ${
isExistingWebpackConfigFunction
? `async function (env, argv) {
const oldConfig = await baseWebpackConfig;
const browserConfig = baseConfig[0];
return oldConfig(browserConfig);
}`
: 'webpackMerge(baseConfig[0], baseWebpackConfig);'
}
`
: ''
}
`;
tree.write(joinPathFragments(root, 'rspack.config.ts'), configContents);
}

View File

@ -0,0 +1,63 @@
import { convertWebpackConfigToUseNxModuleFederationPlugin } from './get-custom-webpack-config';
describe('convertconvertWebpackConfigToUseNxModuleFederationPlugin', () => {
it('should convert a basic webpack config to use Nx Module Federation Plugin', () => {
// ARRANGE
const webpackConfigContents = `
import { withModuleFederation } from '@nx/module-federation/angular';
import config from './module-federation.config';
export default withModuleFederation(config, { dts: false });
`;
// ACT
const newWebpackConfigContents =
convertWebpackConfigToUseNxModuleFederationPlugin(webpackConfigContents);
// ASSERT
expect(newWebpackConfigContents).toMatchInlineSnapshot(`
"
import { NxModuleFederationPlugin } from '@nx/module-federation/rspack';
import config from './module-federation.config';
export default {
plugins: [
new NxModuleFederationPlugin(config, {
dts: false,
}),
]
}
"
`);
});
it('should convert a basic cjs webpack config to use Nx Module Federation Plugin', () => {
// ARRANGE
const webpackConfigContents = `
const { withModuleFederation } = require('@nx/module-federation/angular');
const config = require('./module-federation.config');
module.exports = withModuleFederation(config, { dts: false });
`;
// ACT
const newWebpackConfigContents =
convertWebpackConfigToUseNxModuleFederationPlugin(webpackConfigContents);
// ASSERT
expect(newWebpackConfigContents).toMatchInlineSnapshot(`
"
const { NxModuleFederationPlugin } = require('@nx/module-federation/rspack');
const config = require('./module-federation.config');
module.exports = {
plugins: [
new NxModuleFederationPlugin({ config }, {
dts: false,
}),
]
}
"
`);
});
});

View File

@ -0,0 +1,135 @@
import { logger, Tree } from '@nx/devkit';
import { loadConfigFile } from '@nx/devkit/src/utils/config-utils';
import { join, relative } from 'path';
import { tsquery } from '@phenomnomnominal/tsquery';
const FILE_EXTENSION_REGEX = /\.[^.]+$/;
export async function getCustomWebpackConfig(
tree: Tree,
projectRoot: string,
pathToCustomWebpackConfig: string
) {
const webpackConfigContents = tree.read(pathToCustomWebpackConfig, 'utf-8');
if (
webpackConfigContents.includes('@nx/module-federation/angular') &&
webpackConfigContents.includes('withModuleFederation')
) {
tree.write(
pathToCustomWebpackConfig,
convertWebpackConfigToUseNxModuleFederationPlugin(webpackConfigContents)
);
return {
isWebpackConfigFunction: false,
normalizedPathToCustomWebpackConfig: `./${relative(
projectRoot,
pathToCustomWebpackConfig
).replace(FILE_EXTENSION_REGEX, '')}`,
};
}
const configFile = await loadConfigFile(
join(tree.root, pathToCustomWebpackConfig)
);
const webpackConfig =
'default' in configFile ? configFile.default : configFile;
return {
isWebpackConfigFunction: typeof webpackConfig === 'function',
normalizedPathToCustomWebpackConfig: `./${relative(
projectRoot,
pathToCustomWebpackConfig
).replace(FILE_EXTENSION_REGEX, '')}`,
};
}
export function convertWebpackConfigToUseNxModuleFederationPlugin(
webpackConfigContents: string
): string {
let newWebpackConfigContents = webpackConfigContents;
let ast = tsquery.ast(webpackConfigContents);
const withModuleFederationImportNodes = tsquery(
ast,
'ImportDeclaration:has(StringLiteral[value=@nx/module-federation/angular])'
);
if (withModuleFederationImportNodes.length > 0) {
const withModuleFederationImportNode = withModuleFederationImportNodes[0];
newWebpackConfigContents = `${webpackConfigContents.slice(
0,
withModuleFederationImportNode.getStart()
)}import { NxModuleFederationPlugin } from '@nx/module-federation/rspack';${webpackConfigContents.slice(
withModuleFederationImportNode.getEnd()
)}`;
ast = tsquery.ast(newWebpackConfigContents);
const exportedWithModuleFederationNodes = tsquery(
ast,
'ExportAssignment:has(CallExpression > Identifier[name=withModuleFederation])'
);
if (exportedWithModuleFederationNodes.length > 0) {
const exportedWithModuleFederationNode =
exportedWithModuleFederationNodes[0];
newWebpackConfigContents = `${newWebpackConfigContents.slice(
0,
exportedWithModuleFederationNode.getStart()
)}${newWebpackConfigContents.slice(
exportedWithModuleFederationNode.getEnd()
)}
export default {
plugins: [
new NxModuleFederationPlugin(config, {
dts: false,
}),
]
}
`;
} else {
logger.warn(
"Could not find 'export default withModuleFederation' in the webpack config file. Skipping conversion."
);
}
}
const withModuleFederationRequireNodes = tsquery(
ast,
'VariableStatement:has(CallExpression > Identifier[name=withModuleFederation], StringLiteral[value=@nx/module-federation/angular])'
);
if (withModuleFederationRequireNodes.length > 0) {
const withModuleFederationRequireNode = withModuleFederationRequireNodes[0];
newWebpackConfigContents = `${webpackConfigContents.slice(
0,
withModuleFederationRequireNode.getStart()
)}const { NxModuleFederationPlugin } = require('@nx/module-federation/rspack');${webpackConfigContents.slice(
withModuleFederationRequireNode.getEnd()
)}`;
ast = tsquery.ast(newWebpackConfigContents);
const exportedWithModuleFederationNodes = tsquery(
ast,
'ExpressionStatement:has(BinaryExpression > PropertyAccessExpression:has(Identifier[name=module], Identifier[name=exports]), CallExpression:has(Identifier[name=withModuleFederation]))'
);
if (exportedWithModuleFederationNodes.length > 0) {
const exportedWithModuleFederationNode =
exportedWithModuleFederationNodes[0];
newWebpackConfigContents = `${newWebpackConfigContents.slice(
0,
exportedWithModuleFederationNode.getStart()
)}${newWebpackConfigContents.slice(
exportedWithModuleFederationNode.getEnd()
)}
module.exports = {
plugins: [
new NxModuleFederationPlugin({ config }, {
dts: false,
}),
]
}
`;
} else {
logger.warn(
"Could not find 'module.exports = withModuleFederation' in the webpack config file. Skipping conversion."
);
}
}
return newWebpackConfigContents;
}

View File

@ -0,0 +1,12 @@
import { joinPathFragments, readJson, Tree, writeJson } from '@nx/devkit';
export function updateTsconfig(tree: Tree, projectRoot: string) {
const tsconfigPath = joinPathFragments(projectRoot, 'tsconfig.json');
const tsconfig = readJson(tree, tsconfigPath);
tsconfig['ts-node'] = {
compilerOptions: {
module: 'CommonJS',
},
};
writeJson(tree, tsconfigPath, tsconfig);
}

View File

@ -0,0 +1,20 @@
import { TargetConfiguration } from '@nx/devkit';
const SUPPORTED_BUILD_EXECUTORS = [
'@angular-devkit/build-angular:browser',
'@nx/angular:webpack-browser',
];
export function validateSupportedBuildExecutor(targets: TargetConfiguration[]) {
const executorsUsedByProject = targets.map((target) => target.executor);
if (
!executorsUsedByProject.some((executor) =>
SUPPORTED_BUILD_EXECUTORS.includes(executor)
)
) {
throw new Error(
'The project does not use a supported build executor. Please use one of the following executors: ' +
SUPPORTED_BUILD_EXECUTORS.join(', ')
);
}
}

View File

@ -0,0 +1,5 @@
export interface ConvertToRspackSchema {
project: string;
skipFormat?: boolean;
skipInstall?: boolean;
}

View File

@ -0,0 +1,30 @@
{
"$schema": "https://json-schema.org/schema",
"$id": "GeneratorNxApp",
"title": "Creates an Angular application.",
"description": "Creates an Angular application.",
"type": "object",
"cli": "nx",
"properties": {
"project": {
"type": "string",
"aliases": ["name", "projectName"],
"description": "Project for which to convert to rspack.",
"$default": {
"$source": "argv",
"index": 0
},
"x-priority": "important"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"skipInstall": {
"description": "Skip installing dependencies.",
"type": "boolean",
"default": false
}
}
}

View File

@ -28,6 +28,7 @@ export const backwardCompatibleVersions: VersionMap = {
angularVersion: '~17.3.0',
angularDevkitVersion: '~17.3.0',
ngPackagrVersion: '~17.3.0',
angularRspackVersion: '~20.6.1',
ngrxVersion: '~17.0.0',
rxjsVersion: '~7.8.0',
zoneJsVersion: '~0.14.3',
@ -55,6 +56,7 @@ export const backwardCompatibleVersions: VersionMap = {
angularVersion: '~18.2.0',
angularDevkitVersion: '~18.2.0',
ngPackagrVersion: '~18.2.0',
angularRspackVersion: '~20.6.1',
ngrxVersion: '~18.0.2',
rxjsVersion: '~7.8.0',
zoneJsVersion: '~0.14.3',

View File

@ -3,6 +3,7 @@ export const nxVersion = require('../../package.json').version;
export const angularVersion = '~19.2.0';
export const angularDevkitVersion = '~19.2.0';
export const ngPackagrVersion = '~19.2.0';
export const angularRspackVersion = '~20.6.1';
export const ngrxVersion = '^19.0.0';
export const rxjsVersion = '~7.8.0';
export const zoneJsVersion = '~0.15.0';

35
pnpm-lock.yaml generated
View File

@ -171,7 +171,7 @@ importers:
version: 0.1902.0(chokidar@3.6.0)
'@angular-devkit/build-angular':
specifier: 19.2.0
version: 19.2.0(rggnxzdz2sowtz4b5zhppsknj4)
version: 19.2.0(35iavymh3xczxvipjxbozhxvvq)
'@angular-devkit/core':
specifier: 19.2.0
version: 19.2.0(chokidar@3.6.0)
@ -297,7 +297,7 @@ importers:
version: 3.13.2(rollup@4.22.0)(webpack-sources@3.2.3)
'@nx/angular':
specifier: 20.5.0-rc.4
version: 20.5.0-rc.4(6bzzb6mump3mw6fvztrlsbt2rq)
version: 20.5.0-rc.4(gup3gk2idig5qjq722krlimspe)
'@nx/conformance':
specifier: 1.3.0-beta.7
version: 1.3.0-beta.7(@nx/js@20.5.0-rc.4(@babel/traverse@7.26.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(nx@20.5.0-rc.4(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(typescript@5.7.3)(verdaccio@6.0.5(encoding@0.1.13)(typanion@3.14.0)))(nx@20.5.0-rc.4(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.11)))
@ -1384,6 +1384,17 @@ packages:
'@angular/platform-browser': 19.2.0
rxjs: ^6.5.3 || ^7.4.0
'@angular/ssr@19.2.0':
resolution: {integrity: sha512-2Tnunv0FDMxf5KHpxnxHhxQ5Xd+4qZ+fGBBbvH2njO5tDdFab6QxTRpKH9xWNiLjAqVIarytmTsuYtpchRmnaw==}
peerDependencies:
'@angular/common': ^19.0.0 || ^19.2.0-next.0
'@angular/core': ^19.0.0 || ^19.2.0-next.0
'@angular/platform-server': ^19.0.0 || ^19.2.0-next.0
'@angular/router': ^19.0.0 || ^19.2.0-next.0
peerDependenciesMeta:
'@angular/platform-server':
optional: true
'@antfu/utils@0.7.10':
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
@ -18457,13 +18468,13 @@ snapshots:
transitivePeerDependencies:
- chokidar
'@angular-devkit/build-angular@19.2.0(rggnxzdz2sowtz4b5zhppsknj4)':
'@angular-devkit/build-angular@19.2.0(35iavymh3xczxvipjxbozhxvvq)':
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.1902.0(chokidar@3.6.0)
'@angular-devkit/build-webpack': 0.1902.0(chokidar@3.6.0)(webpack-dev-server@5.2.0(bufferutil@4.0.7)(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))(webpack@5.98.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.25.0)(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))))(webpack@5.98.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.25.0)(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0)))
'@angular-devkit/core': 19.2.0(chokidar@3.6.0)
'@angular/build': 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10)))(typescript@5.7.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10)))(@types/node@20.16.10)(chokidar@3.6.0)(jiti@1.21.6)(less@4.2.2)(ng-packagr@19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10)))(typescript@5.7.3))(tailwindcss@3.4.4(ts-node@10.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(typescript@5.7.3)))(tslib@2.7.0)(typescript@5.7.3))(postcss@8.5.2)(sass-embedded@1.85.1)(stylus@0.64.0)(tailwindcss@3.4.4(ts-node@10.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(typescript@5.7.3)))(terser@5.39.0)(typescript@5.7.3)(yaml@2.6.1)
'@angular/build': 19.2.0(cxjrqumqlcnastohrhciqzxvse)
'@angular/compiler-cli': 19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10)))(typescript@5.7.3)
'@babel/core': 7.26.9
'@babel/generator': 7.26.9
@ -18517,6 +18528,7 @@ snapshots:
webpack-merge: 6.0.1
webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.5.0(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.25.0)(webpack-cli@5.1.4)))(webpack@5.98.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.25.0)(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0)))
optionalDependencies:
'@angular/ssr': 19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(@angular/router@19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(@angular/platform-browser@18.2.5(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10)))(rxjs@7.8.1))
esbuild: 0.25.0
jest: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(typescript@5.7.3))
jest-environment-jsdom: 29.7.0(bufferutil@4.0.7)
@ -18687,7 +18699,7 @@ snapshots:
eslint: 8.57.0
typescript: 5.7.3
'@angular/build@19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10)))(typescript@5.7.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10)))(@types/node@20.16.10)(chokidar@3.6.0)(jiti@1.21.6)(less@4.2.2)(ng-packagr@19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10)))(typescript@5.7.3))(tailwindcss@3.4.4(ts-node@10.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(typescript@5.7.3)))(tslib@2.7.0)(typescript@5.7.3))(postcss@8.5.2)(sass-embedded@1.85.1)(stylus@0.64.0)(tailwindcss@3.4.4(ts-node@10.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(typescript@5.7.3)))(terser@5.39.0)(typescript@5.7.3)(yaml@2.6.1)':
'@angular/build@19.2.0(cxjrqumqlcnastohrhciqzxvse)':
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.1902.0(chokidar@3.6.0)
@ -18719,6 +18731,7 @@ snapshots:
vite: 6.1.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.2.2)(sass-embedded@1.85.1)(sass@1.85.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1)
watchpack: 2.4.2
optionalDependencies:
'@angular/ssr': 19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(@angular/router@19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(@angular/platform-browser@18.2.5(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10)))(rxjs@7.8.1))
less: 4.2.2
lmdb: 3.2.6
ng-packagr: 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10)))(typescript@5.7.3))(tailwindcss@3.4.4(ts-node@10.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(typescript@5.7.3)))(tslib@2.7.0)(typescript@5.7.3)
@ -18809,6 +18822,14 @@ snapshots:
rxjs: 7.8.1
tslib: 2.8.1
'@angular/ssr@19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(@angular/router@19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(@angular/platform-browser@18.2.5(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10)))(rxjs@7.8.1))':
dependencies:
'@angular/common': 19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1)
'@angular/core': 19.2.0(rxjs@7.8.1)(zone.js@0.14.10)
'@angular/router': 19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(@angular/platform-browser@18.2.5(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.14.10)))(rxjs@7.8.1)
tslib: 2.8.1
optional: true
'@antfu/utils@0.7.10': {}
'@apidevtools/json-schema-ref-parser@9.0.9':
@ -23434,9 +23455,9 @@ snapshots:
transitivePeerDependencies:
- encoding
'@nx/angular@20.5.0-rc.4(6bzzb6mump3mw6fvztrlsbt2rq)':
'@nx/angular@20.5.0-rc.4(gup3gk2idig5qjq722krlimspe)':
dependencies:
'@angular-devkit/build-angular': 19.2.0(rggnxzdz2sowtz4b5zhppsknj4)
'@angular-devkit/build-angular': 19.2.0(35iavymh3xczxvipjxbozhxvvq)
'@angular-devkit/core': 19.2.0(chokidar@3.6.0)
'@angular-devkit/schematics': 19.2.0(chokidar@3.6.0)
'@nx/devkit': 20.5.0-rc.4(nx@20.5.0-rc.4(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.11)))