diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index df37cdf4f0..d5a9bcb542 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -9901,6 +9901,22 @@ "children": [], "isExternal": false, "disableCollapsible": false + }, + { + "id": "convert-config-to-rspack-plugin", + "path": "/nx-api/rspack/generators/convert-config-to-rspack-plugin", + "name": "convert-config-to-rspack-plugin", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, + { + "id": "convert-to-inferred", + "path": "/nx-api/rspack/generators/convert-to-inferred", + "name": "convert-to-inferred", + "children": [], + "isExternal": false, + "disableCollapsible": false } ], "isExternal": false, diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index c6fe5ba432..ac99acb732 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -3012,6 +3012,24 @@ "originalFilePath": "/packages/rspack/src/generators/convert-webpack/schema.json", "path": "/nx-api/rspack/generators/convert-webpack", "type": "generator" + }, + "/nx-api/rspack/generators/convert-config-to-rspack-plugin": { + "description": "Convert the project to use the `NxAppRspackPlugin` and `NxReactRspackPlugin`.", + "file": "generated/packages/rspack/generators/convert-config-to-rspack-plugin.json", + "hidden": false, + "name": "convert-config-to-rspack-plugin", + "originalFilePath": "/packages/rspack/src/generators/convert-config-to-rspack-plugin/schema.json", + "path": "/nx-api/rspack/generators/convert-config-to-rspack-plugin", + "type": "generator" + }, + "/nx-api/rspack/generators/convert-to-inferred": { + "description": "Convert existing Rspack project(s) using `@nx/rspack:rspack` executor to use `@nx/rspack/plugin`.", + "file": "generated/packages/rspack/generators/convert-to-inferred.json", + "hidden": false, + "name": "convert-to-inferred", + "originalFilePath": "/packages/rspack/src/generators/convert-to-inferred/schema.json", + "path": "/nx-api/rspack/generators/convert-to-inferred", + "type": "generator" } }, "path": "/nx-api/rspack" diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index 0f8d2dceca..04e75a49bb 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -2981,6 +2981,24 @@ "originalFilePath": "/packages/rspack/src/generators/convert-webpack/schema.json", "path": "rspack/generators/convert-webpack", "type": "generator" + }, + { + "description": "Convert the project to use the `NxAppRspackPlugin` and `NxReactRspackPlugin`.", + "file": "generated/packages/rspack/generators/convert-config-to-rspack-plugin.json", + "hidden": false, + "name": "convert-config-to-rspack-plugin", + "originalFilePath": "/packages/rspack/src/generators/convert-config-to-rspack-plugin/schema.json", + "path": "rspack/generators/convert-config-to-rspack-plugin", + "type": "generator" + }, + { + "description": "Convert existing Rspack project(s) using `@nx/rspack:rspack` executor to use `@nx/rspack/plugin`.", + "file": "generated/packages/rspack/generators/convert-to-inferred.json", + "hidden": false, + "name": "convert-to-inferred", + "originalFilePath": "/packages/rspack/src/generators/convert-to-inferred/schema.json", + "path": "rspack/generators/convert-to-inferred", + "type": "generator" } ], "githubRoot": "https://github.com/nrwl/nx/blob/master", diff --git a/docs/generated/packages/rspack/generators/convert-config-to-rspack-plugin.json b/docs/generated/packages/rspack/generators/convert-config-to-rspack-plugin.json new file mode 100644 index 0000000000..3e3b596fce --- /dev/null +++ b/docs/generated/packages/rspack/generators/convert-config-to-rspack-plugin.json @@ -0,0 +1,30 @@ +{ + "name": "convert-config-to-rspack-plugin", + "factory": "./src/generators/convert-config-to-rspack-plugin/convert-config-to-rspack-plugin", + "schema": { + "$schema": "https://json-schema.org/schema", + "$id": "NxRspackConvertConfigToRspackPlugin", + "description": "Convert existing Rspack project(s) using `@nx/rspack:rspack` executor that uses `withNx` to use `NxAppRspackPlugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "title": "Convert Rspack project using withNx to NxAppRspackPlugin", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The project to convert from using the `@nx/rspack:rspack` executor and `withNx` plugin to use `NxAppRspackPlugin`.", + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files at the end of the migration.", + "default": false + } + }, + "presets": [] + }, + "description": "Convert the project to use the `NxAppRspackPlugin` and `NxReactRspackPlugin`.", + "implementation": "/packages/rspack/src/generators/convert-config-to-rspack-plugin/convert-config-to-rspack-plugin.ts", + "aliases": [], + "hidden": false, + "path": "/packages/rspack/src/generators/convert-config-to-rspack-plugin/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/rspack/generators/convert-to-inferred.json b/docs/generated/packages/rspack/generators/convert-to-inferred.json new file mode 100644 index 0000000000..d31da11e09 --- /dev/null +++ b/docs/generated/packages/rspack/generators/convert-to-inferred.json @@ -0,0 +1,30 @@ +{ + "name": "convert-to-inferred", + "factory": "./src/generators/convert-to-inferred/convert-to-inferred#convertToInferred", + "schema": { + "$schema": "https://json-schema.org/schema", + "$id": "NxWebpackConvertToInferred", + "description": "Convert existing Webpack project(s) using `@nx/webpack:wepack` executor to use `@nx/webpack/plugin`.", + "title": "Convert a Webpack project from executor to plugin", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The project to convert from using the `@nx/webpack:webpack` executor to use `@nx/webpack/plugin`. If not provided, all projects using the `@nx/webpack:webpack` executor will be converted.", + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files.", + "default": false + } + }, + "presets": [] + }, + "description": "Convert existing Rspack project(s) using `@nx/rspack:rspack` executor to use `@nx/rspack/plugin`.", + "implementation": "/packages/rspack/src/generators/convert-to-inferred/convert-to-inferred#convertToInferred.ts", + "aliases": [], + "hidden": false, + "path": "/packages/rspack/src/generators/convert-to-inferred/schema.json", + "type": "generator" +} diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index 48b8c9aa58..86290fa318 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -697,6 +697,8 @@ - [preset](/nx-api/rspack/generators/preset) - [application](/nx-api/rspack/generators/application) - [convert-webpack](/nx-api/rspack/generators/convert-webpack) + - [convert-config-to-rspack-plugin](/nx-api/rspack/generators/convert-config-to-rspack-plugin) + - [convert-to-inferred](/nx-api/rspack/generators/convert-to-inferred) - [storybook](/nx-api/storybook) - [documents](/nx-api/storybook/documents) - [Overview](/nx-api/storybook/documents/overview) diff --git a/package.json b/package.json index 5c003eea3e..7e1a5dd262 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@nx/vite": "20.2.0-beta.2", "@nx/web": "20.2.0-beta.2", "@nx/webpack": "20.2.0-beta.2", + "@nx/rspack": "20.2.0-beta.2", "@phenomnomnominal/tsquery": "~5.0.1", "@playwright/test": "^1.36.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", diff --git a/packages/rspack/generators.json b/packages/rspack/generators.json index d89b9a265e..06098320c0 100644 --- a/packages/rspack/generators.json +++ b/packages/rspack/generators.json @@ -31,6 +31,16 @@ "factory": "./src/generators/convert-webpack/convert-webpack", "schema": "./src/generators/convert-webpack/schema.json", "description": "Convert a webpack application to use rspack." + }, + "convert-config-to-rspack-plugin": { + "factory": "./src/generators/convert-config-to-rspack-plugin/convert-config-to-rspack-plugin", + "schema": "./src/generators/convert-config-to-rspack-plugin/schema.json", + "description": "Convert the project to use the `NxAppRspackPlugin` and `NxReactRspackPlugin`." + }, + "convert-to-inferred": { + "factory": "./src/generators/convert-to-inferred/convert-to-inferred#convertToInferred", + "schema": "./src/generators/convert-to-inferred/schema.json", + "description": "Convert existing Rspack project(s) using `@nx/rspack:rspack` executor to use `@nx/rspack/plugin`." } } } diff --git a/packages/rspack/src/executors/rspack/schema.d.ts b/packages/rspack/src/executors/rspack/schema.d.ts index 10dcd8c545..286f42c82b 100644 --- a/packages/rspack/src/executors/rspack/schema.d.ts +++ b/packages/rspack/src/executors/rspack/schema.d.ts @@ -28,7 +28,7 @@ export interface RspackExecutorSchema { progress?: boolean; publicPath?: string; rebaseRootRelative?: boolean; - rspackConfig: string; + rspackConfig?: string; runtimeChunk?: boolean; scripts?: Array; skipTypeChecking?: boolean; diff --git a/packages/rspack/src/generators/convert-config-to-rspack-plugin/convert-config-to-rspack-plugin.spec.ts b/packages/rspack/src/generators/convert-config-to-rspack-plugin/convert-config-to-rspack-plugin.spec.ts new file mode 100644 index 0000000000..f8e4434323 --- /dev/null +++ b/packages/rspack/src/generators/convert-config-to-rspack-plugin/convert-config-to-rspack-plugin.spec.ts @@ -0,0 +1,437 @@ +import { + ProjectConfiguration, + Tree, + addProjectConfiguration, +} from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import convertConfigToRspackPluginGenerator from './convert-config-to-rspack-plugin'; + +interface CreateProjectOptions { + name: string; + root: string; + targetName: string; + targetOptions: Record; + additionalTargets?: Record; +} + +const defaultOptions: CreateProjectOptions = { + name: 'my-app', + root: 'my-app', + targetName: 'build', + targetOptions: {}, +}; + +function createProject(tree: Tree, options: Partial) { + const projectOpts = { + ...defaultOptions, + ...options, + targetOptions: { + ...defaultOptions.targetOptions, + ...options?.targetOptions, + }, + }; + const project: ProjectConfiguration = { + name: projectOpts.name, + root: projectOpts.root, + targets: { + build: { + executor: '@nx/rspack:rspack', + options: { + rspackConfig: `${projectOpts.root}/rspack.config.js`, + ...projectOpts.targetOptions, + }, + }, + ...options.additionalTargets, + }, + }; + + addProjectConfiguration(tree, project.name, project); + + return project; +} + +describe('convertConfigToRspackPluginGenerator', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should migrate the rspack config of the specified project', async () => { + const project = createProject(tree, { + name: 'my-app', + root: 'my-app', + }); + + createProject(tree, { + name: 'another-app', + root: 'another-app', + }); + + tree.write( + 'another-app/rspack.config.js', + ` + const { composePlugins, withNx } = require('@nx/rspack'); + const { withReact } = require('@nx/rspack'); + + // Nx plugins for rspack. + module.exports = composePlugins( + withNx(), + withReact({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + (config) => { + return config; + } + ); + ` + ); + + tree.write( + `${project.name}/rspack.config.js`, + ` + const { composePlugins, withNx } = require('@nx/rspack'); + const { withReact } = require('@nx/rspack'); + + // Nx plugins for rspack. + module.exports = composePlugins( + withNx(), + withReact({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + (config) => { + return config; + } + ); + ` + ); + + await convertConfigToRspackPluginGenerator(tree, { + project: project.name, + }); + expect(tree.read(`${project.name}/rspack.config.js`, 'utf-8')) + .toMatchInlineSnapshot(` + "const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); + const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); + const { useLegacyNxPlugin } = require('@nx/rspack'); + + // This file was migrated using @nx/rspack:convert-config-to-rspack-plugin from your './rspack.config.old.js' + // Please check that the options here are correct as they were moved from the old rspack.config.js to this file. + const options = {}; + + /** + * @type{import('@rspack/core').RspackOptionsNormalized} + */ + module.exports = async () => ({ + plugins: [ + new NxAppRspackPlugin(), + new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + // NOTE: useLegacyNxPlugin ensures that the non-standard Rspack configuration file previously used still works. + // To remove its usage, move options such as "plugins" into this file as standard Rspack configuration options. + // To enhance configurations after Nx plugins have applied, you can add a new plugin with the \\\`apply\\\` method. + // e.g. \\\`{ apply: (compiler) => { /* modify compiler.options */ }\\\` + // eslint-disable-next-line react-hooks/rules-of-hooks + await useLegacyNxPlugin(require('./rspack.config.old'), options), + ], + }); + " + `); + + expect(tree.read(`${project.name}/rspack.config.old.js`, 'utf-8')) + .toMatchInlineSnapshot(` + "const { composePlugins } = require('@nx/rspack'); + // Nx plugins for rspack. + module.exports = composePlugins((config) => { + return config; + }); + " + `); + + expect(tree.read(`another-app/rspack.config.js`, 'utf-8')) + .toMatchInlineSnapshot(` + "const { composePlugins, withNx } = require('@nx/rspack'); + const { withReact } = require('@nx/rspack'); + + // Nx plugins for rspack. + module.exports = composePlugins( + withNx(), + withReact({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + (config) => { + return config; + } + ); + " + `); + + expect(tree.exists(`${project.name}/rspack.config.old.js`)).toBe(true); + expect(tree.exists(`another-app/rspack.config.old.js`)).toBe(false); + }); + + it('should update project.json adding the standardRspackConfigFunction option', async () => { + const project = createProject(tree, { + name: 'my-app', + root: 'my-app', + }); + + tree.write( + `${project.name}/rspack.config.js`, + ` + const { composePlugins, withNx } = require('@nx/rspack'); + const { withReact } = require('@nx/rspack'); + + // Nx plugins for rspack. + module.exports = composePlugins( + withNx(), + withReact({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + (config) => { + return config; + } + ); + ` + ); + + await convertConfigToRspackPluginGenerator(tree, { + project: project.name, + }); + + expect(tree.read(`${project.name}/project.json`, 'utf-8')) + .toMatchInlineSnapshot(` + "{ + "name": "my-app", + "$schema": "../node_modules/nx/schemas/project-schema.json", + "targets": { + "build": { + "executor": "@nx/rspack:rspack", + "options": { + "rspackConfig": "my-app/rspack.config.js", + "standardRspackConfigFunction": true + } + } + } + } + " + `); + }); + + it('should throw an error if no projects are found', async () => { + const project = createProject(tree, { + name: 'my-app', + root: 'my-app', + }); + + await expect( + convertConfigToRspackPluginGenerator(tree, { + project: project.name, + }) + ).rejects.toThrowError('Could not find any projects to migrate.'); + }); + + it('should not migrate a rspack config that does not use withNx', async () => { + const project = createProject(tree, { + name: 'my-app', + root: 'my-app', + }); + + tree.write(`${project.name}/rspack.config.js`, `module.exports = {};`); + + await expect( + convertConfigToRspackPluginGenerator(tree, { + project: project.name, + }) + ).rejects.toThrowError('Could not find any projects to migrate.'); + + expect( + tree.read(`${project.name}/rspack.config.js`, 'utf-8') + ).toMatchInlineSnapshot(`"module.exports = {};"`); + }); + + it('should throw an error if the project is using Module federation', async () => { + const project = createProject(tree, { + name: 'my-app', + root: 'my-app', + additionalTargets: { + serve: { + executor: '@nx/rspack:module-federation-dev-server', + options: { + buildTarget: 'my-app:build', + }, + }, + }, + }); + + await expect( + convertConfigToRspackPluginGenerator(tree, { project: project.name }) + ).rejects.toThrowError( + `The project ${project.name} is using Module Federation. At the moment, we don't support migrating projects that use Module Federation.` + ); + }); + + it('should throw an error if the project is a Nest project', async () => { + const project = createProject(tree, { + name: 'my-app', + root: 'my-app', + additionalTargets: { + serve: { + executor: '@nx/js:node', + options: { + buildTarget: 'my-app:build', + }, + }, + }, + }); + + await expect( + convertConfigToRspackPluginGenerator(tree, { project: project.name }) + ).rejects.toThrowError( + `The project ${project.name} is using the '@nx/js:node' executor. At the moment, we do not support migrating such projects.` + ); + }); + + it('should not migrate a rspack config that is already using NxAppRspackPlugin', async () => { + const project = createProject(tree, { + name: 'my-app', + root: 'my-app', + }); + + tree.write( + `${project.name}/rspack.config.js`, + ` + const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); + + module.exports = { + plugins: [ + new NxAppRspackPlugin(), + ], + }; + ` + ); + + await expect( + convertConfigToRspackPluginGenerator(tree, { project: project.name }) + ).rejects.toThrowError(`Could not find any projects to migrate.`); + expect(tree.read(`${project.name}/rspack.config.js`, 'utf-8')) + .toMatchInlineSnapshot(` + " + const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); + + module.exports = { + plugins: [ + new NxAppRspackPlugin(), + ], + }; + " + `); + expect(tree.exists(`${project.name}/rspack.config.old.js`)).toBe(false); + }); + + it('should convert absolute options paths to relative paths during the conversion', async () => { + const project = createProject(tree, { + name: 'my-app', + root: 'apps/my-app', + }); + + tree.write( + `${project.root}/rspack.config.js`, + ` + const { composePlugins, withNx } = require('@nx/rspack'); + const { withReact } = require('@nx/rspack'); + + // Nx plugins for rspack. + module.exports = composePlugins( + withNx({ + assets: ["apps/${project.name}/src/favicon.ico","apps/${project.name}/src/assets"], + styles: ["apps/${project.name}/src/styles.scss"], + scripts: ["apps/${project.name}/src/scripts.js"], + tsConfig: "apps/${project.name}/tsconfig.app.json", + fileReplacements: [ + { + replace: "apps/${project.name}/src/environments/environment.ts", + with: "apps/${project.name}/src/environments/environment.prod.ts" + } + ], + additionalEntryPoints: [ + { + entryPath: "apps/${project.name}/src/polyfills.ts", + } + ] + }), + withReact({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + (config) => { + return config; + } + ); + ` + ); + + await convertConfigToRspackPluginGenerator(tree, { + project: project.name, + }); + expect(tree.read(`${project.root}/rspack.config.js`, 'utf-8')) + .toMatchInlineSnapshot(` + "const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); + const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); + const { useLegacyNxPlugin } = require('@nx/rspack'); + + // This file was migrated using @nx/rspack:convert-config-to-rspack-plugin from your './rspack.config.old.js' + // Please check that the options here are correct as they were moved from the old rspack.config.js to this file. + const options = { + assets: ['./src/favicon.ico', './src/assets'], + styles: ['./src/styles.scss'], + scripts: ['./src/scripts.js'], + tsConfig: './tsconfig.app.json', + fileReplacements: [ + { + replace: './src/environments/environment.ts', + with: './src/environments/environment.prod.ts', + }, + ], + additionalEntryPoints: [ + { + entryPath: './src/polyfills.ts', + }, + ], + }; + + /** + * @type{import('@rspack/core').RspackOptionsNormalized} + */ + module.exports = async () => ({ + plugins: [ + new NxAppRspackPlugin(options), + new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + // NOTE: useLegacyNxPlugin ensures that the non-standard Rspack configuration file previously used still works. + // To remove its usage, move options such as "plugins" into this file as standard Rspack configuration options. + // To enhance configurations after Nx plugins have applied, you can add a new plugin with the \\\`apply\\\` method. + // e.g. \\\`{ apply: (compiler) => { /* modify compiler.options */ }\\\` + // eslint-disable-next-line react-hooks/rules-of-hooks + await useLegacyNxPlugin(require('./rspack.config.old'), options), + ], + }); + " + `); + }); +}); diff --git a/packages/rspack/src/generators/convert-config-to-rspack-plugin/convert-config-to-rspack-plugin.ts b/packages/rspack/src/generators/convert-config-to-rspack-plugin/convert-config-to-rspack-plugin.ts new file mode 100644 index 0000000000..ed64851a2c --- /dev/null +++ b/packages/rspack/src/generators/convert-config-to-rspack-plugin/convert-config-to-rspack-plugin.ts @@ -0,0 +1,139 @@ +import { + formatFiles, + getProjects, + stripIndents, + Tree, + joinPathFragments, + updateProjectConfiguration, + ProjectConfiguration, +} from '@nx/devkit'; +import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils'; +import { RspackExecutorSchema } from '../../executors/rspack/schema'; +import { extractRspackOptions } from './lib/extract-rspack-options'; +import { normalizePathOptions } from './lib/normalize-path-options'; +import { parse } from 'path'; +import { validateProject } from './lib/validate-project'; + +interface Schema { + project?: string; + skipFormat?: boolean; +} + +// Make text JSON compatible +const preprocessText = (text: string) => { + return text + .replace(/(\w+):/g, '"$1":') // Quote property names + .replace(/'/g, '"') // Convert single quotes to double quotes + .replace(/,(\s*[}\]])/g, '$1') // Remove trailing commas + .replace(/(\r\n|\n|\r|\t)/gm, ''); // Remove newlines and tabs +}; + +export async function convertConfigToRspackPluginGenerator( + tree: Tree, + options: Schema +) { + let migrated = 0; + + const projects = getProjects(tree); + forEachExecutorOptions( + tree, + '@nx/rspack:rspack', + (currentTargetOptions, projectName, targetName, configurationName) => { + if (options.project && projectName !== options.project) { + return; + } + if (!configurationName) { + const project = projects.get(projectName); + const target = project.targets[targetName]; + + const hasError = validateProject(tree, project); + if (hasError) { + throw new Error(hasError); + } + + const rspackConfigPath = currentTargetOptions?.rspackConfig || ''; + + if (rspackConfigPath && tree.exists(rspackConfigPath)) { + let { withNxConfig: rspackOptions, withReactConfig } = + extractRspackOptions(tree, rspackConfigPath); + + // if rspackOptions === undefined + // withNx was not found in the rspack.config.js file so we should skip this project + if (rspackOptions !== undefined) { + let parsedOptions = {}; + if (rspackOptions) { + parsedOptions = JSON.parse( + preprocessText(rspackOptions.getText()) + ); + parsedOptions = normalizePathOptions(project.root, parsedOptions); + } + + target.options.standardRspackConfigFunction = true; + + updateProjectConfiguration(tree, projectName, project); + + const { dir, name, ext } = parse(rspackConfigPath); + + tree.rename( + rspackConfigPath, + `${joinPathFragments(dir, `${name}.old${ext}`)}` + ); + + tree.write( + rspackConfigPath, + stripIndents` + const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); + const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); + const { useLegacyNxPlugin } = require('@nx/rspack'); + + // This file was migrated using @nx/rspack:convert-config-to-rspack-plugin from your './rspack.config.old.js' + // Please check that the options here are correct as they were moved from the old rspack.config.js to this file. + const options = ${ + rspackOptions ? JSON.stringify(parsedOptions, null, 2) : '{}' + }; + + /** + * @type{import('@rspack/core').RspackOptionsNormalized} + */ + module.exports = async () => ({ + plugins: [ + ${ + rspackOptions + ? 'new NxAppRspackPlugin(options)' + : 'new NxAppRspackPlugin()' + }, + ${ + withReactConfig + ? `new NxReactRspackPlugin(${withReactConfig.getText()})` + : `new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + })` + }, + // NOTE: useLegacyNxPlugin ensures that the non-standard Rspack configuration file previously used still works. + // To remove its usage, move options such as "plugins" into this file as standard Rspack configuration options. + // To enhance configurations after Nx plugins have applied, you can add a new plugin with the \`apply\` method. + // e.g. \`{ apply: (compiler) => { /* modify compiler.options */ }\` + // eslint-disable-next-line react-hooks/rules-of-hooks + await useLegacyNxPlugin(require('./rspack.config.old'), options), + ], + }); + ` + ); + migrated++; + } + } + } + } + ); + if (migrated === 0) { + throw new Error('Could not find any projects to migrate.'); + } + + if (!options.skipFormat) { + await formatFiles(tree); + } +} + +export default convertConfigToRspackPluginGenerator; diff --git a/packages/rspack/src/generators/convert-config-to-rspack-plugin/lib/extract-rspack-options.ts b/packages/rspack/src/generators/convert-config-to-rspack-plugin/lib/extract-rspack-options.ts new file mode 100644 index 0000000000..6d4a5603ed --- /dev/null +++ b/packages/rspack/src/generators/convert-config-to-rspack-plugin/lib/extract-rspack-options.ts @@ -0,0 +1,176 @@ +import { Tree } from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import * as ts from 'typescript'; + +export function extractRspackOptions(tree: Tree, rspackConfigPath: string) { + const source = tree.read(rspackConfigPath).toString('utf-8'); + const ast = tsquery.ast(source); + + const withNxQuery = 'CallExpression:has(Identifier[name="withNx"])'; + const withReactQuery = 'CallExpression:has(Identifier[name="withReact"])'; + const withWebQuery = 'CallExpression:has(Identifier[name="withWeb"])'; + + const withNxCall = tsquery(ast, withNxQuery) as ts.CallExpression[]; + + const withReactCall = tsquery(ast, withReactQuery) as ts.CallExpression[]; + + const withWebCall = tsquery(ast, withWebQuery) as ts.CallExpression[]; + + // If the config is empty set to empty string to avoid undefined. Undefined is used to check if the withNx exists inside of the config file. + let withNxConfig: ts.Node | '' | undefined, + withReactConfig: ts.Node | '' | undefined; + + withWebCall.forEach((node) => { + const argument = node.arguments[0] || ''; + withNxConfig = argument; // Since withWeb and withNx use the same config object and both should not exist in the same file, we can reuse the withNxConfig variable. + }); + + withNxCall.forEach((node) => { + const argument = node.arguments[0] || ''; // The first argument is the config object + withNxConfig = argument; + }); + + withReactCall.forEach((node) => { + const argument = node.arguments[0] || ''; + withReactConfig = argument; + }); + + if (withNxConfig !== undefined) { + // Only remove the withNx and withReact calls if they exist + let updatedSource = removeCallExpressions(source, [ + 'withNx', + 'withReact', + 'withWeb', + ]); + updatedSource = removeImportDeclarations( + updatedSource, + 'withNx', + '@nx/rspack' + ); + updatedSource = removeImportDeclarations( + updatedSource, + 'withWeb', + '@nx/rspack' + ); + updatedSource = removeImportDeclarations( + updatedSource, + 'withReact', + '@nx/rspack' + ); + + tree.write(rspackConfigPath, updatedSource); + } + + return { withNxConfig, withReactConfig }; +} + +function removeCallExpressions( + source: string, + functionNames: string[] +): string { + let modifiedSource = source; + functionNames.forEach((functionName) => { + const callExpressionQuery = `CallExpression:has(Identifier[name="composePlugins"]) > CallExpression:has(Identifier[name="${functionName}"])`; + + modifiedSource = tsquery.replace( + modifiedSource, + callExpressionQuery, + () => { + return ''; // Removes the entire CallExpression + } + ); + }); + + return modifiedSource; +} + +function removeImportDeclarations( + source: string, + importName: string, + moduleName: string +) { + const sourceFile = tsquery.ast(source); + + const modifiedStatements = sourceFile.statements + .map((statement) => { + if (!ts.isVariableStatement(statement)) return statement; + + const declarationList = statement.declarationList; + const newDeclarations = declarationList.declarations + .map((declaration) => { + if ( + !ts.isVariableDeclaration(declaration) || + !declaration.initializer + ) + return declaration; + + if ( + ts.isCallExpression(declaration.initializer) && + ts.isIdentifier(declaration.initializer.expression) + ) { + const callExpr = declaration.initializer.expression; + if ( + callExpr.text === 'require' && + declaration.initializer.arguments[0] + ?.getText() + .replace(/['"]/g, '') === moduleName + ) { + if (ts.isObjectBindingPattern(declaration.name)) { + const bindingElements = declaration.name.elements.filter( + (element) => { + const elementName = element.name.getText(); + return elementName !== importName; + } + ); + + if (bindingElements.length > 0) { + const newBindingPattern = + ts.factory.updateObjectBindingPattern( + declaration.name, + bindingElements + ); + + // Update the variable declaration with the new binding pattern without the specified import name + return ts.factory.updateVariableDeclaration( + declaration, + newBindingPattern, + declaration.exclamationToken, + declaration.type, + declaration.initializer + ); + } else { + return null; // Remove this declaration entirely if no bindings remain + } + } + } + } + return declaration; + }) + .filter(Boolean); + + if (newDeclarations.length > 0) { + const newDeclarationList = ts.factory.updateVariableDeclarationList( + declarationList, + newDeclarations as ts.VariableDeclaration[] + ); + return ts.factory.updateVariableStatement( + statement, + statement.modifiers, + newDeclarationList + ); + } else { + return null; // Remove the entire statement + } + }) + .filter(Boolean); + + // Use printer to format the source code and rewrite the modified + const newSourceFile = ts.factory.updateSourceFile( + sourceFile, + modifiedStatements as ts.Statement[] + ); + const printer = ts.createPrinter(); + const formattedSource = printer.printFile(newSourceFile); + + return formattedSource; +} diff --git a/packages/rspack/src/generators/convert-config-to-rspack-plugin/lib/normalize-path-options.ts b/packages/rspack/src/generators/convert-config-to-rspack-plugin/lib/normalize-path-options.ts new file mode 100644 index 0000000000..aaf8cd93d3 --- /dev/null +++ b/packages/rspack/src/generators/convert-config-to-rspack-plugin/lib/normalize-path-options.ts @@ -0,0 +1,91 @@ +import { RspackExecutorSchema } from '../../../executors/rspack/schema'; +import { toProjectRelativePath } from './utils'; + +const executorFieldsToNormalize: Array = [ + 'outputPath', + 'index', + 'main', + 'assets', + 'tsConfig', + 'styles', + 'additionalEntryPoints', + 'scripts', + 'fileReplacements', + 'postcssConfig', + 'stylePreprocessorOptions', + 'publicPath', +]; + +export function normalizePathOptions( + projectRoot: string, + options: Partial +) { + for (const [key, value] of Object.entries(options)) { + if ( + !executorFieldsToNormalize.includes(key as keyof RspackExecutorSchema) + ) { + continue; + } + options[key] = normalizePath( + projectRoot, + key as keyof RspackExecutorSchema, + value + ); + } + return options; +} + +function normalizePath( + projectRoot: string, + key: K, + value: RspackExecutorSchema[K] +) { + if (!value) return value; + + switch (key) { + case 'assets': + return value.map((asset) => { + if (typeof asset === 'string') { + return toProjectRelativePath(asset, projectRoot); + } + return { + ...asset, + input: toProjectRelativePath(asset.input, projectRoot), + output: toProjectRelativePath(asset.output, projectRoot), + }; + }); + + case 'styles': + case 'scripts': + return value.map((item) => { + if (typeof item === 'string') { + return toProjectRelativePath(item, projectRoot); + } + return { + ...item, + input: toProjectRelativePath(item.input, projectRoot), + }; + }); + + case 'additionalEntryPoints': + return value.map((entry) => { + return { + ...entry, + entryPath: toProjectRelativePath(entry.entryPath, projectRoot), + }; + }); + + case 'fileReplacements': + return value.map((replacement) => { + return { + replace: toProjectRelativePath(replacement.replace, projectRoot), + with: toProjectRelativePath(replacement.with, projectRoot), + }; + }); + + default: + return Array.isArray(value) + ? value.map((item) => toProjectRelativePath(item, projectRoot)) + : toProjectRelativePath(value, projectRoot); + } +} diff --git a/packages/rspack/src/generators/convert-config-to-rspack-plugin/lib/utils.ts b/packages/rspack/src/generators/convert-config-to-rspack-plugin/lib/utils.ts new file mode 100644 index 0000000000..701daa4629 --- /dev/null +++ b/packages/rspack/src/generators/convert-config-to-rspack-plugin/lib/utils.ts @@ -0,0 +1,19 @@ +import { relative, resolve } from 'path/posix'; +import { workspaceRoot } from '@nx/devkit'; + +export function toProjectRelativePath( + path: string, + projectRoot: string +): string { + if (projectRoot === '.') { + // workspace and project root are the same, we normalize it to ensure it + return path.startsWith('.') ? path : `./${path}`; + } + + const relativePath = relative( + resolve(workspaceRoot, projectRoot), + resolve(workspaceRoot, path) + ); + + return relativePath.startsWith('.') ? relativePath : `./${relativePath}`; +} diff --git a/packages/rspack/src/generators/convert-config-to-rspack-plugin/lib/validate-project.ts b/packages/rspack/src/generators/convert-config-to-rspack-plugin/lib/validate-project.ts new file mode 100644 index 0000000000..d1a1c6c445 --- /dev/null +++ b/packages/rspack/src/generators/convert-config-to-rspack-plugin/lib/validate-project.ts @@ -0,0 +1,47 @@ +import { ProjectConfiguration, Tree } from '@nx/devkit'; + +function hasAnotherRspackConfig(tree: Tree, projectRoot: string) { + const files = tree.children(projectRoot); + const projectJsonString = tree.read(`${projectRoot}/project.json`, 'utf-8'); + for (const file of files) { + if ( + file !== 'rspack.config.js' && + file.endsWith('.js') && + file.includes('rspack.config') && + projectJsonString.includes(file) && + tree.exists(`${projectRoot}/rspack.config.js`) + ) { + return 'Cannot convert a project with multiple rspack config files. Please consolidate them into a single rspack.config.js file.'; + } + } +} + +function isNestProject(project: ProjectConfiguration) { + for (const target in project.targets) { + if (project.targets[target].executor === '@nx/js:node') { + return `The project ${project.name} is using the '@nx/js:node' executor. At the moment, we do not support migrating such projects.`; + } + } +} + +/** + * Validates the project to ensure it can be migrated + * + * @param tree The virtual file system + * @param project the project configuration object for the project + * @returns A string if there is an error, otherwise undefined + */ +export function validateProject(tree: Tree, project: ProjectConfiguration) { + const containsMfeExecutor = Object.keys(project.targets).some((target) => { + return ['@nx/rspack:module-federation-dev-server'].includes( + project.targets[target].executor + ); + }); + + if (containsMfeExecutor) { + return `The project ${project.name} is using Module Federation. At the moment, we don't support migrating projects that use Module Federation.`; + } + + const hasAnotherConfig = hasAnotherRspackConfig(tree, project.root); + return hasAnotherConfig || isNestProject(project); +} diff --git a/packages/rspack/src/generators/convert-config-to-rspack-plugin/schema.json b/packages/rspack/src/generators/convert-config-to-rspack-plugin/schema.json new file mode 100644 index 0000000000..c40536bfa0 --- /dev/null +++ b/packages/rspack/src/generators/convert-config-to-rspack-plugin/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "NxRspackConvertConfigToRspackPlugin", + "description": "Convert existing Rspack project(s) using `@nx/rspack:rspack` executor that uses `withNx` to use `NxAppRspackPlugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "title": "Convert Rspack project using withNx to NxAppRspackPlugin", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The project to convert from using the `@nx/rspack:rspack` executor and `withNx` plugin to use `NxAppRspackPlugin`.", + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files at the end of the migration.", + "default": false + } + } +} diff --git a/packages/rspack/src/generators/convert-to-inferred/__snapshots__/convert-to-inferred.spec.ts.snap b/packages/rspack/src/generators/convert-to-inferred/__snapshots__/convert-to-inferred.spec.ts.snap new file mode 100644 index 0000000000..e9f0027a5f --- /dev/null +++ b/packages/rspack/src/generators/convert-to-inferred/__snapshots__/convert-to-inferred.spec.ts.snap @@ -0,0 +1,268 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`convert-to-inferred all projects should migrate all projects using the rspack executors 1`] = ` +"const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); +const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); +const { useLegacyNxPlugin } = require('@nx/rspack'); + +// These options were migrated by @nx/rspack:convert-to-inferred from +// the project.json file and merged with the options in this file +const configValues = { + build: { + default: { + outputPath: '../../dist/apps/app1', + index: './src/index.html', + main: './src/main.tsx', + tsConfig: './tsconfig.app.json', + assets: ['./src/favicon.ico', './src/assets'], + styles: ['./src/styles.scss'], + }, + development: { + extractLicenses: false, + optimization: false, + sourceMap: true, + vendorChunk: true, + }, + production: { + fileReplacements: [ + { + replace: './src/environments/environment.ts', + with: './src/environments/environment.prod.ts', + }, + ], + optimization: true, + outputHashing: 'all', + sourceMap: false, + namedChunks: false, + extractLicenses: true, + vendorChunk: false, + }, + }, + serve: { + default: { + hmr: true, + server: { + type: 'https', + options: { cert: './server.crt', key: './server.key' }, + }, + port: 4200, + headers: { 'Access-Control-Allow-Origin': '*' }, + historyApiFallback: { + index: '/index.html', + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + }, + }, + development: { open: true }, + production: { hmr: false }, + }, +}; + +// Determine the correct configValue to use based on the configuration +const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default'; + +const buildOptions = { + ...configValues.build.default, + ...configValues.build[configuration], +}; +const devServerOptions = { + ...configValues.serve.default, + ...configValues.serve[configuration], +}; + +/** + * @type{import('@rspack/core').RspackOptionsNormalized} + */ +module.exports = async () => ({ + devServer: devServerOptions, + plugins: [ + new NxAppRspackPlugin(buildOptions), + new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + // eslint-disable-next-line react-hooks/rules-of-hooks + await useLegacyNxPlugin(require('./rspack.config.old'), buildOptions), + ], +}); +" +`; + +exports[`convert-to-inferred all projects should migrate all projects using the rspack executors 2`] = ` +"const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); +const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); +const { useLegacyNxPlugin } = require('@nx/rspack'); + +// These options were migrated by @nx/rspack:convert-to-inferred from +// the project.json file and merged with the options in this file +const configValues = { + build: { + default: { + outputPath: '../../dist/apps/app2', + index: './src/index.html', + main: './src/main.tsx', + tsConfig: './tsconfig.app.json', + assets: ['./src/favicon.ico', './src/assets'], + styles: ['./src/styles.scss'], + }, + development: { + extractLicenses: false, + optimization: false, + sourceMap: true, + vendorChunk: true, + }, + production: { + fileReplacements: [ + { + replace: './src/environments/environment.ts', + with: './src/environments/environment.prod.ts', + }, + ], + optimization: true, + outputHashing: 'all', + sourceMap: false, + namedChunks: false, + extractLicenses: true, + vendorChunk: false, + }, + }, + serve: { + default: { + hmr: true, + server: { + type: 'https', + options: { cert: './server.crt', key: './server.key' }, + }, + port: 4200, + headers: { 'Access-Control-Allow-Origin': '*' }, + historyApiFallback: { + index: '/index.html', + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + }, + }, + development: { open: true }, + production: { hmr: false }, + }, +}; + +// Determine the correct configValue to use based on the configuration +const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default'; + +const buildOptions = { + ...configValues.build.default, + ...configValues.build[configuration], +}; +const devServerOptions = { + ...configValues.serve.default, + ...configValues.serve[configuration], +}; + +/** + * @type{import('@rspack/core').RspackOptionsNormalized} + */ +module.exports = async () => ({ + devServer: devServerOptions, + plugins: [ + new NxAppRspackPlugin(buildOptions), + new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + // eslint-disable-next-line react-hooks/rules-of-hooks + await useLegacyNxPlugin(require('./rspack.config.old'), buildOptions), + ], +}); +" +`; + +exports[`convert-to-inferred all projects should migrate all projects using the rspack executors 3`] = ` +"const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); +const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); +const { useLegacyNxPlugin } = require('@nx/rspack'); + +// These options were migrated by @nx/rspack:convert-to-inferred from +// the project.json file and merged with the options in this file +const configValues = { + build: { + default: { + outputPath: '../../dist/apps/app3', + index: './src/index.html', + main: './src/main.tsx', + tsConfig: './tsconfig.app.json', + assets: ['./src/favicon.ico', './src/assets'], + styles: ['./src/styles.scss'], + }, + development: { + extractLicenses: false, + optimization: false, + sourceMap: true, + vendorChunk: true, + }, + production: { + fileReplacements: [ + { + replace: './src/environments/environment.ts', + with: './src/environments/environment.prod.ts', + }, + ], + optimization: true, + outputHashing: 'all', + sourceMap: false, + namedChunks: false, + extractLicenses: true, + vendorChunk: false, + }, + }, + serve: { + default: { + hmr: true, + server: { + type: 'https', + options: { cert: './server.crt', key: './server.key' }, + }, + port: 4200, + headers: { 'Access-Control-Allow-Origin': '*' }, + historyApiFallback: { + index: '/index.html', + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + }, + }, + development: { open: true }, + production: { hmr: false }, + }, +}; + +// Determine the correct configValue to use based on the configuration +const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default'; + +const buildOptions = { + ...configValues.build.default, + ...configValues.build[configuration], +}; +const devServerOptions = { + ...configValues.serve.default, + ...configValues.serve[configuration], +}; + +/** + * @type{import('@rspack/core').RspackOptionsNormalized} + */ +module.exports = async () => ({ + devServer: devServerOptions, + plugins: [ + new NxAppRspackPlugin(buildOptions), + new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + // eslint-disable-next-line react-hooks/rules-of-hooks + await useLegacyNxPlugin(require('./rspack.config.old'), buildOptions), + ], +}); +" +`; diff --git a/packages/rspack/src/generators/convert-to-inferred/convert-to-inferred.spec.ts b/packages/rspack/src/generators/convert-to-inferred/convert-to-inferred.spec.ts new file mode 100644 index 0000000000..fadb024f7b --- /dev/null +++ b/packages/rspack/src/generators/convert-to-inferred/convert-to-inferred.spec.ts @@ -0,0 +1,1257 @@ +import { + addProjectConfiguration, + joinPathFragments, + readNxJson, + readProjectConfiguration, + updateNxJson, + updateProjectConfiguration, + writeJson, + type ExpandedPluginConfiguration, + type ProjectConfiguration, + type ProjectGraph, + type Tree, +} from '@nx/devkit'; +import { TempFs } from '@nx/devkit/internal-testing-utils'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { join } from 'node:path'; +import { getRelativeProjectJsonSchemaPath } from 'nx/src/generators/utils/project-configuration'; +import type { RspackPluginOptions } from '../../plugins/plugin'; +import { convertToInferred } from './convert-to-inferred'; + +let fs: TempFs; +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest + .fn() + .mockImplementation(() => Promise.resolve(projectGraph)), + updateProjectConfiguration: jest + .fn() + .mockImplementation((tree, projectName, projectConfiguration) => { + function handleEmptyTargets( + projectName: string, + projectConfiguration: ProjectConfiguration + ): void { + if ( + projectConfiguration.targets && + !Object.keys(projectConfiguration.targets).length + ) { + // Re-order `targets` to appear after the `// target` comment. + delete projectConfiguration.targets; + projectConfiguration[ + '// targets' + ] = `to see all targets run: nx show project ${projectName} --web`; + projectConfiguration.targets = {}; + } else { + delete projectConfiguration['// targets']; + } + } + + const projectConfigFile = joinPathFragments( + projectConfiguration.root, + 'project.json' + ); + + if (!tree.exists(projectConfigFile)) { + throw new Error( + `Cannot update Project ${projectName} at ${projectConfiguration.root}. It either doesn't exist yet, or may not use project.json for configuration. Use \`addProjectConfiguration()\` instead if you want to create a new project.` + ); + } + handleEmptyTargets(projectName, projectConfiguration); + writeJson(tree, projectConfigFile, { + name: projectConfiguration.name ?? projectName, + $schema: getRelativeProjectJsonSchemaPath(tree, projectConfiguration), + ...projectConfiguration, + root: undefined, + }); + projectGraph.nodes[projectName].data = projectConfiguration; + }), +})); +jest.mock('nx/src/devkit-internals', () => ({ + ...jest.requireActual('nx/src/devkit-internals'), + getExecutorInformation: jest + .fn() + .mockImplementation((pkg, ...args) => + jest + .requireActual('nx/src/devkit-internals') + .getExecutorInformation('@nx/rspack', ...args) + ), +})); + +function addProject(tree: Tree, name: string, project: ProjectConfiguration) { + addProjectConfiguration(tree, name, project); + projectGraph.nodes[name] = { + name: name, + type: project.projectType === 'application' ? 'app' : 'lib', + data: { + projectType: project.projectType, + root: project.root, + targets: project.targets, + }, + }; +} + +interface ProjectOptions { + appName: string; + appRoot: string; + buildTargetName: string; + buildExecutor: string; + serveTargetName: string; + serveExecutor: string; +} + +const defaultProjectOptions: ProjectOptions = { + appName: 'app1', + appRoot: 'apps/app1', + buildTargetName: 'build', + buildExecutor: '@nx/rspack:rspack', + serveTargetName: 'serve', + serveExecutor: '@nx/rspack:dev-server', +}; + +const defaultRspackConfig = `const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); +const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); +const { useLegacyNxPlugin } = require('@nx/rspack'); + +// This file was migrated using @nx/rspack:convert-config-to-rspack-plugin from your './rspack.config.old.js' +// Please check that the options here are correct as they were moved from the old rspack.config.js to this file. +const options = {}; + +/** + * @type{import('@rspack/core').RspackOptionsNormalized} + */ +module.exports = async () => ({ + plugins: [ + new NxAppRspackPlugin(options), + new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + // eslint-disable-next-line react-hooks/rules-of-hooks + await useLegacyNxPlugin(require('./rspack.config.old'), options), + ], +}); +`; + +function writeRspackConfig( + tree: Tree, + projectRoot: string, + rspackConfig = defaultRspackConfig +) { + tree.write(`${projectRoot}/rspack.config.js`, rspackConfig); + fs.createFileSync(`${projectRoot}/rspack.config.js`, rspackConfig); + jest.doMock(join(fs.tempDir, projectRoot, 'rspack.config.js'), () => ({}), { + virtual: true, + }); +} + +function createProject( + tree: Tree, + opts: Partial = {}, + extraTargetOptions?: Record> +) { + let projectOpts = { ...defaultProjectOptions, ...opts }; + const project: ProjectConfiguration = { + name: projectOpts.appName, + root: projectOpts.appRoot, + projectType: 'application', + targets: { + [projectOpts.buildTargetName]: { + executor: projectOpts.buildExecutor, + options: { + rspackConfig: `${projectOpts.appRoot}/rspack.config.js`, + outputPath: `dist/${projectOpts.appRoot}`, + index: `${projectOpts.appRoot}/src/index.html`, + main: `${projectOpts.appRoot}/src/main.tsx`, + tsConfig: `${projectOpts.appRoot}/tsconfig.app.json`, + assets: [ + `${projectOpts.appRoot}/src/favicon.ico`, + `${projectOpts.appRoot}/src/assets`, + ], + styles: [`${projectOpts.appRoot}/src/styles.scss`], + scripts: [], + ...extraTargetOptions?.[projectOpts.buildTargetName], + }, + configurations: { + development: { + extractLicenses: false, + optimization: false, + sourceMap: true, + vendorChunk: true, + }, + production: { + fileReplacements: [ + { + replace: `${projectOpts.appRoot}/src/environments/environment.ts`, + with: `${projectOpts.appRoot}/src/environments/environment.prod.ts`, + }, + ], + optimization: true, + outputHashing: 'all', + sourceMap: false, + namedChunks: false, + extractLicenses: true, + vendorChunk: false, + }, + }, + defaultConfiguration: 'production', + }, + [projectOpts.serveTargetName]: { + executor: projectOpts.serveExecutor, + options: { + buildTarget: `${projectOpts.appName}:${projectOpts.buildTargetName}`, + hmr: true, + ssl: true, + sslCert: `${projectOpts.appRoot}/server.crt`, + sslKey: `${projectOpts.appRoot}/server.key`, + ...extraTargetOptions?.[projectOpts.serveTargetName], + }, + configurations: { + development: { + buildTarget: `${projectOpts.appName}:${projectOpts.buildTargetName}:development`, + open: true, + }, + production: { + buildTarget: `${projectOpts.appName}:${projectOpts.buildTargetName}:production`, + hmr: false, + }, + }, + defaultConfiguration: 'development', + }, + }, + }; + fs.createFileSync( + `${projectOpts.appRoot}/proxy.conf.json`, + `{ + "/api": { + "target": "http://localhost:3333", + "secure": false + } + }` + ); + + writeRspackConfig(tree, projectOpts.appRoot, `module.exports = {};`); + + addProject(tree, project.name, project); + fs.createFileSync( + `${projectOpts.appRoot}/project.json`, + JSON.stringify(project) + ); + return project; +} + +describe('convert-to-inferred', () => { + let tree: Tree; + + beforeEach(() => { + fs = new TempFs('rspack'); + tree = createTreeWithEmptyWorkspace(); + tree.root = fs.tempDir; + + projectGraph = { + nodes: {}, + dependencies: {}, + externalNodes: {}, + }; + }); + + afterEach(() => { + fs.cleanup(); + jest.resetModules(); + }); + + describe('--project', () => { + it('should not convert projects without the "rspackConfig" option set', async () => { + const project = createProject(tree); + delete project.targets.build.options.rspackConfig; + updateProjectConfiguration(tree, project.name, project); + const project2 = createProject(tree, { + appName: 'app2', + appRoot: 'apps/app2', + }); + const project2BuildTarget = project2.targets.build; + + await expect( + convertToInferred(tree, { project: project.name }) + ).rejects.toThrow(/missing in the project configuration/); + // assert other projects were not modified + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets.build).toStrictEqual(project2BuildTarget); + }); + + it('should not convert projects still using "composePlugins"', async () => { + const project = createProject(tree); + writeRspackConfig( + tree, + project.root, + `const { composePlugins, withNx } = require('@nx/rspack'); + const { withReact } = require('@nx/react'); + + // Nx plugins for rspack. + module.exports = composePlugins( + withNx(), + withReact({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + (config) => { + return config; + } + ); + ` + ); + const project2 = createProject(tree, { + appName: 'app2', + appRoot: 'apps/app2', + }); + const project2BuildTarget = project2.targets.build; + + await expect( + convertToInferred(tree, { project: project.name }) + ).rejects.toThrow(/@nx\/rspack:convert-config-to-rspack-plugin"/); + // assert other projects were not modified + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets.build).toStrictEqual(project2BuildTarget); + }); + + it('should not convert projects not using "NxAppRspackPlugin"', async () => { + const project = createProject(tree); + writeRspackConfig( + tree, + project.root, + `module.exports = { + entry: './src/main.js', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'main.bundle.js', + }, + }; + ` + ); + const project2 = createProject(tree, { + appName: 'app2', + appRoot: 'apps/app2', + }); + const project2BuildTarget = project2.targets.build; + + await expect( + convertToInferred(tree, { project: project.name }) + ).rejects.toThrow(/rspack config/); + // assert other projects were not modified + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets.build).toStrictEqual(project2BuildTarget); + }); + + it('should register plugin in nx.json', async () => { + const project = createProject(tree); + writeRspackConfig(tree, project.root); + const project2 = createProject(tree, { + appName: 'app2', + appRoot: 'apps/app2', + }); + const project2BuildTarget = project2.targets.build; + + await convertToInferred(tree, { project: project.name }); + + // assert plugin was added to nx.json + const nxJsonPlugins = readNxJson(tree).plugins; + const rspackPlugin = nxJsonPlugins.find( + (plugin): plugin is ExpandedPluginConfiguration => + typeof plugin !== 'string' && + plugin.plugin === '@nx/rspack/plugin' && + plugin.include?.length === 1 + ); + expect(rspackPlugin).toBeTruthy(); + expect(rspackPlugin.include).toEqual([`${project.root}/**/*`]); + // project configuration + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets.build).toStrictEqual({ + configurations: { development: {}, production: {} }, + defaultConfiguration: 'production', + }); + // assert other projects were not modified + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets.build).toStrictEqual(project2BuildTarget); + }); + + it('should remove "includes" from the plugin registration when all projects are included', async () => { + const project1 = createProject(tree); + writeRspackConfig(tree, project1.root); + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/rspack/plugin', + options: { + buildTargetName: 'build', + previewTargetName: 'preview', + serveStaticTargetName: 'serve-static', + serveTargetName: 'serve', + }, + include: [`${project1.root}/**/*`], + }); + updateNxJson(tree, nxJson); + const project2 = createProject(tree, { + appName: 'app2', + appRoot: 'apps/app2', + }); + writeRspackConfig(tree, project2.root); + + await convertToInferred(tree, { project: project2.name }); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const rspackPluginRegistrations = nxJsonPlugins.filter( + (plugin): plugin is ExpandedPluginConfiguration => + typeof plugin !== 'string' && plugin.plugin === '@nx/rspack/plugin' + ); + expect(rspackPluginRegistrations.length).toBe(1); + expect(rspackPluginRegistrations[0].include).toBeUndefined(); + }); + + it('should not add to "includes" when existing matching registration does not have it set', async () => { + const project1 = createProject(tree); + writeRspackConfig(tree, project1.root); + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/rspack/plugin', + options: { + buildTargetName: 'build', + previewTargetName: 'preview', + serveStaticTargetName: 'serve-static', + serveTargetName: 'serve', + }, + }); + updateNxJson(tree, nxJson); + const project2 = createProject(tree, { + appName: 'app2', + appRoot: 'apps/app2', + }); + writeRspackConfig(tree, project2.root); + const project3 = createProject(tree, { + appName: 'app3', + appRoot: 'apps/app3', + }); + writeRspackConfig(tree, project3.root); + + await convertToInferred(tree, { project: project2.name }); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const rspackPluginRegistrations = nxJsonPlugins.filter( + (plugin): plugin is ExpandedPluginConfiguration => + typeof plugin !== 'string' && plugin.plugin === '@nx/rspack/plugin' + ); + expect(rspackPluginRegistrations.length).toBe(1); + expect(rspackPluginRegistrations[0].include).toBeUndefined(); + }); + + it('should move options to the rspack config file', async () => { + const project = createProject(tree); + writeRspackConfig(tree, project.root); + const project2 = createProject(tree, { + appName: 'app2', + appRoot: 'apps/app2', + }); + const project2BuildTarget = project2.targets.build; + + await convertToInferred(tree, { project: project.name }); + + // check the updated rspack config + expect(tree.read(`${project.root}/rspack.config.js`, 'utf-8')) + .toMatchInlineSnapshot(` + "const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); + const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); + const { useLegacyNxPlugin } = require('@nx/rspack'); + + // These options were migrated by @nx/rspack:convert-to-inferred from + // the project.json file and merged with the options in this file + const configValues = { + build: { + default: { + outputPath: '../../dist/apps/app1', + index: './src/index.html', + main: './src/main.tsx', + tsConfig: './tsconfig.app.json', + assets: ['./src/favicon.ico', './src/assets'], + styles: ['./src/styles.scss'], + }, + development: { + extractLicenses: false, + optimization: false, + sourceMap: true, + vendorChunk: true, + }, + production: { + fileReplacements: [ + { + replace: './src/environments/environment.ts', + with: './src/environments/environment.prod.ts', + }, + ], + optimization: true, + outputHashing: 'all', + sourceMap: false, + namedChunks: false, + extractLicenses: true, + vendorChunk: false, + }, + }, + serve: { + default: { + hmr: true, + server: { + type: 'https', + options: { cert: './server.crt', key: './server.key' }, + }, + port: 4200, + headers: { 'Access-Control-Allow-Origin': '*' }, + historyApiFallback: { + index: '/index.html', + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + }, + }, + development: { open: true }, + production: { hmr: false }, + }, + }; + + // Determine the correct configValue to use based on the configuration + const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default'; + + const buildOptions = { + ...configValues.build.default, + ...configValues.build[configuration], + }; + const devServerOptions = { + ...configValues.serve.default, + ...configValues.serve[configuration], + }; + + /** + * @type{import('@rspack/core').RspackOptionsNormalized} + */ + module.exports = async () => ({ + devServer: devServerOptions, + plugins: [ + new NxAppRspackPlugin(buildOptions), + new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + // eslint-disable-next-line react-hooks/rules-of-hooks + await useLegacyNxPlugin(require('./rspack.config.old'), buildOptions), + ], + }); + " + `); + // project configuration + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets.build).toStrictEqual({ + configurations: { development: {}, production: {} }, + defaultConfiguration: 'production', + }); + // assert other projects were not modified + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets.build).toStrictEqual(project2BuildTarget); + }); + + it('should merge options into the options object in the rspack config file', async () => { + const project = createProject(tree, undefined, { + build: { + main: `${defaultProjectOptions.appRoot}/src/main.tsx`, + tsConfig: `${defaultProjectOptions.appRoot}/tsconfig.app.json`, + assets: [ + `${defaultProjectOptions.appRoot}/src/favicon.ico`, + `${defaultProjectOptions.appRoot}/src/public`, + ], + styles: [`${defaultProjectOptions.appRoot}/src/theme.scss`], + }, + }); + writeRspackConfig( + tree, + project.root, + `const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); + const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); + const { useLegacyNxPlugin } = require('@nx/rspack'); + + // This file was migrated using @nx/rspack:convert-config-to-rspack-plugin from your './rspack.config.old.js' + // Please check that the options here are correct as they were moved from the old rspack.config.js to this file. + const options = { + assets: ['./src/favicon.ico', './src/assets'], + styles: ['./src/styles.scss'], + memoryLimit: 4096, + }; + + /** + * @type{import('@rspack/core').RspackOptionsNormalized} + */ + module.exports = async () => ({ + plugins: [ + new NxAppRspackPlugin(options), + new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + // eslint-disable-next-line react-hooks/rules-of-hooks + await useLegacyNxPlugin(require('./rspack.config.old'), options), + ], + }); + ` + ); + const project2 = createProject(tree, { + appName: 'app2', + appRoot: 'apps/app2', + }); + const project2BuildTarget = project2.targets.build; + + await convertToInferred(tree, { project: project.name }); + + // check the updated rspack config + expect(tree.read(`${project.root}/rspack.config.js`, 'utf-8')) + .toMatchInlineSnapshot(` + "const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); + const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); + const { useLegacyNxPlugin } = require('@nx/rspack'); + + // These options were migrated by @nx/rspack:convert-to-inferred from + // the project.json file and merged with the options in this file + const configValues = { + build: { + default: { + assets: ['./src/favicon.ico', './src/assets'], + styles: ['./src/styles.scss'], + memoryLimit: 4096, + outputPath: '../../dist/apps/app1', + index: './src/index.html', + main: './src/main.tsx', + tsConfig: './tsconfig.app.json', + }, + development: { + extractLicenses: false, + optimization: false, + sourceMap: true, + vendorChunk: true, + }, + production: { + fileReplacements: [ + { + replace: './src/environments/environment.ts', + with: './src/environments/environment.prod.ts', + }, + ], + optimization: true, + outputHashing: 'all', + sourceMap: false, + namedChunks: false, + extractLicenses: true, + vendorChunk: false, + }, + }, + serve: { + default: { + hmr: true, + server: { + type: 'https', + options: { cert: './server.crt', key: './server.key' }, + }, + port: 4200, + headers: { 'Access-Control-Allow-Origin': '*' }, + historyApiFallback: { + index: '/index.html', + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + }, + }, + development: { open: true }, + production: { hmr: false }, + }, + }; + + // Determine the correct configValue to use based on the configuration + const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default'; + + const buildOptions = { + ...configValues.build.default, + ...configValues.build[configuration], + }; + const devServerOptions = { + ...configValues.serve.default, + ...configValues.serve[configuration], + }; + + /** + * @type{import('@rspack/core').RspackOptionsNormalized} + */ + module.exports = async () => ({ + devServer: devServerOptions, + plugins: [ + new NxAppRspackPlugin(buildOptions), + new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + // eslint-disable-next-line react-hooks/rules-of-hooks + await useLegacyNxPlugin(require('./rspack.config.old'), buildOptions), + ], + }); + " + `); + // project configuration + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets.build).toStrictEqual({ + configurations: { development: {}, production: {} }, + defaultConfiguration: 'production', + }); + // assert other projects were not modified + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets.build).toStrictEqual(project2BuildTarget); + }); + + it('should not touch the existing "devServer" option', async () => { + const project = createProject(tree); + writeRspackConfig( + tree, + project.root, + `const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); + const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); + const { useLegacyNxPlugin } = require('@nx/rspack'); + + // This file was migrated using @nx/rspack:convert-config-to-rspack-plugin from your './rspack.config.old.js' + // Please check that the options here are correct as they were moved from the old rspack.config.js to this file. + const options = {}; + + /** + * @type{import('@rspack/core').RspackOptionsNormalized} + */ + module.exports = async () => ({ + devServer: { hot: true }, + plugins: [ + new NxAppRspackPlugin(options), + new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + // eslint-disable-next-line react-hooks/rules-of-hooks + await useLegacyNxPlugin(require('./rspack.config.old'), options), + ], + }); + ` + ); + const project2 = createProject(tree, { + appName: 'app2', + appRoot: 'apps/app2', + }); + const project2BuildTarget = project2.targets.build; + + await convertToInferred(tree, { project: project.name }); + + // check the updated rspack config + expect(tree.read(`${project.root}/rspack.config.js`, 'utf-8')).toEqual( + expect.stringContaining(`// This is the untouched "devServer" option from the original rspack config. Please review it and make any necessary changes manually. + devServer: { hot: true },`) + ); + // project configuration + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets.build).toStrictEqual({ + configurations: { development: {}, production: {} }, + defaultConfiguration: 'production', + }); + // assert other projects were not modified + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets.build).toStrictEqual(project2BuildTarget); + }); + + it('should keep the "port" value if set', async () => { + const project = createProject(tree, undefined, { + serve: { port: 1234 }, + }); + writeRspackConfig(tree, project.root); + const project2 = createProject(tree, { + appName: 'app2', + appRoot: 'apps/app2', + }); + const project2BuildTarget = project2.targets.build; + + await convertToInferred(tree, { project: project.name }); + + // check the updated rspack config + expect(tree.read(`${project.root}/rspack.config.js`, 'utf-8')).toContain( + 'port: 1234,' + ); + expect( + tree.read(`${project.root}/rspack.config.js`, 'utf-8') + ).not.toContain('port: 4200,'); + // project configuration + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets.build).toStrictEqual({ + configurations: { development: {}, production: {} }, + defaultConfiguration: 'production', + }); + // assert other projects were not modified + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets.build).toStrictEqual(project2BuildTarget); + }); + }); + + describe('all projects', () => { + it('should migrate all projects using the rspack executors', async () => { + const project1 = createProject(tree); + writeRspackConfig(tree, project1.root); + const project2 = createProject(tree, { + appName: 'app2', + appRoot: 'apps/app2', + buildExecutor: '@nrwl/rspack:rspack', + serveExecutor: '@nrwl/rspack:dev-server', + }); + writeRspackConfig(tree, project2.root); + const project3 = createProject(tree, { + appName: 'app3', + appRoot: 'apps/app3', + buildTargetName: 'build-rspack', + serveTargetName: 'serve-rspack', + }); + writeRspackConfig(tree, project3.root); + const project4 = createProject(tree, { + appName: 'app4', + appRoot: 'apps/app4', + buildTargetName: 'build', + serveTargetName: 'serve-rspack', + }); + writeRspackConfig(tree, project4.root); + const project5 = createProject(tree, { + appName: 'app5', + appRoot: 'apps/app5', + buildTargetName: 'build-rspack', + serveTargetName: 'serve', + }); + writeRspackConfig(tree, project5.root); + const projectWithComposePlugins = createProject(tree, { + appName: 'app6', + appRoot: 'apps/app6', + }); + const projectWithComposePluginsInitialTargets = + projectWithComposePlugins.targets; + const initialProjectWithComposePluginsRspackConfig = `const { composePlugins, withNx } = require('@nx/rspack'); +const { withReact } = require('@nx/react'); + +// Nx plugins for rspack. +module.exports = composePlugins( + withNx(), + withReact({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + (config) => { + return config; + } +); +`; + writeRspackConfig( + tree, + projectWithComposePlugins.root, + initialProjectWithComposePluginsRspackConfig + ); + const projectWithNoNxAppRspackPlugin = createProject(tree, { + appName: 'app7', + appRoot: 'apps/app7', + }); + const projectWithNoNxAppRspackPluginInitialTargets = + projectWithNoNxAppRspackPlugin.targets; + const initialProjectWithNoNxAppRspackPluginRspackConfig = `module.exports = { + entry: './src/main.js', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'main.bundle.js', + }, +}; +`; + writeRspackConfig( + tree, + projectWithNoNxAppRspackPlugin.root, + initialProjectWithNoNxAppRspackPluginRspackConfig + ); + + await convertToInferred(tree, {}); + + // project configurations + const updatedProject1 = readProjectConfiguration(tree, project1.name); + expect(updatedProject1.targets).toStrictEqual({ + build: { + configurations: { development: {}, production: {} }, + defaultConfiguration: 'production', + }, + serve: { + configurations: { development: {}, production: {} }, + defaultConfiguration: 'development', + }, + }); + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets).toStrictEqual({ + build: { + configurations: { development: {}, production: {} }, + defaultConfiguration: 'production', + }, + serve: { + configurations: { development: {}, production: {} }, + defaultConfiguration: 'development', + }, + }); + const updatedProject3 = readProjectConfiguration(tree, project3.name); + expect(updatedProject3.targets).toStrictEqual({ + 'build-rspack': { + configurations: { development: {}, production: {} }, + defaultConfiguration: 'production', + }, + 'serve-rspack': { + configurations: { development: {}, production: {} }, + defaultConfiguration: 'development', + }, + }); + const updatedProject4 = readProjectConfiguration(tree, project4.name); + expect(updatedProject4.targets).toStrictEqual({ + build: { + configurations: { development: {}, production: {} }, + defaultConfiguration: 'production', + }, + 'serve-rspack': { + configurations: { development: {}, production: {} }, + defaultConfiguration: 'development', + }, + }); + const updatedProject5 = readProjectConfiguration(tree, project5.name); + expect(updatedProject5.targets).toStrictEqual({ + 'build-rspack': { + configurations: { development: {}, production: {} }, + defaultConfiguration: 'production', + }, + serve: { + configurations: { development: {}, production: {} }, + defaultConfiguration: 'development', + }, + }); + const updatedProjectWithComposePlugins = readProjectConfiguration( + tree, + projectWithComposePlugins.name + ); + expect(updatedProjectWithComposePlugins.targets).toStrictEqual( + projectWithComposePluginsInitialTargets + ); + const updatedProjectWithNoNxAppRspackPlugin = readProjectConfiguration( + tree, + projectWithNoNxAppRspackPlugin.name + ); + expect(updatedProjectWithNoNxAppRspackPlugin.targets).toStrictEqual( + projectWithNoNxAppRspackPluginInitialTargets + ); + // rspack config files + const project1RspackConfig = tree.read( + `${project1.root}/rspack.config.js`, + 'utf-8' + ); + expect(project1RspackConfig).toMatchSnapshot(); + const project2RspackConfig = tree.read( + `${project2.root}/rspack.config.js`, + 'utf-8' + ); + expect(project2RspackConfig).toMatchSnapshot(); + const project3RspackConfig = tree.read( + `${project3.root}/rspack.config.js`, + 'utf-8' + ); + expect(project3RspackConfig).toMatchSnapshot(); + const updatedProjectWithComposePluginsRspackConfig = tree.read( + `${projectWithComposePlugins.root}/rspack.config.js`, + 'utf-8' + ); + expect(updatedProjectWithComposePluginsRspackConfig).toBe( + initialProjectWithComposePluginsRspackConfig + ); + const updatedProjectWithNoNxAppRspackPluginRspackConfig = tree.read( + `${projectWithNoNxAppRspackPlugin.root}/rspack.config.js`, + 'utf-8' + ); + expect(updatedProjectWithNoNxAppRspackPluginRspackConfig).toBe( + initialProjectWithNoNxAppRspackPluginRspackConfig + ); + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const rspackPluginRegistrations = nxJsonPlugins.filter( + (plugin): plugin is ExpandedPluginConfiguration => + typeof plugin !== 'string' && plugin.plugin === '@nx/rspack/plugin' + ); + expect(rspackPluginRegistrations.length).toBe(4); + expect(rspackPluginRegistrations[0].options.buildTargetName).toBe( + 'build' + ); + expect(rspackPluginRegistrations[0].options.serveTargetName).toBe( + 'serve' + ); + expect(rspackPluginRegistrations[0].include).toEqual([ + `${project1.root}/**/*`, + `${project2.root}/**/*`, + ]); + expect(rspackPluginRegistrations[1].options.buildTargetName).toBe( + 'build' + ); + expect(rspackPluginRegistrations[1].options.serveTargetName).toBe( + 'serve-rspack' + ); + expect(rspackPluginRegistrations[1].include).toEqual([ + `${project4.root}/**/*`, + ]); + expect(rspackPluginRegistrations[2].options.buildTargetName).toBe( + 'build-rspack' + ); + expect(rspackPluginRegistrations[2].options.serveTargetName).toBe( + 'serve-rspack' + ); + expect(rspackPluginRegistrations[2].include).toEqual([ + `${project3.root}/**/*`, + ]); + expect(rspackPluginRegistrations[3].options.buildTargetName).toBe( + 'build-rspack' + ); + expect(rspackPluginRegistrations[3].options.serveTargetName).toBe( + 'serve' + ); + expect(rspackPluginRegistrations[3].include).toEqual([ + `${project5.root}/**/*`, + ]); + }); + + it('should remove "includes" from the plugin registration when all projects are included', async () => { + const project1 = createProject(tree); + writeRspackConfig(tree, project1.root); + const project2 = createProject(tree, { + appName: 'app2', + appRoot: 'apps/app2', + buildExecutor: '@nrwl/rspack:rspack', + serveExecutor: '@nrwl/rspack:dev-server', + }); + writeRspackConfig(tree, project2.root); + + await convertToInferred(tree, {}); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const rspackPluginRegistrations = nxJsonPlugins.filter( + (plugin): plugin is ExpandedPluginConfiguration => + typeof plugin !== 'string' && plugin.plugin === '@nx/rspack/plugin' + ); + expect(rspackPluginRegistrations.length).toBe(1); + expect(rspackPluginRegistrations[0].include).toBeUndefined(); + }); + + it('should keep the higher "memoryLimit" value in the build configuration', async () => { + const project = createProject(tree, undefined, { + build: { memoryLimit: 4096 }, + serve: { memoryLimit: 8192 }, // higher value, should be set in the build configuration + }); + writeRspackConfig(tree, project.root); + const project2 = createProject( + tree, + { appName: 'app2', appRoot: 'apps/app2' }, + { + build: { memoryLimit: 8192 }, // higher value, should be kept in the build configuration + serve: { memoryLimit: 4096 }, + } + ); + writeRspackConfig(tree, project2.root); + + await convertToInferred(tree, {}); + + // check the updated rspack config + expect(tree.read(`${project.root}/rspack.config.js`, 'utf-8')) + .toMatchInlineSnapshot(` + "const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); + const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); + const { useLegacyNxPlugin } = require('@nx/rspack'); + + // These options were migrated by @nx/rspack:convert-to-inferred from + // the project.json file and merged with the options in this file + const configValues = { + build: { + default: { + outputPath: '../../dist/apps/app1', + index: './src/index.html', + main: './src/main.tsx', + tsConfig: './tsconfig.app.json', + assets: ['./src/favicon.ico', './src/assets'], + styles: ['./src/styles.scss'], + memoryLimit: 8192, + }, + development: { + extractLicenses: false, + optimization: false, + sourceMap: true, + vendorChunk: true, + }, + production: { + fileReplacements: [ + { + replace: './src/environments/environment.ts', + with: './src/environments/environment.prod.ts', + }, + ], + optimization: true, + outputHashing: 'all', + sourceMap: false, + namedChunks: false, + extractLicenses: true, + vendorChunk: false, + }, + }, + serve: { + default: { + hmr: true, + server: { + type: 'https', + options: { cert: './server.crt', key: './server.key' }, + }, + memoryLimit: 8192, + port: 4200, + headers: { 'Access-Control-Allow-Origin': '*' }, + historyApiFallback: { + index: '/index.html', + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + }, + }, + development: { open: true }, + production: { hmr: false }, + }, + }; + + // Determine the correct configValue to use based on the configuration + const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default'; + + const buildOptions = { + ...configValues.build.default, + ...configValues.build[configuration], + }; + const devServerOptions = { + ...configValues.serve.default, + ...configValues.serve[configuration], + }; + + /** + * @type{import('@rspack/core').RspackOptionsNormalized} + */ + module.exports = async () => ({ + devServer: devServerOptions, + plugins: [ + new NxAppRspackPlugin(buildOptions), + new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + // eslint-disable-next-line react-hooks/rules-of-hooks + await useLegacyNxPlugin(require('./rspack.config.old'), buildOptions), + ], + }); + " + `); + expect(tree.read(`${project2.root}/rspack.config.js`, 'utf-8')) + .toMatchInlineSnapshot(` + "const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin'); + const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin'); + const { useLegacyNxPlugin } = require('@nx/rspack'); + + // These options were migrated by @nx/rspack:convert-to-inferred from + // the project.json file and merged with the options in this file + const configValues = { + build: { + default: { + outputPath: '../../dist/apps/app2', + index: './src/index.html', + main: './src/main.tsx', + tsConfig: './tsconfig.app.json', + assets: ['./src/favicon.ico', './src/assets'], + styles: ['./src/styles.scss'], + memoryLimit: 8192, + }, + development: { + extractLicenses: false, + optimization: false, + sourceMap: true, + vendorChunk: true, + }, + production: { + fileReplacements: [ + { + replace: './src/environments/environment.ts', + with: './src/environments/environment.prod.ts', + }, + ], + optimization: true, + outputHashing: 'all', + sourceMap: false, + namedChunks: false, + extractLicenses: true, + vendorChunk: false, + }, + }, + serve: { + default: { + hmr: true, + server: { + type: 'https', + options: { cert: './server.crt', key: './server.key' }, + }, + memoryLimit: 4096, + port: 4200, + headers: { 'Access-Control-Allow-Origin': '*' }, + historyApiFallback: { + index: '/index.html', + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + }, + }, + development: { open: true }, + production: { hmr: false }, + }, + }; + + // Determine the correct configValue to use based on the configuration + const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default'; + + const buildOptions = { + ...configValues.build.default, + ...configValues.build[configuration], + }; + const devServerOptions = { + ...configValues.serve.default, + ...configValues.serve[configuration], + }; + + /** + * @type{import('@rspack/core').RspackOptionsNormalized} + */ + module.exports = async () => ({ + devServer: devServerOptions, + plugins: [ + new NxAppRspackPlugin(buildOptions), + new NxReactRspackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + // eslint-disable-next-line react-hooks/rules-of-hooks + await useLegacyNxPlugin(require('./rspack.config.old'), buildOptions), + ], + }); + " + `); + }); + }); +}); diff --git a/packages/rspack/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/rspack/src/generators/convert-to-inferred/convert-to-inferred.ts new file mode 100644 index 0000000000..30c9c4057c --- /dev/null +++ b/packages/rspack/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -0,0 +1,185 @@ +import { + addDependenciesToPackageJson, + createProjectGraphAsync, + formatFiles, + runTasksInSerial, + type ProjectConfiguration, + type Tree, +} from '@nx/devkit'; +import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; +import { + migrateProjectExecutorsToPlugin, + NoTargetsToMigrateError, +} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import * as ts from 'typescript'; +import { createNodesV2, type RspackPluginOptions } from '../../plugins/plugin'; +import { rspackCoreVersion } from '../../utils/versions'; +import { + buildPostTargetTransformerFactory, + servePostTargetTransformerFactory, + type MigrationContext, +} from './utils'; +import { logger as devkitLogger } from 'nx/src/devkit-exports'; + +interface Schema { + project?: string; + skipFormat?: boolean; +} + +export async function convertToInferred(tree: Tree, options: Schema) { + const projectGraph = await createProjectGraphAsync(); + const migrationContext: MigrationContext = { + logger: new AggregatedLog(), + projectGraph, + workspaceRoot: tree.root, + }; + + const logger = createCollectingLogger(); + + const migratedProjects = + await migrateProjectExecutorsToPlugin( + tree, + projectGraph, + '@nx/rspack/plugin', + createNodesV2, + { + buildTargetName: 'build', + previewTargetName: 'preview', + serveStaticTargetName: 'serve-static', + serveTargetName: 'serve', + }, + [ + { + executors: ['@nx/rspack:rspack', '@nrwl/rspack:rspack'], + postTargetTransformer: + buildPostTargetTransformerFactory(migrationContext), + targetPluginOptionMapper: (target) => ({ buildTargetName: target }), + skipProjectFilter: skipProjectFilterFactory(tree), + }, + { + executors: ['@nx/rspack:dev-server', '@nrwl/rspack:dev-server'], + postTargetTransformer: + servePostTargetTransformerFactory(migrationContext), + targetPluginOptionMapper: (target) => ({ serveTargetName: target }), + skipProjectFilter: skipProjectFilterFactory(tree), + }, + ], + options.project, + logger + ); + + if (migratedProjects.size === 0) { + const convertMessage = [...logger.loggedMessages.values()] + .flat() + .find((v) => v.includes('@nx/rspack:convert-config-to-rspack-plugin')); + + if (convertMessage.length > 0) { + logger.flushLogs((message) => !convertMessage.includes(message)); + throw new Error(convertMessage); + } else { + logger.flushLogs(); + throw new NoTargetsToMigrateError(); + } + } else { + logger.flushLogs(); + } + + const installCallback = addDependenciesToPackageJson( + tree, + {}, + { '@rspack/core': rspackCoreVersion }, + undefined, + true + ); + + if (!options.skipFormat) { + await formatFiles(tree); + } + + return runTasksInSerial(installCallback, () => { + migrationContext.logger.flushLogs(); + }); +} + +function skipProjectFilterFactory(tree: Tree) { + return function skipProjectFilter( + projectConfiguration: ProjectConfiguration + ): false | string { + const buildTarget = Object.values(projectConfiguration.targets).find( + (target) => + target.executor === '@nx/rspack:rspack' || + target.executor === '@nrwl/rspack:rspack' + ); + // the projects for which this is called are guaranteed to have a build target + const rspackConfigPath = buildTarget.options.rspackConfig; + if (!rspackConfigPath) { + return `The rspack config path is missing in the project configuration (${projectConfiguration.root}).`; + } + + const sourceFile = tsquery.ast(tree.read(rspackConfigPath, 'utf-8')); + + const composePluginsSelector = + 'CallExpression:has(Identifier[name=composePlugins])'; + const composePlugins = tsquery( + sourceFile, + composePluginsSelector + )[0]; + + if (composePlugins) { + return `The rspack config (${rspackConfigPath}) can only work with the "@nx/rspack:rspack" executor. Run the "@nx/rspack:convert-config-to-rspack-plugin" generator first.`; + } + + const nxAppRspackPluginSelector = + 'PropertyAssignment:has(Identifier[name=plugins]) NewExpression:has(Identifier[name=NxAppRspackPlugin])'; + const nxAppRspackPlugin = tsquery( + sourceFile, + nxAppRspackPluginSelector + )[0]; + + if (!nxAppRspackPlugin) { + return `No "NxAppRspackPlugin" found in the rspack config (${rspackConfigPath}). Its usage is required for the migration to work.`; + } + + return false; + }; +} + +export function createCollectingLogger(): typeof devkitLogger & { + loggedMessages: Map; + flushLogs: (filter?: (message: string) => boolean) => void; +} { + const loggedMessages = new Map(); + + const flushLogs = (filter?: (message: string) => boolean) => { + loggedMessages.forEach((messages, method) => { + messages.forEach((message) => { + if (!filter || filter(message)) { + devkitLogger[method](message); + } + }); + }); + }; + + return new Proxy( + { ...devkitLogger, loggedMessages, flushLogs }, + { + get(target, property) { + const originalMethod = target[property]; + + if (typeof originalMethod === 'function') { + return (...args) => { + const message = args.join(' '); + const propertyString = String(property); + if (!loggedMessages.has(message)) { + loggedMessages.set(propertyString, []); + } + loggedMessages.get(propertyString).push(message); + }; + } + + return originalMethod; + }, + } + ); +} diff --git a/packages/rspack/src/generators/convert-to-inferred/schema.json b/packages/rspack/src/generators/convert-to-inferred/schema.json new file mode 100644 index 0000000000..7de0321ae1 --- /dev/null +++ b/packages/rspack/src/generators/convert-to-inferred/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "NxWebpackConvertToInferred", + "description": "Convert existing Webpack project(s) using `@nx/webpack:wepack` executor to use `@nx/webpack/plugin`.", + "title": "Convert a Webpack project from executor to plugin", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The project to convert from using the `@nx/webpack:webpack` executor to use `@nx/webpack/plugin`. If not provided, all projects using the `@nx/webpack:webpack` executor will be converted.", + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files.", + "default": false + } + } +} diff --git a/packages/rspack/src/generators/convert-to-inferred/utils/ast.ts b/packages/rspack/src/generators/convert-to-inferred/utils/ast.ts new file mode 100644 index 0000000000..a6a3f9d951 --- /dev/null +++ b/packages/rspack/src/generators/convert-to-inferred/utils/ast.ts @@ -0,0 +1,59 @@ +import * as ts from 'typescript'; + +export function toPropertyAssignment( + key: string, + value: unknown +): ts.PropertyAssignment { + if (typeof value === 'string') { + return ts.factory.createPropertyAssignment( + ts.factory.createStringLiteral(key), + ts.factory.createStringLiteral(value) + ); + } else if (typeof value === 'number') { + return ts.factory.createPropertyAssignment( + ts.factory.createStringLiteral(key), + ts.factory.createNumericLiteral(value) + ); + } else if (typeof value === 'boolean') { + return ts.factory.createPropertyAssignment( + ts.factory.createStringLiteral(key), + value ? ts.factory.createTrue() : ts.factory.createFalse() + ); + } else if (Array.isArray(value)) { + return ts.factory.createPropertyAssignment( + ts.factory.createStringLiteral(key), + ts.factory.createArrayLiteralExpression( + value.map((item) => toExpression(item)) + ) + ); + } else { + return ts.factory.createPropertyAssignment( + ts.factory.createStringLiteral(key), + ts.factory.createObjectLiteralExpression( + Object.entries(value).map(([key, value]) => + toPropertyAssignment(key, value) + ) + ) + ); + } +} + +export function toExpression(value: unknown): ts.Expression { + if (typeof value === 'string') { + return ts.factory.createStringLiteral(value); + } else if (typeof value === 'number') { + return ts.factory.createNumericLiteral(value); + } else if (typeof value === 'boolean') { + return value ? ts.factory.createTrue() : ts.factory.createFalse(); + } else if (Array.isArray(value)) { + return ts.factory.createArrayLiteralExpression( + value.map((item) => toExpression(item)) + ); + } else { + return ts.factory.createObjectLiteralExpression( + Object.entries(value).map(([key, value]) => + toPropertyAssignment(key, value) + ) + ); + } +} diff --git a/packages/rspack/src/generators/convert-to-inferred/utils/build-post-target-transformer.ts b/packages/rspack/src/generators/convert-to-inferred/utils/build-post-target-transformer.ts new file mode 100644 index 0000000000..724bc01fac --- /dev/null +++ b/packages/rspack/src/generators/convert-to-inferred/utils/build-post-target-transformer.ts @@ -0,0 +1,397 @@ +import type { TargetConfiguration, Tree } from '@nx/devkit'; +import { + processTargetOutputs, + toProjectRelativePath, +} from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import * as ts from 'typescript'; +import type { RspackExecutorSchema } from '../../../executors/rspack/schema'; +import type { NxAppRspackPluginOptions } from '../../../plugins/utils/models'; +import { toPropertyAssignment } from './ast'; +import type { MigrationContext, TransformerContext } from './types'; + +export function buildPostTargetTransformerFactory( + migrationContext: MigrationContext +) { + return function buildPostTargetTransformer( + target: TargetConfiguration, + tree: Tree, + projectDetails: { projectName: string; root: string }, + inferredTarget: TargetConfiguration + ): TargetConfiguration { + const context: TransformerContext = { + ...migrationContext, + projectName: projectDetails.projectName, + projectRoot: projectDetails.root, + }; + + const { pluginOptions, rspackConfigPath } = processOptions(target, context); + + updateRspackConfig(tree, rspackConfigPath, pluginOptions); + + if (target.outputs) { + processTargetOutputs(target, [], inferredTarget, { + projectName: projectDetails.projectName, + projectRoot: projectDetails.root, + }); + } + + return target; + }; +} + +type ExtractedOptions = { + default: NxAppRspackPluginOptions; + [configName: string]: NxAppRspackPluginOptions; +}; + +function processOptions( + target: TargetConfiguration, + context: TransformerContext +): { + pluginOptions: ExtractedOptions; + rspackConfigPath: string; +} { + const rspackConfig = target.options.rspackConfig; + delete target.options.rspackConfig; + + const pluginOptions: ExtractedOptions = { + default: extractPluginOptions(target.options, context), + }; + + if (target.configurations && Object.keys(target.configurations).length) { + for (const [configName, config] of Object.entries(target.configurations)) { + pluginOptions[configName] = extractPluginOptions( + config, + context, + configName + ); + } + } + + return { pluginOptions, rspackConfigPath: rspackConfig }; +} + +const pathOptions = new Set([ + 'index', + 'main', + 'outputPath', + 'polyfills', + 'postcssConfig', + 'tsConfig', +]); +const assetsOptions = new Set(['assets', 'scripts', 'styles']); + +function extractPluginOptions( + options: RspackExecutorSchema, + context: TransformerContext, + configName?: string +): NxAppRspackPluginOptions { + const pluginOptions: NxAppRspackPluginOptions = {}; + + for (const [key, value] of Object.entries(options)) { + if (pathOptions.has(key)) { + pluginOptions[key] = toProjectRelativePath(value, context.projectRoot); + delete options[key]; + } else if (assetsOptions.has(key)) { + pluginOptions[key] = value.map((asset: string | { input: string }) => { + if (typeof asset === 'string') { + return toProjectRelativePath(asset, context.projectRoot); + } + + asset.input = toProjectRelativePath(asset.input, context.projectRoot); + return asset; + }); + delete options[key]; + } else if (key === 'fileReplacements') { + pluginOptions.fileReplacements = value.map( + (replacement: { replace: string; with: string }) => ({ + replace: toProjectRelativePath( + replacement.replace, + context.projectRoot + ), + with: toProjectRelativePath(replacement.with, context.projectRoot), + }) + ); + delete options.fileReplacements; + } else if (key === 'additionalEntryPoints') { + pluginOptions.additionalEntryPoints = value.map((entryPoint) => { + entryPoint.entryPath = toProjectRelativePath( + entryPoint.entryPath, + context.projectRoot + ); + return entryPoint; + }); + delete options.additionalEntryPoints; + } else if (key === 'memoryLimit') { + pluginOptions.memoryLimit = value; + const serveMemoryLimit = getMemoryLimitFromServeTarget( + context, + configName + ); + if (serveMemoryLimit) { + pluginOptions.memoryLimit = Math.max(serveMemoryLimit, value); + context.logger.addLog({ + executorName: '@nx/rspack:rspack', + log: `The "memoryLimit" option was set in both the serve and build configurations. The migration set the higher value to the build configuration and removed the option from the serve configuration.`, + project: context.projectName, + }); + } + delete options.memoryLimit; + } else if (key === 'standardRspackConfigFunction') { + delete options.standardRspackConfigFunction; + } else { + pluginOptions[key] = value; + delete options[key]; + } + } + + return pluginOptions; +} + +function updateRspackConfig( + tree: Tree, + rspackConfig: string, + pluginOptions: ExtractedOptions +): void { + let sourceFile: ts.SourceFile; + let rspackConfigText: string; + + const updateSources = () => { + rspackConfigText = tree.read(rspackConfig, 'utf-8'); + sourceFile = tsquery.ast(rspackConfigText); + }; + updateSources(); + + setOptionsInRspackConfig( + tree, + rspackConfigText, + sourceFile, + rspackConfig, + pluginOptions + ); + updateSources(); + + setOptionsInNxRspackPlugin(tree, rspackConfigText, sourceFile, rspackConfig); + updateSources(); + + setOptionsInLegacyNxPlugin(tree, rspackConfigText, sourceFile, rspackConfig); +} + +function setOptionsInRspackConfig( + tree: Tree, + text: string, + sourceFile: ts.SourceFile, + rspackConfig: string, + pluginOptions: ExtractedOptions +): void { + const { default: defaultOptions, ...configurationOptions } = pluginOptions; + + const optionsSelector = + 'VariableStatement:has(VariableDeclaration:has(Identifier[name=options]))'; + const optionsVariable = tsquery( + sourceFile, + optionsSelector + )[0]; + + // This is assuming the `options` variable will be available since it's what the + // `convert-config-to-rspack-plugin` generates + + let defaultOptionsObject: ts.ObjectLiteralExpression; + const optionsObject = tsquery( + optionsVariable, + 'ObjectLiteralExpression' + )[0]; + if (optionsObject.properties.length === 0) { + defaultOptionsObject = ts.factory.createObjectLiteralExpression( + Object.entries(defaultOptions).map(([key, value]) => + toPropertyAssignment(key, value) + ) + ); + } else { + // filter out the default options that are already in the options object + // the existing options override the options from the project.json file + const filteredDefaultOptions = Object.entries(defaultOptions).filter( + ([key]) => + !optionsObject.properties.some( + (property) => + ts.isPropertyAssignment(property) && + ts.isIdentifier(property.name) && + property.name.text === key + ) + ); + defaultOptionsObject = ts.factory.createObjectLiteralExpression([ + ...optionsObject.properties, + ...filteredDefaultOptions.map(([key, value]) => + toPropertyAssignment(key, value) + ), + ]); + } + + /** + * const configValues = { + * build: { + * default: { ... }, + * configuration1: { ... }, + * configuration2: { ... }, + * } + */ + const configValuesVariable = ts.factory.createVariableStatement( + undefined, + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + 'configValues', + undefined, + undefined, + ts.factory.createObjectLiteralExpression( + [ + ts.factory.createPropertyAssignment( + 'build', + ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment( + 'default', + defaultOptionsObject + ), + ...(configurationOptions + ? Object.entries(configurationOptions).map(([key, value]) => + ts.factory.createPropertyAssignment( + key, + ts.factory.createObjectLiteralExpression( + Object.entries(value).map(([key, value]) => + toPropertyAssignment(key, value) + ) + ) + ) + ) + : []), + ]) + ), + ], + true + ) + ), + ], + ts.NodeFlags.Const + ) + ); + + text = `${text.slice(0, optionsVariable.getStart())} + // These options were migrated by @nx/rspack:convert-to-inferred from + // the project.json file and merged with the options in this file + ${ts + .createPrinter() + .printNode(ts.EmitHint.Unspecified, configValuesVariable, sourceFile)} + + // Determine the correct configValue to use based on the configuration + const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default'; + + const buildOptions = { + ...configValues.build.default, + ...configValues.build[configuration], + };${text.slice(optionsVariable.getEnd())}`; + + // These are comments written by the `convert-config-to-rspack-plugin` that are no longer needed + text = text + .replace( + `// This file was migrated using @nx/rspack:convert-config-to-rspack-plugin from your './rspack.config.old.js'`, + '' + ) + .replace( + '// Please check that the options here are correct as they were moved from the old rspack.config.js to this file.', + '' + ); + + tree.write(rspackConfig, text); +} + +function setOptionsInNxRspackPlugin( + tree: Tree, + text: string, + sourceFile: ts.SourceFile, + rspackConfig: string +): void { + const nxAppRspackPluginSelector = + 'PropertyAssignment:has(Identifier[name=plugins]) NewExpression:has(Identifier[name=NxAppRspackPlugin])'; + const nxAppRspackPlugin = tsquery( + sourceFile, + nxAppRspackPluginSelector + )[0]; + + // the NxAppRspackPlugin must exists, otherwise, the migration doesn't run and we wouldn't reach this point + const updatedNxAppRspackPlugin = ts.factory.updateNewExpression( + nxAppRspackPlugin, + nxAppRspackPlugin.expression, + undefined, + [ts.factory.createIdentifier('buildOptions')] + ); + + text = `${text.slice(0, nxAppRspackPlugin.getStart())}${ts + .createPrinter() + .printNode( + ts.EmitHint.Unspecified, + updatedNxAppRspackPlugin, + sourceFile + )}${text.slice(nxAppRspackPlugin.getEnd())}`; + + tree.write(rspackConfig, text); +} + +function setOptionsInLegacyNxPlugin( + tree: Tree, + text: string, + sourceFile: ts.SourceFile, + rspackConfig: string +): void { + const legacyNxPluginSelector = + 'AwaitExpression CallExpression:has(Identifier[name=useLegacyNxPlugin])'; + const legacyNxPlugin = tsquery( + sourceFile, + legacyNxPluginSelector + )[0]; + + // we're assuming the `useLegacyNxPlugin` function is being called since it's what the `convert-config-to-rspack-plugin` generates + // we've already "ensured" that the `convert-config-to-rspack-plugin` was run by checking for the `NxAppRspackPlugin` in the project validation + const updatedLegacyNxPlugin = ts.factory.updateCallExpression( + legacyNxPlugin, + legacyNxPlugin.expression, + undefined, + [legacyNxPlugin.arguments[0], ts.factory.createIdentifier('buildOptions')] + ); + + text = `${text.slice(0, legacyNxPlugin.getStart())}${ts + .createPrinter() + .printNode( + ts.EmitHint.Unspecified, + updatedLegacyNxPlugin, + sourceFile + )}${text.slice(legacyNxPlugin.getEnd())}`; + + tree.write(rspackConfig, text); +} + +function getMemoryLimitFromServeTarget( + context: TransformerContext, + configName: string | undefined +): number | undefined { + const { targets } = context.projectGraph.nodes[context.projectName].data; + + const serveTarget = Object.values(targets).find( + (target) => + target.executor === '@nx/rspack:dev-server' || + target.executor === '@nrwl/web:dev-server' + ); + + if (!serveTarget) { + return undefined; + } + + if (configName && serveTarget.configurations?.[configName]) { + return ( + serveTarget.configurations[configName].options?.memoryLimit ?? + serveTarget.options?.memoryLimit + ); + } + + return serveTarget.options?.memoryLimit; +} diff --git a/packages/rspack/src/generators/convert-to-inferred/utils/index.ts b/packages/rspack/src/generators/convert-to-inferred/utils/index.ts new file mode 100644 index 0000000000..a453ed810b --- /dev/null +++ b/packages/rspack/src/generators/convert-to-inferred/utils/index.ts @@ -0,0 +1,3 @@ +export * from './build-post-target-transformer'; +export * from './serve-post-target-transformer'; +export * from './types'; diff --git a/packages/rspack/src/generators/convert-to-inferred/utils/serve-post-target-transformer.ts b/packages/rspack/src/generators/convert-to-inferred/utils/serve-post-target-transformer.ts new file mode 100644 index 0000000000..b870ef542a --- /dev/null +++ b/packages/rspack/src/generators/convert-to-inferred/utils/serve-post-target-transformer.ts @@ -0,0 +1,376 @@ +import { + parseTargetString, + readJson, + readTargetOptions, + type ExecutorContext, + type ProjectsConfigurations, + type TargetConfiguration, + type Tree, +} from '@nx/devkit'; +import { + processTargetOutputs, + toProjectRelativePath, +} from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { basename, resolve } from 'path'; +import * as ts from 'typescript'; +import type { RspackOptionsNormalized } from '@rspack/core'; +import { buildServePath } from '../../../executors/dev-server/lib/serve-path'; +import type { DevServerExecutorSchema as DevServerExecutorOptions } from '../../../executors/dev-server/schema'; +import { toPropertyAssignment } from './ast'; +import type { MigrationContext, TransformerContext } from './types'; + +export function servePostTargetTransformerFactory( + migrationContext: MigrationContext +) { + return async function servePostTargetTransformer( + target: TargetConfiguration, + tree: Tree, + projectDetails: { projectName: string; root: string }, + inferredTarget: TargetConfiguration + ): Promise { + const context: TransformerContext = { + ...migrationContext, + projectName: projectDetails.projectName, + projectRoot: projectDetails.root, + }; + + const { devServerOptions, rspackConfigPath } = await processOptions( + tree, + target, + context + ); + + updateRspackConfig(tree, rspackConfigPath, devServerOptions, context); + + if (target.outputs) { + processTargetOutputs(target, [], inferredTarget, { + projectName: projectDetails.projectName, + projectRoot: projectDetails.root, + }); + } + + return target; + }; +} + +type RspackConfigDevServerOptions = RspackOptionsNormalized['devServer']; +type ExtractedOptions = { + default: RspackConfigDevServerOptions; + [configName: string]: RspackConfigDevServerOptions; +}; + +async function processOptions( + tree: Tree, + target: TargetConfiguration, + context: TransformerContext +): Promise<{ + devServerOptions: ExtractedOptions; + rspackConfigPath: string; +}> { + const executorContext = { + cwd: process.cwd(), + nxJsonConfiguration: readJson(tree, 'nx.json'), + projectGraph: context.projectGraph, + projectName: context.projectName, + projectsConfigurations: Object.entries(context.projectGraph.nodes).reduce( + (acc, [projectName, project]) => { + acc.projects[projectName] = project.data; + return acc; + }, + { version: 1, projects: {} } as ProjectsConfigurations + ), + root: context.workspaceRoot, + } as ExecutorContext; + const buildTarget = parseTargetString( + target.options.buildTarget, + executorContext + ); + const buildOptions = readTargetOptions(buildTarget, executorContext); + + // it must exist, we validated it in the project filter + const rspackConfigPath = buildOptions.rspackConfig; + + const defaultOptions = extractDevServerOptions(target.options, context); + applyDefaults(defaultOptions, buildOptions); + const devServerOptions: ExtractedOptions = { + default: defaultOptions, + }; + + if (target.configurations && Object.keys(target.configurations).length) { + for (const [configName, config] of Object.entries(target.configurations)) { + devServerOptions[configName] = extractDevServerOptions(config, context); + } + } + + return { devServerOptions, rspackConfigPath: rspackConfigPath }; +} + +function extractDevServerOptions( + options: DevServerExecutorOptions, + context: TransformerContext +): RspackConfigDevServerOptions { + const devServerOptions: RspackConfigDevServerOptions = {}; + + for (const [key, value] of Object.entries(options)) { + if (key === 'ssl' || key === 'sslCert' || key === 'sslKey') { + if (key === 'ssl' || 'ssl' in options) { + if (options.ssl) { + devServerOptions.server = { type: 'https' }; + + if (options.sslCert && options.sslKey) { + devServerOptions.server.options = {}; + devServerOptions.server.options.cert = toProjectRelativePath( + options.sslCert, + context.projectRoot + ); + devServerOptions.server.options.key = toProjectRelativePath( + options.sslKey, + context.projectRoot + ); + } else if (options.sslCert) { + context.logger.addLog({ + executorName: '@nx/rspack:dev-server', + log: 'The "sslCert" option was set but "sslKey" was missing and "ssl" was set to "true". This means that "sslCert" was ignored by the executor. It has been removed from the options.', + project: context.projectName, + }); + } else if (options.sslKey) { + context.logger.addLog({ + executorName: '@nx/rspack:dev-server', + log: 'The "sslKey" option was set but "sslCert" was missing and "ssl" was set to "true". This means that "sslKey" was ignored by the executor. It has been removed from the options.', + project: context.projectName, + }); + } + } else if (options.sslCert || options.sslKey) { + context.logger.addLog({ + executorName: '@nx/rspack:dev-server', + log: 'The "sslCert" and/or "sslKey" were set with "ssl" set to "false". This means they were ignored by the executor. They have been removed from the options.', + project: context.projectName, + }); + } + delete options.ssl; + delete options.sslCert; + delete options.sslKey; + } else if (options.sslCert || options.sslKey) { + context.logger.addLog({ + executorName: '@nx/rspack:dev-server', + log: 'The "sslCert" and/or "sslKey" were set but the "ssl" was not set. This means they were ignored by the executor. They have been removed from the options.', + project: context.projectName, + }); + delete options.sslCert; + delete options.sslKey; + } + } else if (key === 'buildTarget') { + delete options.buildTarget; + } else { + devServerOptions[key] = value; + delete options[key]; + } + } + + return devServerOptions; +} + +function applyDefaults( + options: RspackConfigDevServerOptions, + buildOptions: any +) { + if (options.port === undefined) { + options.port = 4200; + } + + options.headers = { 'Access-Control-Allow-Origin': '*' }; + + const servePath = buildServePath(buildOptions); + options.historyApiFallback = { + index: buildOptions.index && `${servePath}${basename(buildOptions.index)}`, + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + }; +} + +function getProxyConfig(root: string, proxyConfig: string) { + const proxyPath = resolve(root, proxyConfig); + return require(proxyPath); +} + +function updateRspackConfig( + tree: Tree, + rspackConfigPath: string, + devServerOptions: ExtractedOptions, + context: TransformerContext +): void { + let sourceFile: ts.SourceFile; + let rspackConfigText: string; + + const updateSources = () => { + rspackConfigText = tree.read(rspackConfigPath, 'utf-8'); + sourceFile = tsquery.ast(rspackConfigText); + }; + updateSources(); + + setOptionsInRspackConfig( + tree, + rspackConfigText, + sourceFile, + rspackConfigPath, + devServerOptions + ); + updateSources(); + + setDevServerOptionsInRspackConfig( + tree, + rspackConfigText, + sourceFile, + rspackConfigPath, + context + ); +} + +function setOptionsInRspackConfig( + tree: Tree, + text: string, + sourceFile: ts.SourceFile, + rspackConfigPath: string, + devServerOptions: ExtractedOptions +) { + const { default: defaultOptions, ...configurationOptions } = devServerOptions; + + const configValuesSelector = + 'VariableDeclaration:has(Identifier[name=configValues]) ObjectLiteralExpression'; + const configValuesObject = tsquery( + sourceFile, + configValuesSelector + )[0]; + + // configValues must exist at this point, we added it when processing the build target + + /** + * const configValues = { + * ... + * serve: { + * default: { ... }, + * configuration1: { ... }, + * ... + * }, + */ + const updatedConfigValuesObject = ts.factory.updateObjectLiteralExpression( + configValuesObject, + [ + ...configValuesObject.properties, + ts.factory.createPropertyAssignment( + 'serve', + ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment( + 'default', + ts.factory.createObjectLiteralExpression( + Object.entries(defaultOptions).map(([key, value]) => + toPropertyAssignment(key, value) + ) + ) + ), + ...(configurationOptions + ? Object.entries(configurationOptions).map(([key, value]) => + ts.factory.createPropertyAssignment( + key, + ts.factory.createObjectLiteralExpression( + Object.entries(value).map(([key, value]) => + toPropertyAssignment(key, value) + ) + ) + ) + ) + : []), + ]) + ), + ] + ); + + text = `${text.slice(0, configValuesObject.getStart())}${ts + .createPrinter() + .printNode( + ts.EmitHint.Unspecified, + updatedConfigValuesObject, + sourceFile + )}${text.slice(configValuesObject.getEnd())}`; + + tree.write(rspackConfigPath, text); + + sourceFile = tsquery.ast(text); + const buildOptionsSelector = + 'VariableStatement:has(VariableDeclaration:has(Identifier[name=buildOptions]))'; + const buildOptionsStatement = tsquery( + sourceFile, + buildOptionsSelector + )[0]; + + text = `${text.slice(0, buildOptionsStatement.getEnd())} + const devServerOptions = { + ...configValues.serve.default, + ...configValues.serve[configuration], + };${text.slice(buildOptionsStatement.getEnd())}`; + + tree.write(rspackConfigPath, text); +} + +function setDevServerOptionsInRspackConfig( + tree: Tree, + text: string, + sourceFile: ts.SourceFile, + rspackConfigPath: string, + context: TransformerContext +) { + const rspackConfigDevServerSelector = + 'ObjectLiteralExpression > PropertyAssignment:has(Identifier[name=devServer])'; + const rspackConfigDevServer = tsquery( + sourceFile, + rspackConfigDevServerSelector + )[0]; + if (rspackConfigDevServer) { + context.logger.addLog({ + executorName: '@nx/rspack:dev-server', + log: `The "devServer" option is already set in the rspack config. The migration doesn't support updating it. Please review it and make any necessary changes manually.`, + project: context.projectName, + }); + + text = `${text.slice( + 0, + rspackConfigDevServer.getStart() + )}// This is the untouched "devServer" option from the original rspack config. Please review it and make any necessary changes manually. + ${text.slice(rspackConfigDevServer.getStart())}`; + + tree.write(rspackConfigPath, text); + + // If the devServer property already exists, we don't know how to merge the + // options, so we leave it as is. + return; + } + + const rspackConfigSelector = + 'ObjectLiteralExpression:has(PropertyAssignment:has(Identifier[name=plugins]))'; + const rspackConfig = tsquery( + sourceFile, + rspackConfigSelector + )[0]; + + const updatedRspackConfig = ts.factory.updateObjectLiteralExpression( + rspackConfig, + [ + ts.factory.createPropertyAssignment( + 'devServer', + ts.factory.createIdentifier('devServerOptions') + ), + ...rspackConfig.properties, + ] + ); + + text = `${text.slice(0, rspackConfig.getStart())}${ts + .createPrinter() + .printNode( + ts.EmitHint.Unspecified, + updatedRspackConfig, + sourceFile + )}${text.slice(rspackConfig.getEnd())}`; + + tree.write(rspackConfigPath, text); +} diff --git a/packages/rspack/src/generators/convert-to-inferred/utils/types.ts b/packages/rspack/src/generators/convert-to-inferred/utils/types.ts new file mode 100644 index 0000000000..85b5c1646f --- /dev/null +++ b/packages/rspack/src/generators/convert-to-inferred/utils/types.ts @@ -0,0 +1,13 @@ +import type { ProjectGraph } from '@nx/devkit'; +import type { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; + +export type MigrationContext = { + logger: AggregatedLog; + projectGraph: ProjectGraph; + workspaceRoot: string; +}; + +export type TransformerContext = MigrationContext & { + projectName: string; + projectRoot: string; +}; diff --git a/packages/rspack/src/index.ts b/packages/rspack/src/index.ts index 10a9306018..188ed4c373 100644 --- a/packages/rspack/src/index.ts +++ b/packages/rspack/src/index.ts @@ -4,3 +4,4 @@ export * from './utils/config'; export * from './utils/with-nx'; export * from './utils/with-react'; export * from './utils/with-web'; +export * from './plugins/use-legacy-nx-plugin/use-legacy-nx-plugin'; diff --git a/packages/rspack/src/plugins/nx-app-rspack-plugin/nx-app-rspack-plugin.ts b/packages/rspack/src/plugins/nx-app-rspack-plugin/nx-app-rspack-plugin.ts index 3c24652e4d..63e68f6cb1 100644 --- a/packages/rspack/src/plugins/nx-app-rspack-plugin/nx-app-rspack-plugin.ts +++ b/packages/rspack/src/plugins/nx-app-rspack-plugin/nx-app-rspack-plugin.ts @@ -6,7 +6,6 @@ import type { import { normalizeOptions } from '../utils/plugins/normalize-options'; import { applyBaseConfig } from '../utils/apply-base-config'; import { applyWebConfig } from '../utils/apply-web-config'; -import { deleteOutputDir } from '../utils/delete-output-path'; /** * This plugin provides features to build Node and Web applications. @@ -48,9 +47,5 @@ export class NxAppRspackPlugin { useNormalizedEntry: true, }); } - - if (this.options.deleteOutputPath) { - deleteOutputDir(this.options.root, this.options.outputPath); - } } } diff --git a/packages/rspack/src/plugins/use-legacy-nx-plugin/use-legacy-nx-plugin.ts b/packages/rspack/src/plugins/use-legacy-nx-plugin/use-legacy-nx-plugin.ts new file mode 100644 index 0000000000..53a055cb0c --- /dev/null +++ b/packages/rspack/src/plugins/use-legacy-nx-plugin/use-legacy-nx-plugin.ts @@ -0,0 +1,89 @@ +import { + type ExecutorContext, + readCachedProjectGraph, + readProjectsConfigurationFromProjectGraph, + workspaceRoot, +} from '@nx/devkit'; +import type { NxRspackExecutionContext } from '../../utils/config'; +import type { NxAppRspackPluginOptions } from '../utils/models'; +import type { Compiler, Configuration } from '@rspack/core'; +import { normalizeOptions } from '../utils/plugins/normalize-options'; +import { readNxJson } from 'nx/src/config/configuration'; + +/** + * This function is used to wrap the legacy plugin function to be used with the `composePlugins` function. + * Initially the rspack config would be passed to the legacy plugin function and the options would be passed as a second argument. + * example: + * module.exports = composePlugins( + withNx(), + (config) => { + return config; + } + ); + + Since composePlugins is async, this function is used to wrap the legacy plugin function to be async. + Using the nxUseLegacyPlugin function, the first argument is the legacy plugin function and the second argument is the options. + The context options are created and passed to the legacy plugin function. + + module.exports = async () => ({ + plugins: [ + ...otherPlugins, + await nxUseLegacyPlugin(require({path}), options), + ], + }); + * @param fn The legacy plugin function usually from `combinedPlugins` + * @param executorOptions The options passed usually inside the executor or the config file + * @returns Rspack configuration + */ +export async function useLegacyNxPlugin( + fn: ( + config: Configuration, + ctx: NxRspackExecutionContext + ) => Promise, + executorOptions: NxAppRspackPluginOptions +) { + if (global.NX_GRAPH_CREATION) { + return; + } + const options = normalizeOptions(executorOptions); + + const projectGraph = readCachedProjectGraph(); + const projectName = process.env.NX_TASK_TARGET_PROJECT; + const project = projectGraph.nodes[projectName]; + const targetName = process.env.NX_TASK_TARGET_TARGET; + + const context: ExecutorContext = { + cwd: process.cwd(), + isVerbose: process.env.NX_VERBOSE_LOGGING === 'true', + root: workspaceRoot, + projectGraph, + projectsConfigurations: + readProjectsConfigurationFromProjectGraph(projectGraph), + nxJsonConfiguration: readNxJson(workspaceRoot), + target: project.data.targets[targetName], + targetName: targetName, + projectName: projectName, + }; + + const configuration = process.env.NX_TASK_TARGET_CONFIGURATION; + const ctx: NxRspackExecutionContext = { + context, + options: options as NxRspackExecutionContext['options'], + configuration, + }; + return { + apply(compiler: Compiler) { + compiler.hooks.beforeCompile.tapPromise('NxLegacyAsyncPlugin', () => { + return new Promise((resolve) => { + fn(compiler.options as Configuration, ctx).then((updated) => { + // Merge options back shallowly since it's a fully functional configuration. + // Most likely, the user modified the config in place, but this guarantees that updates are applied if users did something like: + // `return { ...config, plugins: [...config.plugins, new MyPlugin()] }` + Object.assign(compiler.options, updated); + resolve(); + }); + }); + }); + }, + }; +} diff --git a/packages/rspack/src/plugins/utils/apply-base-config.ts b/packages/rspack/src/plugins/utils/apply-base-config.ts index 708f23060d..030035bbb0 100644 --- a/packages/rspack/src/plugins/utils/apply-base-config.ts +++ b/packages/rspack/src/plugins/utils/apply-base-config.ts @@ -112,6 +112,7 @@ function applyNxIndependentConfig( hashFunction: config.output?.hashFunction ?? 'xxhash64', // Disabled for performance pathinfo: config.output?.pathinfo ?? false, + clean: options.deleteOutputPath, }; config.watch = options.watch; diff --git a/packages/rspack/src/plugins/utils/delete-output-path.ts b/packages/rspack/src/plugins/utils/delete-output-path.ts deleted file mode 100644 index 4b2b24f28a..0000000000 --- a/packages/rspack/src/plugins/utils/delete-output-path.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { rmSync } from 'fs'; -import { resolve } from 'path'; - -/** - * Delete an output directory, but error out if it's the root of the project. - */ -export function deleteOutputDir(root: string, outputPath: string) { - const resolvedOutputPath = resolve(root, outputPath); - if (resolvedOutputPath === root) { - throw new Error('Output path MUST not be project root directory!'); - } - - rmSync(resolvedOutputPath, { recursive: true, force: true }); -} diff --git a/packages/rspack/src/utils/create-compiler.ts b/packages/rspack/src/utils/create-compiler.ts index be046c9934..3ba02bd586 100644 --- a/packages/rspack/src/utils/create-compiler.ts +++ b/packages/rspack/src/utils/create-compiler.ts @@ -17,7 +17,9 @@ export async function createCompiler( ): Promise { const config = await getRspackConfigs(options, context); - validateConfig(config); + if (!options.standardRspackConfigFunction) { + validateConfig(config); + } return rspack(config); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b560c0ebd5..9121ff7b21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -340,6 +340,9 @@ importers: '@nx/react': specifier: 20.2.0-beta.2 version: 20.2.0-beta.2(@babel/traverse@7.25.6)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(@zkochan/js-yaml@0.0.7)(eslint@8.57.0)(nx@20.2.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4)(verdaccio@5.32.2(encoding@0.1.13)(typanion@3.14.0))(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + '@nx/rspack': + specifier: 20.2.0-beta.2 + version: 20.2.0-beta.2(qqfm4c7f7lave6shde5djyndcy) '@nx/storybook': specifier: 20.2.0-beta.2 version: 20.2.0-beta.2(@babel/traverse@7.25.6)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(@zkochan/js-yaml@0.0.7)(cypress@13.13.0)(eslint@8.57.0)(nx@20.2.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(typescript@5.5.4)(verdaccio@5.32.2(encoding@0.1.13)(typanion@3.14.0)) @@ -1011,7 +1014,7 @@ importers: version: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4) webpack-dev-server: specifier: 5.0.4 - version: 5.0.4(webpack-cli@5.1.4)(webpack@5.88.0) + version: 5.0.4(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) webpack-merge: specifier: ^5.8.0 version: 5.10.0 @@ -3711,12 +3714,21 @@ packages: '@mediapipe/tasks-vision@0.10.8': resolution: {integrity: sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==} + '@module-federation/bridge-react-webpack-plugin@0.6.11': + resolution: {integrity: sha512-VUD7g1RIom7KtQaO7bcPd7sCzsO6jeRVwOSx5smFr9K6FpkWeiwWtJmhyuhc0uzstzVdkOk77pqMP0xmrXpV+g==} + '@module-federation/bridge-react-webpack-plugin@0.6.9': resolution: {integrity: sha512-KXTPO0vkrtHEIcthU3TIQEkPxoytcmdyNXRwOojZEVQhqEefykAek48ndFiVTmyOu2LW2EuzP49Le8zY7nESWQ==} '@module-federation/bridge-react-webpack-plugin@0.7.6': resolution: {integrity: sha512-eD1JZDQ+h5WLdA58MmAE1DzLwvFaGJeeam3Tswc/sEUb4QGT86X4Fme+dMTBRYRoAq/tRYql3DlVTFhdmrUVzg==} + '@module-federation/data-prefetch@0.6.11': + resolution: {integrity: sha512-cNCk1YJJal2RvMKu2S413GVHlEUMYbzzzJbWBzZXwcW3DupOeLGs2ENvl32whAvF1RyOlf6LRYcypqE22LUxBQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + '@module-federation/data-prefetch@0.6.9': resolution: {integrity: sha512-rpHxfHNkIiPA441GzXI6TMYjSrUjRWDwxJTvRQopX/P0jK5vKtNwT1UBTNF2DJkbtO1idljfhbrIufEg0OY72w==} peerDependencies: @@ -3729,6 +3741,15 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' + '@module-federation/dts-plugin@0.6.11': + resolution: {integrity: sha512-BRKfLuDuFou/Mg3MlatZN67HSIJ/M4t7mpxeYl93bu7q+87zzD+wUSrY+I+pGz+VEmN/LJ5TujMW4jS3XOP2Pw==} + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: '>=1.0.24' + peerDependenciesMeta: + vue-tsc: + optional: true + '@module-federation/dts-plugin@0.6.9': resolution: {integrity: sha512-uiMjjEFcMlOvRtNu8/tt7sJ5y7WTosTVym0V7lMQjgoeX0QesvZqRhgzw5gQcPcFvbk54RwTUI2rS8OEGScCFw==} peerDependencies: @@ -3747,6 +3768,20 @@ packages: vue-tsc: optional: true + '@module-federation/enhanced@0.6.11': + resolution: {integrity: sha512-billwprfdc/ehPFdwCNTdm0685pry0qvlhrT9UEYjqHDMHanXTWNQJJLqf5Tz8OzA2/ex6+y8yMcdeKJs+nXEQ==} + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: '>=1.0.24' + webpack: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + webpack: + optional: true + '@module-federation/enhanced@0.6.9': resolution: {integrity: sha512-4bEGQSE6zJ2FMdBTOrRiVjNNzWhUqzWEJGWbsr0bpLNAl4BVx2ah5MyKTrSYqaW//BRA2qc8rmrIreaIawr3kQ==} peerDependencies: @@ -3778,18 +3813,50 @@ packages: '@module-federation/error-codes@0.7.6': resolution: {integrity: sha512-XVzX/sRFj1h5JvOOVMoFppxq0t1t3o/AlEICHgWX+dybIwJgz9g4gihZOWVZfz5/xsKGcUwdH5X7Z2nkuYhJEw==} + '@module-federation/managers@0.6.11': + resolution: {integrity: sha512-HVw9eFTHCegRlWSmNbHXAnY19XHSj19RHHpjZ1Oo71DaHgjJAPJlg4izifcdWt0w+ObAQLOH1DacjYKMIT4W6Q==} + '@module-federation/managers@0.6.9': resolution: {integrity: sha512-q3AOQXcWWpdUZI1gDIi9j/UqcP+FJBYXj/e4pNp3QAteJwS/Ve9UP3y0hW27bIbAWZSSajWsYbf/+YLnktA/kQ==} '@module-federation/managers@0.7.6': resolution: {integrity: sha512-NW0LJ6TL13oN004D9e50EalcGZyTYHHgyaeKOc90Omb/HMeHxjyhHx7wl1TLRwVN2E5Rk+IO0JrwgrdlNMfAzg==} + '@module-federation/manifest@0.6.11': + resolution: {integrity: sha512-HLtGulXJQUdOAAXhkDhNOnTld1EsnjNr2GEscsKNmP42vEEqEm6A6jL8hdKS/hrDZJmPOps7XcEv426+jMTDpA==} + '@module-federation/manifest@0.6.9': resolution: {integrity: sha512-JMSPDpHODXOmTyJes8GJ950mbN7tqjQzqgFVUubDOVFOmlC0/MYaRzRPmkApz6d8nUfMbLZYzxNSaBHx8GP0/Q==} '@module-federation/manifest@0.7.6': resolution: {integrity: sha512-xBrFwLjDMUjKRnp+P4X29ZNyhgXSsp+SfrBxVsKJpEESOHalDoNClbo6gXvZAvkBZyo9sY3SJhAwduDwNkg04w==} + '@module-federation/node@2.5.21': + resolution: {integrity: sha512-RSSWlndPZtKOX0mmQkHUc4xmWl18oRZOV58S5Xm1X3YOSDxywLtOXcxaYRfUqufBGdEA+zeDaFryfE8feC61Qw==} + peerDependencies: + next: '*' + react: ^16||^17||^18 + react-dom: ^16||^17||^18 + webpack: ^5.40.0 + peerDependenciesMeta: + next: + optional: true + react: + optional: true + react-dom: + optional: true + + '@module-federation/rspack@0.6.11': + resolution: {integrity: sha512-l2AH5J1oDvChc61dOJTPBBiJGD+wwcqRVbbjTYTCtZdxFgY6uBhTj0zOLWaSLlXO5DNkr5PyuPH1HCfGWlDwPA==} + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: '>=1.0.24' + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + '@module-federation/rspack@0.6.9': resolution: {integrity: sha512-N5yBqN8ijSRZKd0kbIvpZNil0y8rFa8cREKI1QsW1+EYUKwOUBFwF55tFdTmNCKmpZqSEBtcNjRGZXknsYPQxg==} peerDependencies: @@ -3815,6 +3882,9 @@ packages: '@module-federation/runtime-tools@0.5.1': resolution: {integrity: sha512-nfBedkoZ3/SWyO0hnmaxuz0R0iGPSikHZOAZ0N/dVSQaIzlffUo35B5nlC2wgWIc0JdMZfkwkjZRrnuuDIJbzg==} + '@module-federation/runtime-tools@0.6.11': + resolution: {integrity: sha512-MGdCLaFfFyW6hTEaPKs8yEvOd9zvpaLADUL7WEaqWQ6XVt9GVATGDwA0muZT4+OFjtGsOgj5h5NGjZgIJxruSA==} + '@module-federation/runtime-tools@0.6.9': resolution: {integrity: sha512-AhsEBXo8IW1ATMKS1xfJaxBiHu9n5z6WUOAIWdPpWXXBJhTFgOs0K1xAod0xLJY4YH/B5cwEcHRPN3FEs2/0Ww==} @@ -3824,6 +3894,9 @@ packages: '@module-federation/runtime@0.5.1': resolution: {integrity: sha512-xgiMUWwGLWDrvZc9JibuEbXIbhXg6z2oUkemogSvQ4LKvrl/n0kbqP1Blk669mXzyWbqtSp6PpvNdwaE1aN5xQ==} + '@module-federation/runtime@0.6.11': + resolution: {integrity: sha512-UTuavwCybLftAe4VT7cCqj+BVNlZwda/xmqIPAeYX14o7gkYFyA6zkxOQqfNCaDnTMR/KBk6EvE49yA6/ht9UQ==} + '@module-federation/runtime@0.6.9': resolution: {integrity: sha512-G1x+6jyW5sW1X+TtWaKigGhwqiHE8MESvi3ntE9ICxwELAGBonmsqDqnLSrdEy6poBKslvPANPJr0Nn9pvW9lg==} @@ -3833,21 +3906,45 @@ packages: '@module-federation/sdk@0.5.1': resolution: {integrity: sha512-exvchtjNURJJkpqjQ3/opdbfeT2wPKvrbnGnyRkrwW5o3FH1LaST1tkiNviT6OXTexGaVc2DahbdniQHVtQ7pA==} + '@module-federation/sdk@0.6.11': + resolution: {integrity: sha512-Fj2ws9yL6mGAki9GdurcrIhdSg0L2Kfw7L6Dej/DidzAU4bwa3MT0s+87BPuOYFgm2UTMN3g+UrElC2NhsuulQ==} + '@module-federation/sdk@0.6.9': resolution: {integrity: sha512-xmTxb9LgncxPGsBrN6AT/+aHnFGv8swbeNl0PcSeVbXTGLu3Gp7j+5J+AhJoWNB++SLguRwBd8LjB1d8mNKLDg==} '@module-federation/sdk@0.7.6': resolution: {integrity: sha512-MFE+RtsHnutZOCp2eKpa3A/yzZ8tOPmjX7QRdVnB2qqR9JA2SH3ZP5+cYq76tzFQZvU1BCWAQVNMvqGOW2yVZQ==} + '@module-federation/third-party-dts-extractor@0.6.11': + resolution: {integrity: sha512-KEHF71/qmEhME1XK/0XpMHKaSRjwmINpul9iu5Z4UBNtoMIydq6SH41DsWF3HxAManhqe+ZwCxyoSn2Yxm5d0Q==} + '@module-federation/third-party-dts-extractor@0.6.9': resolution: {integrity: sha512-im00IQyX/siJz+SaAmJo6vGmMBig7UYzcrPD1N5NeiZonxdT1RZk9iXUP419UESgovYy4hM6w4qdCq6PMMl2bw==} '@module-federation/third-party-dts-extractor@0.7.6': resolution: {integrity: sha512-JME76/rgr41AKXG6kUTQXdQJiMCypN3qHOgPv4VuIag10UdLo/0gdeN6PYronvYmvPOQMfYev80GcEwl4l531A==} + '@module-federation/utilities@3.1.17': + resolution: {integrity: sha512-dmbLxZwgYgtTJ09HvFZCzAPgoRkAEjqQOEcuBZm/+GSHuAwZq6SMaOs7RdTHH0by7VvUm4O+iMQ7zMu/F4vuqw==} + peerDependencies: + next: '*' + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + webpack: ^5.40.0 + peerDependenciesMeta: + next: + optional: true + react: + optional: true + react-dom: + optional: true + '@module-federation/webpack-bundler-runtime@0.5.1': resolution: {integrity: sha512-mMhRFH0k2VjwHt3Jol9JkUsmI/4XlrAoBG3E0o7HoyoPYv1UFOWyqAflfANcUPgbYpvqmyLzDcO+3IT36LXnrA==} + '@module-federation/webpack-bundler-runtime@0.6.11': + resolution: {integrity: sha512-s9VtE+cthnCsutl0o48qBRaLP3oQGA1FESLG9dwIHpUN9G7zRtewf0HjlCFFZG3ORRyTKBiJUi5qDWt9ky7XwQ==} + '@module-federation/webpack-bundler-runtime@0.6.9': resolution: {integrity: sha512-ME1MjNT/a4MFI3HaJDM06olJ+/+H8lk4oDOdwwEZI2JSH3UoqCDrMcjSKCjBNMGzza57AowGobo1LHQeY8yZ8Q==} @@ -4974,6 +5071,12 @@ packages: '@nx/react@20.2.0-beta.2': resolution: {integrity: sha512-qFHJ72vbuOQ2NMOFtTmnPMRYQBCadWQDsfOycmXEYrTmoGHc30hMiBRo/nD+urHre7IWAi+rlwSeNV3fO4vGXw==} + '@nx/rspack@20.2.0-beta.2': + resolution: {integrity: sha512-dBzne7y4GuQlA53aD2Rfpai/tmyWiY53EaB1dc3pwapNRY3srWlGVUa4GgiqjdDsBULvt10+AHn1P8qFjZ/MHQ==} + peerDependencies: + '@module-federation/enhanced': ~0.6.0 + '@module-federation/node': ~2.5.10 + '@nx/storybook@20.2.0-beta.2': resolution: {integrity: sha512-yUkdQ6G0YOVmLvX03P03u+oR+ieJilb2xYDs+nYAegFykfjOK+DFBm6/jQct7TnkwUb322ACg7FCupBHCFzpsw==} @@ -20457,6 +20560,12 @@ snapshots: '@mediapipe/tasks-vision@0.10.8': {} + '@module-federation/bridge-react-webpack-plugin@0.6.11': + dependencies: + '@module-federation/sdk': 0.6.11 + '@types/semver': 7.5.8 + semver: 7.6.3 + '@module-federation/bridge-react-webpack-plugin@0.6.9': dependencies: '@module-federation/sdk': 0.6.9 @@ -20469,6 +20578,14 @@ snapshots: '@types/semver': 7.5.8 semver: 7.6.3 + '@module-federation/data-prefetch@0.6.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@module-federation/runtime': 0.6.11 + '@module-federation/sdk': 0.6.11 + fs-extra: 9.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@module-federation/data-prefetch@0.6.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@module-federation/runtime': 0.6.9 @@ -20485,6 +20602,30 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@module-federation/dts-plugin@0.6.11(typescript@5.5.4)': + dependencies: + '@module-federation/managers': 0.6.11 + '@module-federation/sdk': 0.6.11 + '@module-federation/third-party-dts-extractor': 0.6.11 + adm-zip: 0.5.16 + ansi-colors: 4.1.3 + axios: 1.7.7 + chalk: 3.0.0 + fs-extra: 9.1.0 + isomorphic-ws: 5.0.0(ws@8.18.0) + koa: 2.15.3 + lodash.clonedeepwith: 4.5.0 + log4js: 6.9.1 + node-schedule: 2.1.1 + rambda: 9.3.0 + typescript: 5.5.4 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + '@module-federation/dts-plugin@0.6.9(typescript@5.5.4)': dependencies: '@module-federation/managers': 0.6.9 @@ -20534,6 +20675,29 @@ snapshots: - supports-color - utf-8-validate + '@module-federation/enhanced@0.6.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4)(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4))': + dependencies: + '@module-federation/bridge-react-webpack-plugin': 0.6.11 + '@module-federation/data-prefetch': 0.6.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@module-federation/dts-plugin': 0.6.11(typescript@5.5.4) + '@module-federation/managers': 0.6.11 + '@module-federation/manifest': 0.6.11(typescript@5.5.4) + '@module-federation/rspack': 0.6.11(typescript@5.5.4) + '@module-federation/runtime-tools': 0.6.11 + '@module-federation/sdk': 0.6.11 + btoa: 1.2.1 + upath: 2.0.1 + optionalDependencies: + typescript: 5.5.4 + webpack: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4) + transitivePeerDependencies: + - bufferutil + - debug + - react + - react-dom + - supports-color + - utf-8-validate + '@module-federation/enhanced@0.6.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4)(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4))': dependencies: '@module-federation/bridge-react-webpack-plugin': 0.6.9 @@ -20582,6 +20746,12 @@ snapshots: '@module-federation/error-codes@0.7.6': {} + '@module-federation/managers@0.6.11': + dependencies: + '@module-federation/sdk': 0.6.11 + find-pkg: 2.0.0 + fs-extra: 9.1.0 + '@module-federation/managers@0.6.9': dependencies: '@module-federation/sdk': 0.6.9 @@ -20594,6 +20764,21 @@ snapshots: find-pkg: 2.0.0 fs-extra: 9.1.0 + '@module-federation/manifest@0.6.11(typescript@5.5.4)': + dependencies: + '@module-federation/dts-plugin': 0.6.11(typescript@5.5.4) + '@module-federation/managers': 0.6.11 + '@module-federation/sdk': 0.6.11 + chalk: 3.0.0 + find-pkg: 2.0.0 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - typescript + - utf-8-validate + - vue-tsc + '@module-federation/manifest@0.6.9(typescript@5.5.4)': dependencies: '@module-federation/dts-plugin': 0.6.9(typescript@5.5.4) @@ -20624,6 +20809,44 @@ snapshots: - utf-8-validate - vue-tsc + '@module-federation/node@2.5.21(next@14.2.16(@babel/core@7.25.2)(@playwright/test@1.47.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.55.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4)(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4))': + dependencies: + '@module-federation/enhanced': 0.6.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4)(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + '@module-federation/runtime': 0.6.11 + '@module-federation/sdk': 0.6.11 + '@module-federation/utilities': 3.1.17(next@14.2.16(@babel/core@7.25.2)(@playwright/test@1.47.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.55.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + btoa: 1.2.1 + encoding: 0.1.13 + node-fetch: 2.7.0(encoding@0.1.13) + webpack: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4) + optionalDependencies: + next: 14.2.16(@babel/core@7.25.2)(@playwright/test@1.47.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.55.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - typescript + - utf-8-validate + - vue-tsc + + '@module-federation/rspack@0.6.11(typescript@5.5.4)': + dependencies: + '@module-federation/bridge-react-webpack-plugin': 0.6.11 + '@module-federation/dts-plugin': 0.6.11(typescript@5.5.4) + '@module-federation/managers': 0.6.11 + '@module-federation/manifest': 0.6.11(typescript@5.5.4) + '@module-federation/runtime-tools': 0.6.11 + '@module-federation/sdk': 0.6.11 + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + '@module-federation/rspack@0.6.9(typescript@5.5.4)': dependencies: '@module-federation/bridge-react-webpack-plugin': 0.6.9 @@ -20661,6 +20884,11 @@ snapshots: '@module-federation/runtime': 0.5.1 '@module-federation/webpack-bundler-runtime': 0.5.1 + '@module-federation/runtime-tools@0.6.11': + dependencies: + '@module-federation/runtime': 0.6.11 + '@module-federation/webpack-bundler-runtime': 0.6.11 + '@module-federation/runtime-tools@0.6.9': dependencies: '@module-federation/runtime': 0.6.9 @@ -20675,6 +20903,10 @@ snapshots: dependencies: '@module-federation/sdk': 0.5.1 + '@module-federation/runtime@0.6.11': + dependencies: + '@module-federation/sdk': 0.6.11 + '@module-federation/runtime@0.6.9': dependencies: '@module-federation/sdk': 0.6.9 @@ -20686,12 +20918,20 @@ snapshots: '@module-federation/sdk@0.5.1': {} + '@module-federation/sdk@0.6.11': {} + '@module-federation/sdk@0.6.9': {} '@module-federation/sdk@0.7.6': dependencies: isomorphic-rslog: 0.0.6 + '@module-federation/third-party-dts-extractor@0.6.11': + dependencies: + find-pkg: 2.0.0 + fs-extra: 9.1.0 + resolve: 1.22.8 + '@module-federation/third-party-dts-extractor@0.6.9': dependencies: find-pkg: 2.0.0 @@ -20704,11 +20944,25 @@ snapshots: fs-extra: 9.1.0 resolve: 1.22.8 + '@module-federation/utilities@3.1.17(next@14.2.16(@babel/core@7.25.2)(@playwright/test@1.47.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.55.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4))': + dependencies: + '@module-federation/sdk': 0.6.11 + webpack: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4) + optionalDependencies: + next: 14.2.16(@babel/core@7.25.2)(@playwright/test@1.47.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.55.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@module-federation/webpack-bundler-runtime@0.5.1': dependencies: '@module-federation/runtime': 0.5.1 '@module-federation/sdk': 0.5.1 + '@module-federation/webpack-bundler-runtime@0.6.11': + dependencies: + '@module-federation/runtime': 0.6.11 + '@module-federation/sdk': 0.6.11 + '@module-federation/webpack-bundler-runtime@0.6.9': dependencies: '@module-federation/runtime': 0.6.9 @@ -22225,6 +22479,61 @@ snapshots: - vue-tsc - webpack + '@nx/rspack@20.2.0-beta.2(qqfm4c7f7lave6shde5djyndcy)': + dependencies: + '@module-federation/enhanced': 0.7.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4)(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + '@module-federation/node': 2.5.21(next@14.2.16(@babel/core@7.25.2)(@playwright/test@1.47.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.55.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4)(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + '@nx/devkit': 20.2.0-beta.2(nx@20.2.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11))) + '@nx/js': 20.2.0-beta.2(@babel/traverse@7.25.6)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(nx@20.2.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(typescript@5.5.4)(verdaccio@5.32.2(encoding@0.1.13)(typanion@3.14.0)) + '@nx/web': 20.2.0-beta.2(@babel/traverse@7.25.6)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(nx@20.2.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(typescript@5.5.4)(verdaccio@5.32.2(encoding@0.1.13)(typanion@3.14.0)) + '@phenomnomnominal/tsquery': 5.0.1(typescript@5.5.4) + '@rspack/core': 1.1.2(@swc/helpers@0.5.11) + '@rspack/dev-server': 1.0.9(@rspack/core@1.1.2(@swc/helpers@0.5.11))(@types/express@4.17.14)(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + '@rspack/plugin-react-refresh': 1.0.0(react-refresh@0.10.0) + autoprefixer: 10.4.13(postcss@8.4.38) + browserslist: 4.23.3 + chalk: 4.1.2 + css-loader: 6.11.0(@rspack/core@1.1.2(@swc/helpers@0.5.11))(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + enquirer: 2.3.6 + express: 4.21.0 + fork-ts-checker-webpack-plugin: 7.2.13(typescript@5.5.4)(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + http-proxy-middleware: 3.0.3 + less-loader: 11.1.0(less@4.1.3)(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + license-webpack-plugin: 4.0.2(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + loader-utils: 2.0.3 + postcss: 8.4.38 + postcss-import: 14.1.0(postcss@8.4.38) + postcss-loader: 8.1.1(@rspack/core@1.1.2(@swc/helpers@0.5.11))(postcss@8.4.38)(typescript@5.5.4)(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + sass: 1.55.0 + sass-loader: 12.6.0(sass@1.55.0)(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + source-map-loader: 5.0.0(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + style-loader: 3.3.4(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + tslib: 2.7.0 + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - '@babel/traverse' + - '@swc-node/register' + - '@swc/core' + - '@swc/helpers' + - '@swc/wasm' + - '@types/express' + - '@types/node' + - bufferutil + - debug + - fibers + - less + - node-sass + - nx + - react-refresh + - sass-embedded + - supports-color + - typescript + - utf-8-validate + - verdaccio + - vue-template-compiler + - webpack + - webpack-cli + '@nx/storybook@20.2.0-beta.2(@babel/traverse@7.25.6)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(@zkochan/js-yaml@0.0.7)(cypress@13.13.0)(eslint@8.57.0)(nx@20.2.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(typescript@5.5.4)(verdaccio@5.32.2(encoding@0.1.13)(typanion@3.14.0))': dependencies: '@nx/cypress': 20.2.0-beta.2(@babel/traverse@7.25.6)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(@zkochan/js-yaml@0.0.7)(cypress@13.13.0)(eslint@8.57.0)(nx@20.2.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(typescript@5.5.4)(verdaccio@5.32.2(encoding@0.1.13)(typanion@3.14.0)) @@ -22332,7 +22641,7 @@ snapshots: tsconfig-paths-webpack-plugin: 4.0.0 tslib: 2.7.0 webpack: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4) - webpack-dev-server: 5.0.4(webpack-cli@5.1.4)(webpack@5.88.0) + webpack-dev-server: 5.0.4(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) webpack-node-externals: 3.0.0 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.19.5)(webpack-cli@5.1.4)))(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) transitivePeerDependencies: @@ -22706,7 +23015,7 @@ snapshots: webpack: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4) optionalDependencies: type-fest: 3.13.1 - webpack-dev-server: 5.0.4(webpack-cli@5.1.4)(webpack@5.88.0) + webpack-dev-server: 5.0.4(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) webpack-hot-middleware: 2.26.1 '@pnpm/lockfile-types@6.0.0': @@ -23263,7 +23572,7 @@ snapshots: mime-types: 2.1.35 p-retry: 4.6.2 webpack-dev-middleware: 7.4.2(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) - webpack-dev-server: 5.0.4(webpack-cli@5.1.4)(webpack@5.88.0) + webpack-dev-server: 5.0.4(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) ws: 8.18.0 transitivePeerDependencies: - '@types/express' @@ -25523,7 +25832,7 @@ snapshots: webpack: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0) optionalDependencies: - webpack-dev-server: 5.0.4(webpack-cli@5.1.4)(webpack@5.88.0) + webpack-dev-server: 5.0.4(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) '@widgetbot/embed-api@1.2.15': dependencies: @@ -27812,7 +28121,6 @@ snapshots: encoding@0.1.13: dependencies: iconv-lite: 0.6.3 - optional: true end-of-stream@1.4.4: dependencies: @@ -33581,6 +33889,18 @@ snapshots: semver: 7.6.3 webpack: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4) + postcss-loader@8.1.1(@rspack/core@1.1.2(@swc/helpers@0.5.11))(postcss@8.4.38)(typescript@5.5.4)(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)): + dependencies: + cosmiconfig: 9.0.0(typescript@5.5.4) + jiti: 1.21.6 + postcss: 8.4.38 + semver: 7.6.3 + optionalDependencies: + '@rspack/core': 1.1.2(@swc/helpers@0.5.11) + webpack: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4) + transitivePeerDependencies: + - typescript + postcss-loader@8.1.1(@rspack/core@1.1.2(@swc/helpers@0.5.11))(postcss@8.4.41)(typescript@5.5.4)(webpack@5.94.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.23.0)(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))): dependencies: cosmiconfig: 9.0.0(typescript@5.5.4) @@ -37138,7 +37458,7 @@ snapshots: webpack: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4) webpack-merge: 5.10.0 optionalDependencies: - webpack-dev-server: 5.0.4(webpack-cli@5.1.4)(webpack@5.88.0) + webpack-dev-server: 5.0.4(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) webpack-dev-middleware@6.1.3(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)): dependencies: @@ -37172,6 +37492,47 @@ snapshots: optionalDependencies: webpack: 5.94.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.23.0)(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0)) + webpack-dev-server@5.0.4(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)): + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.21 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.7 + '@types/sockjs': 0.3.36 + '@types/ws': 8.5.12 + ansi-html-community: 0.0.8 + bonjour-service: 1.2.1 + chokidar: 3.6.0 + colorette: 2.0.20 + compression: 1.7.4 + connect-history-api-fallback: 2.0.0 + default-gateway: 6.0.3 + express: 4.21.0 + graceful-fs: 4.2.11 + html-entities: 2.5.2 + http-proxy-middleware: 2.0.6(@types/express@4.17.21) + ipaddr.js: 2.2.0 + launch-editor: 2.9.1 + open: 10.1.0 + p-retry: 6.2.0 + rimraf: 5.0.10 + schema-utils: 4.2.0 + selfsigned: 2.4.1 + serve-index: 1.9.1 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack-dev-middleware: 7.4.2(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) + ws: 8.18.0 + optionalDependencies: + webpack: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0) + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + webpack-dev-server@5.0.4(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))(webpack@5.94.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.23.0)(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))): dependencies: '@types/bonjour': 3.5.13 @@ -37213,47 +37574,6 @@ snapshots: - supports-color - utf-8-validate - webpack-dev-server@5.0.4(webpack-cli@5.1.4)(webpack@5.88.0): - dependencies: - '@types/bonjour': 3.5.13 - '@types/connect-history-api-fallback': 1.5.4 - '@types/express': 4.17.21 - '@types/serve-index': 1.9.4 - '@types/serve-static': 1.15.7 - '@types/sockjs': 0.3.36 - '@types/ws': 8.5.12 - ansi-html-community: 0.0.8 - bonjour-service: 1.2.1 - chokidar: 3.6.0 - colorette: 2.0.20 - compression: 1.7.4 - connect-history-api-fallback: 2.0.0 - default-gateway: 6.0.3 - express: 4.21.0 - graceful-fs: 4.2.11 - html-entities: 2.5.2 - http-proxy-middleware: 2.0.6(@types/express@4.17.21) - ipaddr.js: 2.2.0 - launch-editor: 2.9.1 - open: 10.1.0 - p-retry: 6.2.0 - rimraf: 5.0.10 - schema-utils: 4.2.0 - selfsigned: 2.4.1 - serve-index: 1.9.1 - sockjs: 0.3.24 - spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)) - ws: 8.18.0 - optionalDependencies: - webpack: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0) - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - webpack-hot-middleware@2.26.1: dependencies: ansi-html-community: 0.0.8