feat(rspack): add convert-webpack generator (#28167)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
We do not have an automated method for people to switch to rspack from
webpack applications


## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
We should have a generator that will convert webpack application
projects to use rspack

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Colum Ferry 2024-10-23 09:54:49 +01:00 committed by GitHub
parent 12cbfc35da
commit 36556f6f23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1212 additions and 0 deletions

View File

@ -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,

View File

@ -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"

View File

@ -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",

View File

@ -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"
}

View File

@ -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)

View File

@ -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');

View File

@ -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."
}
}
}

View File

@ -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,
},
}
`);
});
});

View File

@ -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<string, any>) => {
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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -0,0 +1,4 @@
export interface Schema {
project: string;
skipFormat?: boolean;
}

View File

@ -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"
}
}
}

View File

@ -1,3 +1,4 @@
export const nxVersion = require('../../package.json').version;
export const rspackCoreVersion = '1.0.5';
export const rspackDevServerVersion = '1.0.5';