diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index 2b2a2f1b2807c..da211f57f8b20 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -9941,6 +9941,14 @@ "children": [], "isExternal": false, "disableCollapsible": false + }, + { + "id": "convert-webpack", + "path": "/nx-api/rspack/generators/convert-webpack", + "name": "convert-webpack", + "children": [], + "isExternal": false, + "disableCollapsible": false } ], "isExternal": false, diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index e7900c34466f4..ba29914be77c7 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -3000,6 +3000,15 @@ "originalFilePath": "/packages/rspack/src/generators/application/schema.json", "path": "/nx-api/rspack/generators/application", "type": "generator" + }, + "/nx-api/rspack/generators/convert-webpack": { + "description": "Convert a webpack application to use rspack.", + "file": "generated/packages/rspack/generators/convert-webpack.json", + "hidden": false, + "name": "convert-webpack", + "originalFilePath": "/packages/rspack/src/generators/convert-webpack/schema.json", + "path": "/nx-api/rspack/generators/convert-webpack", + "type": "generator" } }, "path": "/nx-api/rspack" diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index 55af0240691bd..46067fed13929 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -2970,6 +2970,15 @@ "originalFilePath": "/packages/rspack/src/generators/application/schema.json", "path": "rspack/generators/application", "type": "generator" + }, + { + "description": "Convert a webpack application to use rspack.", + "file": "generated/packages/rspack/generators/convert-webpack.json", + "hidden": false, + "name": "convert-webpack", + "originalFilePath": "/packages/rspack/src/generators/convert-webpack/schema.json", + "path": "rspack/generators/convert-webpack", + "type": "generator" } ], "githubRoot": "https://github.com/nrwl/nx/blob/master", diff --git a/docs/generated/packages/rspack/generators/convert-webpack.json b/docs/generated/packages/rspack/generators/convert-webpack.json new file mode 100644 index 0000000000000..7e2035b378f52 --- /dev/null +++ b/docs/generated/packages/rspack/generators/convert-webpack.json @@ -0,0 +1,34 @@ +{ + "name": "convert-webpack", + "factory": "./src/generators/convert-webpack/convert-webpack", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "Rspack", + "title": "Nx Webpack to Rspack Generator", + "description": "Convert a Webpack project to Rspack.", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { "$source": "argv", "index": 0 }, + "x-dropdown": "project", + "x-prompt": "What is the name of the project to convert to rspack?", + "x-priority": "important" + }, + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false, + "x-priority": "internal" + } + }, + "presets": [] + }, + "description": "Convert a webpack application to use rspack.", + "implementation": "/packages/rspack/src/generators/convert-webpack/convert-webpack.ts", + "aliases": [], + "hidden": false, + "path": "/packages/rspack/src/generators/convert-webpack/schema.json", + "type": "generator" +} diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index ecf7bd03132e3..40e07c3406306 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -697,6 +697,7 @@ - [init](/nx-api/rspack/generators/init) - [preset](/nx-api/rspack/generators/preset) - [application](/nx-api/rspack/generators/application) + - [convert-webpack](/nx-api/rspack/generators/convert-webpack) - [storybook](/nx-api/storybook) - [documents](/nx-api/storybook/documents) - [Overview](/nx-api/storybook/documents/overview) diff --git a/e2e/react/src/react-module-federation.rspack.test.ts b/e2e/react/src/react-module-federation.rspack.test.ts index d8b3c478867ad..3d301304994d5 100644 --- a/e2e/react/src/react-module-federation.rspack.test.ts +++ b/e2e/react/src/react-module-federation.rspack.test.ts @@ -192,6 +192,57 @@ describe('React Rspack Module Federation', () => { } }, 500_000); + it('should generate host and remote apps in webpack, convert to rspack and use playwright for e2es', async () => { + const shell = uniq('shell'); + const remote1 = uniq('remote1'); + + runCLI( + `generate @nx/react:host ${shell} --remotes=${remote1} --bundler=webpack --e2eTestRunner=playwright --style=css --no-interactive --skipFormat` + ); + + runCLI( + `generate @nx/rspack:convert-webpack ${shell} --skipFormat --no-interactive` + ); + runCLI( + `generate @nx/rspack:convert-webpack ${remote1} --skipFormat --no-interactive` + ); + + updateFile( + `apps/${shell}-e2e/src/example.spec.ts`, + stripIndents` + import { test, expect } from '@playwright/test'; + test('should display welcome message', async ({page}) => { + await page.goto("/"); + expect(await page.locator('h1').innerText()).toContain('Welcome'); + }); + + test('should load remote 1', async ({page}) => { + await page.goto("/${remote1}"); + expect(await page.locator('h1').innerText()).toContain('${remote1}'); + }); + ` + ); + + if (runE2ETests()) { + const e2eResultsSwc = await runCommandUntil( + `e2e ${shell}-e2e`, + (output) => output.includes('Successfully ran target e2e for project') + ); + + await killProcessAndPorts(e2eResultsSwc.pid, readPort(shell)); + + const e2eResultsTsNode = await runCommandUntil( + `e2e ${shell}-e2e`, + (output) => + output.includes('Successfully ran target e2e for project'), + { + env: { NX_PREFER_TS_NODE: 'true' }, + } + ); + await killProcessAndPorts(e2eResultsTsNode.pid, readPort(shell)); + } + }, 500_000); + it('should have interop between webpack host and rspack remote', async () => { const shell = uniq('shell'); const remote1 = uniq('remote1'); diff --git a/packages/rspack/generators.json b/packages/rspack/generators.json index 1d2e2968c4a84..d89b9a265e788 100644 --- a/packages/rspack/generators.json +++ b/packages/rspack/generators.json @@ -26,6 +26,11 @@ "aliases": ["app"], "x-type": "application", "description": "React application generator." + }, + "convert-webpack": { + "factory": "./src/generators/convert-webpack/convert-webpack", + "schema": "./src/generators/convert-webpack/schema.json", + "description": "Convert a webpack application to use rspack." } } } diff --git a/packages/rspack/src/generators/convert-webpack/convert-webpack.spec.ts b/packages/rspack/src/generators/convert-webpack/convert-webpack.spec.ts new file mode 100644 index 0000000000000..186fc788e5962 --- /dev/null +++ b/packages/rspack/src/generators/convert-webpack/convert-webpack.spec.ts @@ -0,0 +1,451 @@ +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { readProjectConfiguration } from '@nx/devkit'; +// nx-ignore-next-line +import { applicationGenerator, hostGenerator } from '@nx/react'; +import convertWebpack from './convert-webpack'; + +describe('Convert webpack', () => { + it('should convert basic webpack project to rspack', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + await applicationGenerator(tree, { + directory: 'demo', + bundler: 'webpack', + e2eTestRunner: 'playwright', + linter: 'none', + style: 'css', + addPlugin: false, + }); + + // ACT + await convertWebpack(tree, { project: 'demo' }); + + // ASSERT + const project = readProjectConfiguration(tree, 'demo'); + + expect(tree.exists('demo/rspack.config.js')).toBeTruthy(); + expect(tree.read('demo/rspack.config.js', 'utf-8')).toMatchInlineSnapshot(` + "const { withReact } = require('@nx/rspack'); + const { withNx } = require('@nx/rspack'); + const { composePlugins } = require('@nx/rspack'); + + // Nx plugins for webpack. + module.exports = composePlugins( + withNx(), + withReact({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + (config) => { + // Update the webpack config as needed here. + // e.g. \`config.plugins.push(new MyPlugin())\` + return config; + } + ); + " + `); + expect(project.targets.build).toMatchInlineSnapshot(` + { + "configurations": { + "development": { + "extractLicenses": false, + "optimization": false, + "sourceMap": true, + "vendorChunk": true, + }, + "production": { + "extractLicenses": true, + "fileReplacements": [ + { + "replace": "demo/src/environments/environment.ts", + "with": "demo/src/environments/environment.prod.ts", + }, + ], + "namedChunks": false, + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "vendorChunk": false, + }, + }, + "defaultConfiguration": "production", + "executor": "@nx/rspack:rspack", + "options": { + "assets": [ + "demo/src/favicon.ico", + "demo/src/assets", + ], + "baseHref": "/", + "compiler": "babel", + "index": "demo/src/index.html", + "main": "demo/src/main.tsx", + "outputPath": "dist/demo", + "rspackConfig": "demo/rspack.config.js", + "scripts": [], + "styles": [ + "demo/src/styles.css", + ], + "target": "web", + "tsConfig": "demo/tsconfig.app.json", + }, + "outputs": [ + "{options.outputPath}", + ], + } + `); + expect(project.targets.serve).toMatchInlineSnapshot(` + { + "configurations": { + "development": { + "buildTarget": "demo:build:development", + }, + "production": { + "buildTarget": "demo:build:production", + "hmr": false, + }, + }, + "defaultConfiguration": "development", + "executor": "@nx/rspack:dev-server", + "options": { + "buildTarget": "demo:build", + "hmr": true, + }, + } + `); + }); + + it('should convert react module federation webpack projects to rspack', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + await hostGenerator(tree, { + directory: 'demo', + bundler: 'webpack', + e2eTestRunner: 'playwright', + remotes: ['remote1', 'remote2'], + linter: 'none', + style: 'css', + addPlugin: false, + unitTestRunner: 'none', + typescriptConfiguration: true, + }); + + // ACT + await convertWebpack(tree, { project: 'demo' }); + await convertWebpack(tree, { project: 'remote1' }); + await convertWebpack(tree, { project: 'remote2' }); + + // ASSERT + const project = readProjectConfiguration(tree, 'demo'); + + expect(tree.exists('demo/rspack.config.ts')).toBeTruthy(); + expect(tree.read('demo/rspack.config.ts', 'utf-8')).toMatchInlineSnapshot(` + "import { withModuleFederation } from '@nx/rspack/module-federation'; + import { ModuleFederationConfig } from '@nx/rspack/module-federation'; + import { withReact } from '@nx/rspack'; + import { withNx } from '@nx/rspack'; + import { composePlugins } from '@nx/rspack'; + + import baseConfig from './module-federation.config'; + + const config: ModuleFederationConfig = { + ...baseConfig, + }; + + // Nx plugins for webpack to build config object from Nx options and context. + /** + * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation + * The DTS Plugin can be enabled by setting dts: true + * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html + */ + export default composePlugins( + withNx(), + withReact(), + withModuleFederation(config, { dts: false }) + ); + " + `); + expect(project.targets.build).toMatchInlineSnapshot(` + { + "configurations": { + "development": { + "extractLicenses": false, + "optimization": false, + "sourceMap": true, + "vendorChunk": true, + }, + "production": { + "extractLicenses": true, + "fileReplacements": [ + { + "replace": "demo/src/environments/environment.ts", + "with": "demo/src/environments/environment.prod.ts", + }, + ], + "namedChunks": false, + "optimization": true, + "outputHashing": "all", + "rspackConfig": "demo/rspack.config.prod.ts", + "sourceMap": false, + "vendorChunk": false, + }, + }, + "defaultConfiguration": "production", + "executor": "@nx/rspack:rspack", + "options": { + "assets": [ + "demo/src/favicon.ico", + "demo/src/assets", + ], + "baseHref": "/", + "compiler": "babel", + "index": "demo/src/index.html", + "main": "demo/src/main.ts", + "outputPath": "dist/demo", + "rspackConfig": "demo/rspack.config.ts", + "scripts": [], + "styles": [ + "demo/src/styles.css", + ], + "target": "web", + "tsConfig": "demo/tsconfig.app.json", + }, + "outputs": [ + "{options.outputPath}", + ], + } + `); + expect(project.targets.serve).toMatchInlineSnapshot(` + { + "configurations": { + "development": { + "buildTarget": "demo:build:development", + }, + "production": { + "buildTarget": "demo:build:production", + "hmr": false, + }, + }, + "defaultConfiguration": "development", + "executor": "@nx/rspack:module-federation-dev-server", + "options": { + "buildTarget": "demo:build", + "hmr": true, + "port": 4200, + }, + } + `); + + const remote1 = readProjectConfiguration(tree, 'remote1'); + + expect(tree.exists('remote1/rspack.config.ts')).toBeTruthy(); + expect(tree.read('remote1/rspack.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import { withModuleFederation } from '@nx/rspack/module-federation'; + import { withReact } from '@nx/rspack'; + import { withNx } from '@nx/rspack'; + import { composePlugins } from '@nx/rspack'; + + import baseConfig from './module-federation.config'; + + const config = { + ...baseConfig, + }; + + // Nx plugins for webpack to build config object from Nx options and context. + /** + * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation + * The DTS Plugin can be enabled by setting dts: true + * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html + */ + export default composePlugins( + withNx(), + withReact(), + withModuleFederation(config, { dts: false }) + ); + " + `); + expect(tree.exists('remote1/rspack.config.prod.ts')).toBeTruthy(); + expect(tree.read('remote1/rspack.config.prod.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "export default require('./rspack.config'); + " + `); + expect(project.targets.build).toMatchInlineSnapshot(` + { + "configurations": { + "development": { + "extractLicenses": false, + "optimization": false, + "sourceMap": true, + "vendorChunk": true, + }, + "production": { + "extractLicenses": true, + "fileReplacements": [ + { + "replace": "demo/src/environments/environment.ts", + "with": "demo/src/environments/environment.prod.ts", + }, + ], + "namedChunks": false, + "optimization": true, + "outputHashing": "all", + "rspackConfig": "demo/rspack.config.prod.ts", + "sourceMap": false, + "vendorChunk": false, + }, + }, + "defaultConfiguration": "production", + "executor": "@nx/rspack:rspack", + "options": { + "assets": [ + "demo/src/favicon.ico", + "demo/src/assets", + ], + "baseHref": "/", + "compiler": "babel", + "index": "demo/src/index.html", + "main": "demo/src/main.ts", + "outputPath": "dist/demo", + "rspackConfig": "demo/rspack.config.ts", + "scripts": [], + "styles": [ + "demo/src/styles.css", + ], + "target": "web", + "tsConfig": "demo/tsconfig.app.json", + }, + "outputs": [ + "{options.outputPath}", + ], + } + `); + expect(project.targets.serve).toMatchInlineSnapshot(` + { + "configurations": { + "development": { + "buildTarget": "demo:build:development", + }, + "production": { + "buildTarget": "demo:build:production", + "hmr": false, + }, + }, + "defaultConfiguration": "development", + "executor": "@nx/rspack:module-federation-dev-server", + "options": { + "buildTarget": "demo:build", + "hmr": true, + "port": 4200, + }, + } + `); + + const remote2 = readProjectConfiguration(tree, 'remote2'); + + expect(tree.exists('remote2/rspack.config.ts')).toBeTruthy(); + expect(tree.read('remote2/rspack.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import { withModuleFederation } from '@nx/rspack/module-federation'; + import { withReact } from '@nx/rspack'; + import { withNx } from '@nx/rspack'; + import { composePlugins } from '@nx/rspack'; + + import baseConfig from './module-federation.config'; + + const config = { + ...baseConfig, + }; + + // Nx plugins for webpack to build config object from Nx options and context. + /** + * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation + * The DTS Plugin can be enabled by setting dts: true + * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html + */ + export default composePlugins( + withNx(), + withReact(), + withModuleFederation(config, { dts: false }) + ); + " + `); + expect(tree.exists('remote2/rspack.config.prod.ts')).toBeTruthy(); + expect(tree.read('remote2/rspack.config.prod.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "export default require('./rspack.config'); + " + `); + expect(project.targets.build).toMatchInlineSnapshot(` + { + "configurations": { + "development": { + "extractLicenses": false, + "optimization": false, + "sourceMap": true, + "vendorChunk": true, + }, + "production": { + "extractLicenses": true, + "fileReplacements": [ + { + "replace": "demo/src/environments/environment.ts", + "with": "demo/src/environments/environment.prod.ts", + }, + ], + "namedChunks": false, + "optimization": true, + "outputHashing": "all", + "rspackConfig": "demo/rspack.config.prod.ts", + "sourceMap": false, + "vendorChunk": false, + }, + }, + "defaultConfiguration": "production", + "executor": "@nx/rspack:rspack", + "options": { + "assets": [ + "demo/src/favicon.ico", + "demo/src/assets", + ], + "baseHref": "/", + "compiler": "babel", + "index": "demo/src/index.html", + "main": "demo/src/main.ts", + "outputPath": "dist/demo", + "rspackConfig": "demo/rspack.config.ts", + "scripts": [], + "styles": [ + "demo/src/styles.css", + ], + "target": "web", + "tsConfig": "demo/tsconfig.app.json", + }, + "outputs": [ + "{options.outputPath}", + ], + } + `); + expect(project.targets.serve).toMatchInlineSnapshot(` + { + "configurations": { + "development": { + "buildTarget": "demo:build:development", + }, + "production": { + "buildTarget": "demo:build:production", + "hmr": false, + }, + }, + "defaultConfiguration": "development", + "executor": "@nx/rspack:module-federation-dev-server", + "options": { + "buildTarget": "demo:build", + "hmr": true, + "port": 4200, + }, + } + `); + }); +}); diff --git a/packages/rspack/src/generators/convert-webpack/convert-webpack.ts b/packages/rspack/src/generators/convert-webpack/convert-webpack.ts new file mode 100644 index 0000000000000..2342f39cf5579 --- /dev/null +++ b/packages/rspack/src/generators/convert-webpack/convert-webpack.ts @@ -0,0 +1,123 @@ +import { + addDependenciesToPackageJson, + formatFiles, + getProjects, + type Tree, + updateProjectConfiguration, +} from '@nx/devkit'; +import { Schema } from './schema'; +import { + rspackCoreVersion, + rspackDevServerVersion, +} from '../../utils/versions'; +import { transformEsmConfigFile } from './lib/transform-esm'; +import { transformCjsConfigFile } from './lib/transform-cjs'; + +export default async function (tree: Tree, options: Schema) { + const projects = getProjects(tree); + if (!projects.has(options.project)) { + throw new Error( + `Could not find project '${options.project}'. Ensure you have specified the project you'd like to convert correctly.` + ); + } + const project = projects.get(options.project); + + const webpackConfigsToConvert: [string, string][] = []; + + for (const [targetName, target] of Object.entries(project.targets)) { + if (target.executor === '@nx/webpack:webpack') { + target.executor = '@nx/rspack:rspack'; + if (!target.options.target) { + target.options.target = 'web'; + } + const convertWebpackConfigOption = (options: Record) => { + if (!options.webpackConfig) { + return; + } + const rspackConfigPath = options.webpackConfig.replace( + /webpack(?!.*webpack)/, + 'rspack' + ); + webpackConfigsToConvert.push([options.webpackConfig, rspackConfigPath]); + + options.rspackConfig = rspackConfigPath; + delete options.webpackConfig; + }; + + if (target.options.webpackConfig) { + convertWebpackConfigOption(target.options); + } + + if (target.configurations) { + for (const [configurationName, configuration] of Object.entries( + target.configurations + )) { + convertWebpackConfigOption(configuration); + } + } + } else if (target.executor === '@nx/webpack:dev-server') { + target.executor = '@nx/rspack:dev-server'; + } else if (target.executor === '@nx/webpack:ssr-dev-server') { + target.executor = '@nx/rspack:dev-server'; + } else if (target.executor === '@nx/react:module-federation-dev-server') { + target.executor = '@nx/rspack:module-federation-dev-server'; + } else if ( + target.executor === '@nx/react:module-federation-ssr-dev-server' + ) { + target.executor = '@nx/rspack:module-federation-ssr-dev-server'; + } else if ( + target.executor === '@nx/react:module-federation-static-server' + ) { + target.executor = '@nx/rspack:module-federation-static-server'; + } + } + + for (const [webpackConfigPath, rspackConfigPath] of webpackConfigsToConvert) { + tree.rename(webpackConfigPath, rspackConfigPath); + transformConfigFile(tree, rspackConfigPath); + } + + updateProjectConfiguration(tree, options.project, project); + const installTask = addDependenciesToPackageJson( + tree, + {}, + { + '@rspack/core': rspackCoreVersion, + '@rspack/dev-server': rspackDevServerVersion, + } + ); + + if (!options.skipFormat) { + await formatFiles(tree); + } + + return installTask; +} + +function transformConfigFile(tree: Tree, configPath: string) { + transformEsmConfigFile(tree, configPath); + transformCjsConfigFile(tree, configPath); + cleanupEmptyImports(tree, configPath); + replaceOfRequireOfLocalWebpackConfig(tree, configPath); +} + +function replaceOfRequireOfLocalWebpackConfig(tree: Tree, configPath: string) { + const requireOfLocalWebpackConfig = + /(?<=require\s*\(\s*['"][^'"]*)(webpack)(?!.*webpack)(?=[^'"]*['"]\s*\))/g; + const configContents = tree.read(configPath, 'utf-8'); + const newContents = configContents.replace( + requireOfLocalWebpackConfig, + 'rspack' + ); + tree.write(configPath, newContents); +} + +function cleanupEmptyImports(tree: Tree, configPath: string) { + const emptyImportRegex = /import\s*\{\s*\}\s*from\s*['"][^'"]+['"];/g; + const emptyConstRequires = + /(const|let)\s*\{\s*\}\s*=\s*require\s*\(\s*['"][^'"]+['"]\s*\);/g; + const configContents = tree.read(configPath, 'utf-8'); + let newContents = configContents.replace(emptyImportRegex, ''); + newContents = newContents.replace(emptyConstRequires, ''); + tree.write(configPath, newContents); +} diff --git a/packages/rspack/src/generators/convert-webpack/lib/transform-cjs.ts b/packages/rspack/src/generators/convert-webpack/lib/transform-cjs.ts new file mode 100644 index 0000000000000..6e6657ebb4315 --- /dev/null +++ b/packages/rspack/src/generators/convert-webpack/lib/transform-cjs.ts @@ -0,0 +1,245 @@ +import type { Tree } from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; + +export function transformCjsConfigFile(tree: Tree, configPath: string) { + ['@nx', '@nrwl'].forEach((scope: '@nx' | '@nrwl') => { + transformComposePlugins(tree, configPath, scope); + transformWithNx(tree, configPath, scope); + transformWithWeb(tree, configPath, scope); + transformWithReact(tree, configPath, scope); + transformModuleFederationConfig(tree, configPath, scope); + transformWithModuleFederation(tree, configPath, scope); + transformWithModuleFederationSSR(tree, configPath, scope); + }); +} + +function transformComposePlugins( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_COMPOSE_PLUGINS_FROM_NX_WEBPACK = `VariableDeclaration:has(Identifier[name=composePlugins]) > CallExpression:has(Identifier[name=require]) StringLiteral[value=${scope}/webpack]`; + const nodes = tsquery(ast, HAS_COMPOSE_PLUGINS_FROM_NX_WEBPACK); + if (nodes.length === 0) { + return; + } + + const COMPOSE_PLUGINS_IMPORT = + 'VariableDeclaration:has(Identifier[name=composePlugins]) Identifier[name=composePlugins]'; + const composePluginsNodes = tsquery(ast, COMPOSE_PLUGINS_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = composePluginsNodes[0].getStart(); + let endIndex = composePluginsNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `const { composePlugins } = require('@nx/rspack'); + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} + +function transformWithNx( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_WITH_NX_FROM_NX_WEBPACK = `VariableDeclaration:has(Identifier[name=withNx]) > CallExpression:has(Identifier[name=require]) StringLiteral[value=${scope}/webpack]`; + const nodes = tsquery(ast, HAS_WITH_NX_FROM_NX_WEBPACK); + if (nodes.length === 0) { + return; + } + + const WITH_NX_IMPORT = + 'VariableDeclaration:has(Identifier[name=withNx]) Identifier[name=withNx]'; + const withNxNodes = tsquery(ast, WITH_NX_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = withNxNodes[0].getStart(); + let endIndex = withNxNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `const { withNx } = require('@nx/rspack'); + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} + +function transformWithWeb( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_WITH_WEB_FROM_NX_WEBPACK = `VariableDeclaration:has(Identifier[name=withWeb]) > CallExpression:has(Identifier[name=require]) StringLiteral[value=${scope}/webpack]`; + const nodes = tsquery(ast, HAS_WITH_WEB_FROM_NX_WEBPACK); + if (nodes.length === 0) { + return; + } + + const WITH_WEB_IMPORT = + 'VariableDeclaration:has(Identifier[name=withWeb]) Identifier[name=withWeb]'; + const withWebNodes = tsquery(ast, WITH_WEB_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = withWebNodes[0].getStart(); + let endIndex = withWebNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `const { withWeb } = require('@nx/rspack'); + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} + +function transformWithReact( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_WITH_REACT_FROM_NX_REACT = `VariableDeclaration:has(Identifier[name=withReact]) > CallExpression:has(Identifier[name=require]) StringLiteral[value=${scope}/react]`; + const nodes = tsquery(ast, HAS_WITH_REACT_FROM_NX_REACT); + if (nodes.length === 0) { + return; + } + + const WITH_REACT_IMPORT = + 'VariableDeclaration:has(Identifier[name=withReact]) Identifier[name=withReact]'; + const withReactNodes = tsquery(ast, WITH_REACT_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = withReactNodes[0].getStart(); + let endIndex = withReactNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `const { withReact } = require('@nx/rspack'); + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} + +function transformModuleFederationConfig( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_WITH_MODULE_FEDERATION_FROM_NX_REACT = `VariableDeclaration:has(Identifier[name=ModuleFederationConfig]) > CallExpression:has(Identifier[name=require]) StringLiteral[value=${scope}/webpack]`; + const nodes = tsquery(ast, HAS_WITH_MODULE_FEDERATION_FROM_NX_REACT); + if (nodes.length === 0) { + return; + } + + const WITH_MODULE_FEDERATION_IMPORT = + 'VariableDeclaration:has(Identifier[name=ModuleFederationConfig]) Identifier[name=ModuleFederationConfig]'; + const withModuleFederationNodes = tsquery(ast, WITH_MODULE_FEDERATION_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = withModuleFederationNodes[0].getStart(); + let endIndex = withModuleFederationNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `const { ModuleFederationConfig } = require('@nx/rspack/module-federation'); + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} + +function transformWithModuleFederation( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_WITH_MODULE_FEDERATION_FROM_NX_REACT = `VariableDeclaration:has(Identifier[name=withModuleFederation]) > CallExpression:has(Identifier[name=require]) StringLiteral[value=${scope}/react/module-federation]`; + const nodes = tsquery(ast, HAS_WITH_MODULE_FEDERATION_FROM_NX_REACT); + if (nodes.length === 0) { + return; + } + + const WITH_MODULE_FEDERATION_IMPORT = + 'VariableDeclaration:has(Identifier[name=withModuleFederation]) Identifier[name=withModuleFederation]'; + const withModuleFederationNodes = tsquery(ast, WITH_MODULE_FEDERATION_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = withModuleFederationNodes[0].getStart(); + let endIndex = withModuleFederationNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `const { withModuleFederation } = require('@nx/rspack/module-federation'); + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} + +function transformWithModuleFederationSSR( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_WITH_MODULE_FEDERATION_FROM_NX_REACT = `VariableDeclaration:has(Identifier[name=withModuleFederationForSSR]) > CallExpression:has(Identifier[name=require]) StringLiteral[value=${scope}/react/module-federation]`; + const nodes = tsquery(ast, HAS_WITH_MODULE_FEDERATION_FROM_NX_REACT); + if (nodes.length === 0) { + return; + } + + const WITH_MODULE_FEDERATION_IMPORT = + 'VariableDeclaration:has(Identifier[name=withModuleFederationForSSR]) Identifier[name=withModuleFederationForSSR]'; + const withModuleFederationNodes = tsquery(ast, WITH_MODULE_FEDERATION_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = withModuleFederationNodes[0].getStart(); + let endIndex = withModuleFederationNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `const { withModuleFederationForSSR } = require('@nx/rspack/module-federation'); + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} diff --git a/packages/rspack/src/generators/convert-webpack/lib/transform-esm.ts b/packages/rspack/src/generators/convert-webpack/lib/transform-esm.ts new file mode 100644 index 0000000000000..1036f90c3f723 --- /dev/null +++ b/packages/rspack/src/generators/convert-webpack/lib/transform-esm.ts @@ -0,0 +1,245 @@ +import type { Tree } from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; + +export function transformEsmConfigFile(tree: Tree, configPath: string) { + ['@nx', '@nrwl'].forEach((scope: '@nx' | '@nrwl') => { + transformComposePlugins(tree, configPath, scope); + transformWithNx(tree, configPath, scope); + transformWithWeb(tree, configPath, scope); + transformWithReact(tree, configPath, scope); + transformModuleFederationConfig(tree, configPath, scope); + transformWithModuleFederation(tree, configPath, scope); + transformWithModuleFederationSSR(tree, configPath, scope); + }); +} + +function transformComposePlugins( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_COMPOSE_PLUGINS_FROM_NX_WEBPACK = `ImportDeclaration:has(Identifier[name=composePlugins]) > StringLiteral[value=${scope}/webpack]`; + const nodes = tsquery(ast, HAS_COMPOSE_PLUGINS_FROM_NX_WEBPACK); + if (nodes.length === 0) { + return; + } + + const COMPOSE_PLUGINS_IMPORT = + 'ImportDeclaration:has(Identifier[name=composePlugins]) Identifier[name=composePlugins]'; + const composePluginsNodes = tsquery(ast, COMPOSE_PLUGINS_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = composePluginsNodes[0].getStart(); + let endIndex = composePluginsNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `import { composePlugins } from '@nx/rspack'; + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} + +function transformWithNx( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_WITH_NX_FROM_NX_WEBPACK = `ImportDeclaration:has(Identifier[name=withNx]) > StringLiteral[value=${scope}/webpack]`; + const nodes = tsquery(ast, HAS_WITH_NX_FROM_NX_WEBPACK); + if (nodes.length === 0) { + return; + } + + const WITH_NX_IMPORT = + 'ImportDeclaration:has(Identifier[name=withNx]) Identifier[name=withNx]'; + const withNxNodes = tsquery(ast, WITH_NX_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = withNxNodes[0].getStart(); + let endIndex = withNxNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `import { withNx } from '@nx/rspack'; + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} + +function transformWithWeb( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_WITH_WEB_FROM_NX_WEBPACK = `ImportDeclaration:has(Identifier[name=withWeb]) > StringLiteral[value=${scope}/webpack]`; + const nodes = tsquery(ast, HAS_WITH_WEB_FROM_NX_WEBPACK); + if (nodes.length === 0) { + return; + } + + const WITH_WEB_IMPORT = + 'ImportDeclaration:has(Identifier[name=withWeb]) Identifier[name=withWeb]'; + const withWebNodes = tsquery(ast, WITH_WEB_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = withWebNodes[0].getStart(); + let endIndex = withWebNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `import { withWeb } from '@nx/rspack'; + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} + +function transformWithReact( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_WITH_REACT_FROM_NX_REACT = `ImportDeclaration:has(Identifier[name=withReact]) > StringLiteral[value=${scope}/react]`; + const nodes = tsquery(ast, HAS_WITH_REACT_FROM_NX_REACT); + if (nodes.length === 0) { + return; + } + + const WITH_REACT_IMPORT = + 'ImportDeclaration:has(Identifier[name=withReact]) Identifier[name=withReact]'; + const withReactNodes = tsquery(ast, WITH_REACT_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = withReactNodes[0].getStart(); + let endIndex = withReactNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `import { withReact } from '@nx/rspack'; + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} + +function transformWithModuleFederation( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_WITH_MODULE_FEDERATION_FROM_NX_REACT = `ImportDeclaration:has(Identifier[name=withModuleFederation]) > StringLiteral[value=${scope}/react/module-federation]`; + const nodes = tsquery(ast, HAS_WITH_MODULE_FEDERATION_FROM_NX_REACT); + if (nodes.length === 0) { + return; + } + + const WITH_MODULE_FEDERATION_IMPORT = + 'ImportDeclaration:has(Identifier[name=withModuleFederation]) Identifier[name=withModuleFederation]'; + const withModuleFederationNodes = tsquery(ast, WITH_MODULE_FEDERATION_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = withModuleFederationNodes[0].getStart(); + let endIndex = withModuleFederationNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `import { withModuleFederation } from '@nx/rspack/module-federation'; + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} + +function transformModuleFederationConfig( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_WITH_MODULE_FEDERATION_FROM_NX_REACT = `ImportDeclaration:has(Identifier[name=ModuleFederationConfig]) > StringLiteral[value=${scope}/webpack]`; + const nodes = tsquery(ast, HAS_WITH_MODULE_FEDERATION_FROM_NX_REACT); + if (nodes.length === 0) { + return; + } + + const WITH_MODULE_FEDERATION_IMPORT = + 'ImportDeclaration:has(Identifier[name=ModuleFederationConfig]) Identifier[name=ModuleFederationConfig]'; + const withModuleFederationNodes = tsquery(ast, WITH_MODULE_FEDERATION_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = withModuleFederationNodes[0].getStart(); + let endIndex = withModuleFederationNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `import { ModuleFederationConfig } from '@nx/rspack/module-federation'; + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} + +function transformWithModuleFederationSSR( + tree: Tree, + configPath: string, + scope: '@nx' | '@nrwl' +) { + const configContents = tree.read(configPath, 'utf-8'); + const ast = tsquery.ast(configContents); + + const HAS_WITH_MODULE_FEDERATION_FROM_NX_REACT = `ImportDeclaration:has(Identifier[name=withModuleFederationForSSR]) > StringLiteral[value=${scope}/react/module-federation]`; + const nodes = tsquery(ast, HAS_WITH_MODULE_FEDERATION_FROM_NX_REACT); + if (nodes.length === 0) { + return; + } + + const WITH_MODULE_FEDERATION_IMPORT = + 'ImportDeclaration:has(Identifier[name=withModuleFederationForSSR]) Identifier[name=withModuleFederationForSSR]'; + const withModuleFederationNodes = tsquery(ast, WITH_MODULE_FEDERATION_IMPORT); + if (nodes.length === 0) { + return; + } + + const startIndex = withModuleFederationNodes[0].getStart(); + let endIndex = withModuleFederationNodes[0].getEnd(); + if (configContents.charAt(endIndex) === ',') { + endIndex++; + } + + const newContents = `import { withModuleFederationForSSR } from '@nx/rspack/module-federation'; + ${configContents.slice(0, startIndex)}${configContents.slice(endIndex)}`; + + tree.write(configPath, newContents); +} diff --git a/packages/rspack/src/generators/convert-webpack/schema.d.ts b/packages/rspack/src/generators/convert-webpack/schema.d.ts new file mode 100644 index 0000000000000..e3cab627207cd --- /dev/null +++ b/packages/rspack/src/generators/convert-webpack/schema.d.ts @@ -0,0 +1,4 @@ +export interface Schema { + project: string; + skipFormat?: boolean; +} diff --git a/packages/rspack/src/generators/convert-webpack/schema.json b/packages/rspack/src/generators/convert-webpack/schema.json new file mode 100644 index 0000000000000..66552b3f60890 --- /dev/null +++ b/packages/rspack/src/generators/convert-webpack/schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "Rspack", + "title": "Nx Webpack to Rspack Generator", + "description": "Convert a Webpack project to Rspack.", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-dropdown": "project", + "x-prompt": "What is the name of the project to convert to rspack?", + "x-priority": "important" + }, + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false, + "x-priority": "internal" + } + } +} diff --git a/packages/rspack/src/utils/versions.ts b/packages/rspack/src/utils/versions.ts index 3a9137a86232f..1a91e1981222a 100644 --- a/packages/rspack/src/utils/versions.ts +++ b/packages/rspack/src/utils/versions.ts @@ -1,3 +1,4 @@ +export const nxVersion = require('../../package.json').version; export const rspackCoreVersion = '1.0.5'; export const rspackDevServerVersion = '1.0.5';