diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json
index 7616245b7fcc9..c749b699c4f27 100644
--- a/docs/generated/manifests/menus.json
+++ b/docs/generated/manifests/menus.json
@@ -9875,6 +9875,130 @@
"isExternal": false,
"disableCollapsible": false
},
+ {
+ "id": "rspack",
+ "path": "/nx-api/rspack",
+ "name": "rspack",
+ "children": [
+ {
+ "id": "documents",
+ "path": "/nx-api/rspack/documents",
+ "name": "documents",
+ "children": [
+ {
+ "name": "Overview",
+ "path": "/nx-api/rspack/documents/overview",
+ "id": "overview",
+ "isExternal": false,
+ "children": [],
+ "disableCollapsible": false
+ }
+ ],
+ "isExternal": false,
+ "disableCollapsible": false
+ },
+ {
+ "id": "executors",
+ "path": "/nx-api/rspack/executors",
+ "name": "executors",
+ "children": [
+ {
+ "id": "rspack",
+ "path": "/nx-api/rspack/executors/rspack",
+ "name": "rspack",
+ "children": [],
+ "isExternal": false,
+ "disableCollapsible": false
+ },
+ {
+ "id": "dev-server",
+ "path": "/nx-api/rspack/executors/dev-server",
+ "name": "dev-server",
+ "children": [],
+ "isExternal": false,
+ "disableCollapsible": false
+ },
+ {
+ "id": "ssr-dev-server",
+ "path": "/nx-api/rspack/executors/ssr-dev-server",
+ "name": "ssr-dev-server",
+ "children": [],
+ "isExternal": false,
+ "disableCollapsible": false
+ },
+ {
+ "id": "module-federation-dev-server",
+ "path": "/nx-api/rspack/executors/module-federation-dev-server",
+ "name": "module-federation-dev-server",
+ "children": [],
+ "isExternal": false,
+ "disableCollapsible": false
+ },
+ {
+ "id": "module-federation-ssr-dev-server",
+ "path": "/nx-api/rspack/executors/module-federation-ssr-dev-server",
+ "name": "module-federation-ssr-dev-server",
+ "children": [],
+ "isExternal": false,
+ "disableCollapsible": false
+ },
+ {
+ "id": "module-federation-static-server",
+ "path": "/nx-api/rspack/executors/module-federation-static-server",
+ "name": "module-federation-static-server",
+ "children": [],
+ "isExternal": false,
+ "disableCollapsible": false
+ }
+ ],
+ "isExternal": false,
+ "disableCollapsible": false
+ },
+ {
+ "id": "generators",
+ "path": "/nx-api/rspack/generators",
+ "name": "generators",
+ "children": [
+ {
+ "id": "configuration",
+ "path": "/nx-api/rspack/generators/configuration",
+ "name": "configuration",
+ "children": [],
+ "isExternal": false,
+ "disableCollapsible": false
+ },
+ {
+ "id": "init",
+ "path": "/nx-api/rspack/generators/init",
+ "name": "init",
+ "children": [],
+ "isExternal": false,
+ "disableCollapsible": false
+ },
+ {
+ "id": "preset",
+ "path": "/nx-api/rspack/generators/preset",
+ "name": "preset",
+ "children": [],
+ "isExternal": false,
+ "disableCollapsible": false
+ },
+ {
+ "id": "application",
+ "path": "/nx-api/rspack/generators/application",
+ "name": "application",
+ "children": [],
+ "isExternal": false,
+ "disableCollapsible": false
+ }
+ ],
+ "isExternal": false,
+ "disableCollapsible": false
+ }
+ ],
+ "isExternal": false,
+ "disableCollapsible": false
+ },
{
"id": "storybook",
"path": "/nx-api/storybook",
diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json
index 7b62c652587d2..060a871ac0764 100644
--- a/docs/generated/manifests/nx-api.json
+++ b/docs/generated/manifests/nx-api.json
@@ -2877,6 +2877,122 @@
},
"path": "/nx-api/rollup"
},
+ "rspack": {
+ "githubRoot": "https://github.com/nrwl/nx/blob/master",
+ "name": "rspack",
+ "packageName": "@nx/rspack",
+ "description": "The Nx Plugin for Rspack contains executors and generators that support building applications using Rspack.",
+ "documents": {
+ "/nx-api/rspack/documents/overview": {
+ "id": "overview",
+ "name": "Overview",
+ "description": "The Nx Plugin for Rspack contains executors and generators that support building applications using Rspack.",
+ "file": "generated/packages/rspack/documents/overview",
+ "itemList": [],
+ "isExternal": false,
+ "path": "/nx-api/rspack/documents/overview",
+ "tags": [],
+ "originalFilePath": "shared/packages/rspack/rspack-plugin"
+ }
+ },
+ "root": "/packages/rspack",
+ "source": "/packages/rspack/src",
+ "executors": {
+ "/nx-api/rspack/executors/rspack": {
+ "description": "Run Rspack via an executor for a project.",
+ "file": "generated/packages/rspack/executors/rspack.json",
+ "hidden": false,
+ "name": "rspack",
+ "originalFilePath": "/packages/rspack/src/executors/rspack/schema.json",
+ "path": "/nx-api/rspack/executors/rspack",
+ "type": "executor"
+ },
+ "/nx-api/rspack/executors/dev-server": {
+ "description": "Run @rspack/dev-server to serve a project.",
+ "file": "generated/packages/rspack/executors/dev-server.json",
+ "hidden": false,
+ "name": "dev-server",
+ "originalFilePath": "/packages/rspack/src/executors/dev-server/schema.json",
+ "path": "/nx-api/rspack/executors/dev-server",
+ "type": "executor"
+ },
+ "/nx-api/rspack/executors/ssr-dev-server": {
+ "description": "Serve a SSR application.",
+ "file": "generated/packages/rspack/executors/ssr-dev-server.json",
+ "hidden": false,
+ "name": "ssr-dev-server",
+ "originalFilePath": "/packages/rspack/src/executors/ssr-dev-server/schema.json",
+ "path": "/nx-api/rspack/executors/ssr-dev-server",
+ "type": "executor"
+ },
+ "/nx-api/rspack/executors/module-federation-dev-server": {
+ "description": "Serve a host or remote application.",
+ "file": "generated/packages/rspack/executors/module-federation-dev-server.json",
+ "hidden": false,
+ "name": "module-federation-dev-server",
+ "originalFilePath": "/packages/rspack/src/executors/module-federation-dev-server/schema.json",
+ "path": "/nx-api/rspack/executors/module-federation-dev-server",
+ "type": "executor"
+ },
+ "/nx-api/rspack/executors/module-federation-ssr-dev-server": {
+ "description": "Serve a host application along with it's known remotes.",
+ "file": "generated/packages/rspack/executors/module-federation-ssr-dev-server.json",
+ "hidden": false,
+ "name": "module-federation-ssr-dev-server",
+ "originalFilePath": "/packages/rspack/src/executors/module-federation-ssr-dev-server/schema.json",
+ "path": "/nx-api/rspack/executors/module-federation-ssr-dev-server",
+ "type": "executor"
+ },
+ "/nx-api/rspack/executors/module-federation-static-server": {
+ "description": "Serve a host and its remotes statically.",
+ "file": "generated/packages/rspack/executors/module-federation-static-server.json",
+ "hidden": false,
+ "name": "module-federation-static-server",
+ "originalFilePath": "/packages/rspack/src/executors/module-federation-static-server/schema.json",
+ "path": "/nx-api/rspack/executors/module-federation-static-server",
+ "type": "executor"
+ }
+ },
+ "generators": {
+ "/nx-api/rspack/generators/configuration": {
+ "description": "Rspack configuration generator.",
+ "file": "generated/packages/rspack/generators/configuration.json",
+ "hidden": false,
+ "name": "configuration",
+ "originalFilePath": "/packages/rspack/src/generators/configuration/schema.json",
+ "path": "/nx-api/rspack/generators/configuration",
+ "type": "generator"
+ },
+ "/nx-api/rspack/generators/init": {
+ "description": "Rspack init generator.",
+ "file": "generated/packages/rspack/generators/init.json",
+ "hidden": true,
+ "name": "init",
+ "originalFilePath": "/packages/rspack/src/generators/init/schema.json",
+ "path": "/nx-api/rspack/generators/init",
+ "type": "generator"
+ },
+ "/nx-api/rspack/generators/preset": {
+ "description": "React preset generator.",
+ "file": "generated/packages/rspack/generators/preset.json",
+ "hidden": true,
+ "name": "preset",
+ "originalFilePath": "/packages/rspack/src/generators/preset/schema.json",
+ "path": "/nx-api/rspack/generators/preset",
+ "type": "generator"
+ },
+ "/nx-api/rspack/generators/application": {
+ "description": "React application generator.",
+ "file": "generated/packages/rspack/generators/application.json",
+ "hidden": false,
+ "name": "application",
+ "originalFilePath": "/packages/rspack/src/generators/application/schema.json",
+ "path": "/nx-api/rspack/generators/application",
+ "type": "generator"
+ }
+ },
+ "path": "/nx-api/rspack"
+ },
"storybook": {
"githubRoot": "https://github.com/nrwl/nx/blob/master",
"name": "storybook",
diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json
index dbfe9fb058a61..b1c35172a318b 100644
--- a/docs/generated/packages-metadata.json
+++ b/docs/generated/packages-metadata.json
@@ -2852,6 +2852,121 @@
"root": "/packages/rollup",
"source": "/packages/rollup/src"
},
+ {
+ "description": "The Nx Plugin for Rspack contains executors and generators that support building applications using Rspack.",
+ "documents": [
+ {
+ "id": "overview",
+ "name": "Overview",
+ "description": "The Nx Plugin for Rspack contains executors and generators that support building applications using Rspack.",
+ "file": "generated/packages/rspack/documents/overview",
+ "itemList": [],
+ "isExternal": false,
+ "path": "rspack/documents/overview",
+ "tags": [],
+ "originalFilePath": "shared/packages/rspack/rspack-plugin"
+ }
+ ],
+ "executors": [
+ {
+ "description": "Run Rspack via an executor for a project.",
+ "file": "generated/packages/rspack/executors/rspack.json",
+ "hidden": false,
+ "name": "rspack",
+ "originalFilePath": "/packages/rspack/src/executors/rspack/schema.json",
+ "path": "rspack/executors/rspack",
+ "type": "executor"
+ },
+ {
+ "description": "Run @rspack/dev-server to serve a project.",
+ "file": "generated/packages/rspack/executors/dev-server.json",
+ "hidden": false,
+ "name": "dev-server",
+ "originalFilePath": "/packages/rspack/src/executors/dev-server/schema.json",
+ "path": "rspack/executors/dev-server",
+ "type": "executor"
+ },
+ {
+ "description": "Serve a SSR application.",
+ "file": "generated/packages/rspack/executors/ssr-dev-server.json",
+ "hidden": false,
+ "name": "ssr-dev-server",
+ "originalFilePath": "/packages/rspack/src/executors/ssr-dev-server/schema.json",
+ "path": "rspack/executors/ssr-dev-server",
+ "type": "executor"
+ },
+ {
+ "description": "Serve a host or remote application.",
+ "file": "generated/packages/rspack/executors/module-federation-dev-server.json",
+ "hidden": false,
+ "name": "module-federation-dev-server",
+ "originalFilePath": "/packages/rspack/src/executors/module-federation-dev-server/schema.json",
+ "path": "rspack/executors/module-federation-dev-server",
+ "type": "executor"
+ },
+ {
+ "description": "Serve a host application along with it's known remotes.",
+ "file": "generated/packages/rspack/executors/module-federation-ssr-dev-server.json",
+ "hidden": false,
+ "name": "module-federation-ssr-dev-server",
+ "originalFilePath": "/packages/rspack/src/executors/module-federation-ssr-dev-server/schema.json",
+ "path": "rspack/executors/module-federation-ssr-dev-server",
+ "type": "executor"
+ },
+ {
+ "description": "Serve a host and its remotes statically.",
+ "file": "generated/packages/rspack/executors/module-federation-static-server.json",
+ "hidden": false,
+ "name": "module-federation-static-server",
+ "originalFilePath": "/packages/rspack/src/executors/module-federation-static-server/schema.json",
+ "path": "rspack/executors/module-federation-static-server",
+ "type": "executor"
+ }
+ ],
+ "generators": [
+ {
+ "description": "Rspack configuration generator.",
+ "file": "generated/packages/rspack/generators/configuration.json",
+ "hidden": false,
+ "name": "configuration",
+ "originalFilePath": "/packages/rspack/src/generators/configuration/schema.json",
+ "path": "rspack/generators/configuration",
+ "type": "generator"
+ },
+ {
+ "description": "Rspack init generator.",
+ "file": "generated/packages/rspack/generators/init.json",
+ "hidden": true,
+ "name": "init",
+ "originalFilePath": "/packages/rspack/src/generators/init/schema.json",
+ "path": "rspack/generators/init",
+ "type": "generator"
+ },
+ {
+ "description": "React preset generator.",
+ "file": "generated/packages/rspack/generators/preset.json",
+ "hidden": true,
+ "name": "preset",
+ "originalFilePath": "/packages/rspack/src/generators/preset/schema.json",
+ "path": "rspack/generators/preset",
+ "type": "generator"
+ },
+ {
+ "description": "React application generator.",
+ "file": "generated/packages/rspack/generators/application.json",
+ "hidden": false,
+ "name": "application",
+ "originalFilePath": "/packages/rspack/src/generators/application/schema.json",
+ "path": "rspack/generators/application",
+ "type": "generator"
+ }
+ ],
+ "githubRoot": "https://github.com/nrwl/nx/blob/master",
+ "name": "rspack",
+ "packageName": "@nx/rspack",
+ "root": "/packages/rspack",
+ "source": "/packages/rspack/src"
+ },
{
"description": "The Nx Plugin for Storybook contains executors and generators for allowing your workspace to use the powerful Storybook integration testing & documenting capabilities.",
"documents": [
diff --git a/docs/generated/packages/rspack/documents/overview.md b/docs/generated/packages/rspack/documents/overview.md
new file mode 100644
index 0000000000000..95e3375773c14
--- /dev/null
+++ b/docs/generated/packages/rspack/documents/overview.md
@@ -0,0 +1,98 @@
+---
+title: Overview of the Nx Rspack Plugin
+description: The Nx Plugin for Rspack contains executors, generators, and utilities for managing Rspack projects in an Nx Workspace.
+---
+
+The Nx Plugin for Rspack contains executors, generators, and utilities for managing Rspack projects in an Nx Workspace.
+
+## Setting Up @nx/rspack
+
+### Installation
+
+{% callout type="note" title="Keep Nx Package Versions In Sync" %}
+Make sure to install the `@nx/rspack` version that matches the version of `nx` in your repository. If the version numbers get out of sync, you can encounter some difficult to debug errors. You can [fix Nx version mismatches with this recipe](/recipes/tips-n-tricks/keep-nx-versions-in-sync).
+{% /callout %}
+
+In any Nx workspace, you can install `@nx/rspack` by running the following command:
+
+{% tabs %}
+{% tab label="Nx 18+" %}
+
+```shell {% skipRescope=true %}
+nx add @nx/rspack
+```
+
+This will install the correct version of `@nx/rspack`.
+
+### How @nx/rspack Infers Tasks
+
+The `@nx/rspack` plugin will create a task for any project that has a Rspack configuration file present. Any of the following files will be recognized as a Rspack configuration file:
+
+- `rspack.config.js`
+- `rspack.config.ts`
+- `rspack.config.mjs`
+- `rspack.config.mts`
+- `rspack.config.cjs`
+- `rspack.config.cts`
+
+### View Inferred Tasks
+
+To view inferred tasks for a project, open the [project details view](/concepts/inferred-tasks) in Nx Console or run `nx show project my-project --web` in the command line.
+
+### @nx/rspack Configuration
+
+The `@nx/rspack/plugin` is configured in the `plugins` array in `nx.json`.
+
+```json {% fileName="nx.json" %}
+{
+ "plugins": [
+ {
+ "plugin": "@nx/rspack/plugin",
+ "options": {
+ "buildTargetName": "build",
+ "previewTargetName": "preview",
+ "serveTargetName": "serve",
+ "serveStaticTargetName": "serve-static"
+ }
+ }
+ ]
+}
+```
+
+The `buildTargetName`, `previewTargetName`, `serveTargetName` and `serveStaticTargetName` options control the names of the inferred Rspack tasks. The default names are `build`, `preview`, `serve` and `serve-static`.
+
+{% /tab %}
+{% tab label="Nx < 18" %}
+
+Install the `@nx/rspack` package with your package manager.
+
+```shell
+npm add -D @nx/rspack
+```
+
+{% /tab %}
+{% /tabs %}
+
+## Using @nx/rspack
+
+### Generate a new project using Rspack
+
+You can generate a [React](/nx-api/react) application or library that uses Rspack. The [`@nx/react:app`](/nx-api/react/generators/application) and [`@nx/react:lib`](/nx-api/react/generators/library) generators accept the `bundler` option, where you can pass `rspack`. This will generate a new application configured to use Rspack, and it will also install all the necessary dependencies, including the `@nx/rspack` plugin.
+
+To generate a React application using Rspack, run the following:
+
+```bash
+nx g @nx/react:app my-app --bundler=rspack
+```
+
+To generate a React library using Rspack, run the following:
+
+```bash
+nx g @nx/react:lib my-lib --bundler=rspack
+```
+
+### Modify an existing React project to use Rspack
+
+You can use the `@nx/rspack:configuration` generator to change your React to use Rspack. This generator will modify your project's configuration to use Rspack, and it will also install all the necessary dependencies, including the `@nx/rspack` plugin.
+
+You can read more about this generator on the [`@nx/rspack:configuration`](/nx-api/rspack/generators/configuration) generator page.
diff --git a/docs/generated/packages/rspack/executors/dev-server.json b/docs/generated/packages/rspack/executors/dev-server.json
new file mode 100644
index 0000000000000..fd9399c7db558
--- /dev/null
+++ b/docs/generated/packages/rspack/executors/dev-server.json
@@ -0,0 +1,55 @@
+{
+ "name": "dev-server",
+ "implementation": "/packages/rspack/src/executors/dev-server/dev-server.impl.ts",
+ "schema": {
+ "$schema": "http://json-schema.org/schema",
+ "version": 2,
+ "title": "Rspack dev-server executor",
+ "description": "Run @rspack/dev-server to serve a project.",
+ "type": "object",
+ "properties": {
+ "buildTarget": {
+ "type": "string",
+ "description": "The build target for rspack."
+ },
+ "port": {
+ "type": "number",
+ "description": "The port to for the dev-server to listen on."
+ },
+ "mode": {
+ "type": "string",
+ "description": "Mode to run the server in.",
+ "enum": ["development", "production", "none"]
+ },
+ "host": {
+ "type": "string",
+ "description": "Host to listen on.",
+ "default": "localhost"
+ },
+ "ssl": {
+ "type": "boolean",
+ "description": "Serve using `HTTPS`.",
+ "default": false
+ },
+ "sslKey": {
+ "type": "string",
+ "description": "SSL key to use for serving `HTTPS`."
+ },
+ "sslCert": {
+ "type": "string",
+ "description": "SSL certificate to use for serving `HTTPS`."
+ },
+ "publicHost": {
+ "type": "string",
+ "description": "Public URL where the application will be served."
+ }
+ },
+ "required": ["buildTarget"],
+ "presets": []
+ },
+ "description": "Run @rspack/dev-server to serve a project.",
+ "aliases": [],
+ "hidden": false,
+ "path": "/packages/rspack/src/executors/dev-server/schema.json",
+ "type": "executor"
+}
diff --git a/docs/generated/packages/rspack/executors/module-federation-dev-server.json b/docs/generated/packages/rspack/executors/module-federation-dev-server.json
new file mode 100644
index 0000000000000..571c583e136a2
--- /dev/null
+++ b/docs/generated/packages/rspack/executors/module-federation-dev-server.json
@@ -0,0 +1,100 @@
+{
+ "name": "module-federation-dev-server",
+ "implementation": "/packages/rspack/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts",
+ "schema": {
+ "version": 2,
+ "outputCapture": "direct-nodejs",
+ "title": "Rspack Module Federation Dev Server",
+ "description": "Serve a module federation application.",
+ "cli": "nx",
+ "type": "object",
+ "properties": {
+ "devRemotes": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ { "type": "string" },
+ {
+ "type": "object",
+ "properties": {
+ "remoteName": { "type": "string" },
+ "configuration": { "type": "string" }
+ },
+ "required": ["remoteName"],
+ "additionalProperties": false
+ }
+ ]
+ },
+ "description": "List of remote applications to run in development mode (i.e. using serve target).",
+ "x-priority": "important"
+ },
+ "skipRemotes": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "List of remote applications to not automatically serve, either statically or in development mode. This will not remove the remotes from the `module-federation.config` file, and therefore the application may still try to fetch these remotes.\nThis option is useful if you have other means for serving the `remote` application(s).\n**NOTE:** Remotes that are not in the workspace will be skipped automatically.",
+ "x-priority": "important"
+ },
+ "buildTarget": {
+ "type": "string",
+ "description": "Target which builds the application.",
+ "x-priority": "important"
+ },
+ "port": {
+ "type": "number",
+ "description": "Port to listen on.",
+ "default": 4200,
+ "x-priority": "important"
+ },
+ "host": {
+ "type": "string",
+ "description": "Host to listen on.",
+ "default": "localhost"
+ },
+ "ssl": {
+ "type": "boolean",
+ "description": "Serve using `HTTPS`.",
+ "default": false
+ },
+ "sslKey": {
+ "type": "string",
+ "description": "SSL key to use for serving `HTTPS`."
+ },
+ "sslCert": {
+ "type": "string",
+ "description": "SSL certificate to use for serving `HTTPS`."
+ },
+ "publicHost": {
+ "type": "string",
+ "description": "Public URL where the application will be served."
+ },
+ "static": {
+ "type": "boolean",
+ "description": "Whether to use a static file server instead of the rspack-dev-server. This should be used for remote applications that are also host applications."
+ },
+ "isInitialHost": {
+ "type": "boolean",
+ "description": "Whether the host that is running this executor is the first in the project tree to do so.",
+ "default": true,
+ "x-priority": "internal"
+ },
+ "parallel": {
+ "type": "number",
+ "description": "Max number of parallel processes for building static remotes"
+ },
+ "staticRemotesPort": {
+ "type": "number",
+ "description": "The port at which to serve the file-server for the static remotes."
+ },
+ "pathToManifestFile": {
+ "type": "string",
+ "description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root."
+ }
+ },
+ "presets": []
+ },
+ "description": "Serve a host or remote application.",
+ "aliases": [],
+ "hidden": false,
+ "path": "/packages/rspack/src/executors/module-federation-dev-server/schema.json",
+ "type": "executor"
+}
diff --git a/docs/generated/packages/rspack/executors/module-federation-ssr-dev-server.json b/docs/generated/packages/rspack/executors/module-federation-ssr-dev-server.json
new file mode 100644
index 0000000000000..f91a75342f7f6
--- /dev/null
+++ b/docs/generated/packages/rspack/executors/module-federation-ssr-dev-server.json
@@ -0,0 +1,85 @@
+{
+ "name": "module-federation-ssr-dev-server",
+ "implementation": "/packages/rspack/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts",
+ "schema": {
+ "version": 2,
+ "outputCapture": "direct-nodejs",
+ "title": "Module Federation SSR Dev Server",
+ "description": "Serve a SSR host application along with its known remotes.",
+ "cli": "nx",
+ "type": "object",
+ "properties": {
+ "browserTarget": {
+ "type": "string",
+ "description": "Target which builds the browser application.",
+ "x-priority": "important"
+ },
+ "serverTarget": {
+ "type": "string",
+ "description": "Target which builds the server application.",
+ "x-priority": "important"
+ },
+ "port": {
+ "type": "number",
+ "description": "The port to be set on `process.env.PORT` for use in the server.",
+ "default": 4200,
+ "x-priority": "important"
+ },
+ "devRemotes": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "List of remote applications to run in development mode (i.e. using serve target).",
+ "x-priority": "important"
+ },
+ "skipRemotes": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "List of remote applications to not automatically serve, either statically or in development mode.",
+ "x-priority": "important"
+ },
+ "host": {
+ "type": "string",
+ "description": "Host to listen on.",
+ "default": "localhost"
+ },
+ "staticRemotesPort": {
+ "type": "number",
+ "description": "The port at which to serve the file-server for the static remotes."
+ },
+ "pathToManifestFile": {
+ "type": "string",
+ "description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root."
+ },
+ "ssl": {
+ "type": "boolean",
+ "description": "Serve using HTTPS.",
+ "default": false
+ },
+ "sslKey": {
+ "type": "string",
+ "description": "SSL key to use for serving HTTPS."
+ },
+ "sslCert": {
+ "type": "string",
+ "description": "SSL certificate to use for serving HTTPS."
+ },
+ "publicHost": {
+ "type": "string",
+ "description": "Public URL where the application will be served."
+ },
+ "isInitialHost": {
+ "type": "boolean",
+ "description": "Whether the host that is running this executor is the first in the project tree to do so.",
+ "default": true,
+ "x-priority": "internal"
+ }
+ },
+ "required": ["browserTarget", "serverTarget"],
+ "presets": []
+ },
+ "description": "Serve a host application along with it's known remotes.",
+ "aliases": [],
+ "hidden": false,
+ "path": "/packages/rspack/src/executors/module-federation-ssr-dev-server/schema.json",
+ "type": "executor"
+}
diff --git a/docs/generated/packages/rspack/executors/module-federation-static-server.json b/docs/generated/packages/rspack/executors/module-federation-static-server.json
new file mode 100644
index 0000000000000..83d2a7072f677
--- /dev/null
+++ b/docs/generated/packages/rspack/executors/module-federation-static-server.json
@@ -0,0 +1,20 @@
+{
+ "name": "module-federation-static-server",
+ "implementation": "/packages/rspack/src/executors/module-federation-static-server/module-federation-static-server.impl.ts",
+ "schema": {
+ "version": 2,
+ "outputCapture": "direct-nodejs",
+ "title": "Module Federation Static Dev Server",
+ "description": "Serve a host application statically along with it's remotes.",
+ "cli": "nx",
+ "type": "object",
+ "properties": { "serveTarget": { "type": "string" } },
+ "required": ["serveTarget"],
+ "presets": []
+ },
+ "description": "Serve a host and its remotes statically.",
+ "aliases": [],
+ "hidden": false,
+ "path": "/packages/rspack/src/executors/module-federation-static-server/schema.json",
+ "type": "executor"
+}
diff --git a/docs/generated/packages/rspack/executors/rspack.json b/docs/generated/packages/rspack/executors/rspack.json
new file mode 100644
index 0000000000000..78308a42bc2f1
--- /dev/null
+++ b/docs/generated/packages/rspack/executors/rspack.json
@@ -0,0 +1,202 @@
+{
+ "name": "rspack",
+ "implementation": "/packages/rspack/src/executors/rspack/rspack.impl.ts",
+ "schema": {
+ "$schema": "http://json-schema.org/schema",
+ "version": 2,
+ "title": "Rspack build executor",
+ "description": "Run Rspack via an executor for a project.",
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "string",
+ "description": "The platform to target (e.g. web, node).",
+ "enum": ["web", "node"]
+ },
+ "main": { "type": "string", "description": "The main entry file." },
+ "outputPath": {
+ "type": "string",
+ "description": "The output path for the bundle."
+ },
+ "outputFileName": {
+ "type": "string",
+ "description": "The main output entry file"
+ },
+ "tsConfig": {
+ "type": "string",
+ "description": "The tsconfig file to build the project."
+ },
+ "typeCheck": {
+ "type": "boolean",
+ "description": "Skip the type checking."
+ },
+ "indexHtml": {
+ "type": "string",
+ "description": "The path to the index.html file."
+ },
+ "index": {
+ "type": "string",
+ "description": "HTML File which will be contain the application.",
+ "x-completion-type": "file",
+ "x-completion-glob": "**/*@(.html|.htm)"
+ },
+ "baseHref": {
+ "type": "string",
+ "description": "Base url for the application being built."
+ },
+ "deployUrl": {
+ "type": "string",
+ "description": "URL where the application will be deployed."
+ },
+ "rspackConfig": {
+ "type": "string",
+ "description": "The path to the rspack config file."
+ },
+ "optimization": {
+ "description": "Enables optimization of the build output.",
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "scripts": {
+ "type": "boolean",
+ "description": "Enables optimization of the scripts output.",
+ "default": true
+ },
+ "styles": {
+ "type": "boolean",
+ "description": "Enables optimization of the styles output.",
+ "default": true
+ }
+ },
+ "additionalProperties": false
+ },
+ { "type": "boolean" }
+ ]
+ },
+ "sourceMap": {
+ "description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.",
+ "default": true,
+ "oneOf": [{ "type": "boolean" }, { "type": "string" }]
+ },
+ "assets": {
+ "type": "array",
+ "description": "List of static application assets.",
+ "default": [],
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "glob": {
+ "type": "string",
+ "description": "The pattern to match."
+ },
+ "input": {
+ "type": "string",
+ "description": "The input directory path in which to apply 'glob'. Defaults to the project root."
+ },
+ "ignore": {
+ "description": "An array of globs to ignore.",
+ "type": "array",
+ "items": { "type": "string" }
+ },
+ "output": {
+ "type": "string",
+ "description": "Absolute path within the output."
+ },
+ "watch": {
+ "type": "boolean",
+ "description": "Enable re-building when files change.",
+ "default": false
+ }
+ },
+ "additionalProperties": false,
+ "required": ["glob", "input", "output"]
+ },
+ { "type": "string" }
+ ]
+ }
+ },
+ "extractLicenses": {
+ "type": "boolean",
+ "description": "Extract all licenses in a separate file.",
+ "default": true
+ },
+ "fileReplacements": {
+ "description": "Replace files with other files in the build.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "replace": {
+ "type": "string",
+ "description": "The file to be replaced.",
+ "x-completion-type": "file"
+ },
+ "with": {
+ "type": "string",
+ "description": "The file to replace with.",
+ "x-completion-type": "file"
+ }
+ },
+ "additionalProperties": false,
+ "required": ["replace", "with"]
+ },
+ "default": []
+ },
+ "mode": {
+ "type": "string",
+ "description": "Mode to run the build in.",
+ "enum": ["development", "production", "none"]
+ },
+ "generatePackageJson": {
+ "type": "boolean",
+ "description": "Generates a `package.json` and pruned lock file with the project's `node_module` dependencies populated for installing in a container. If a `package.json` exists in the project's directory, it will be reused with dependencies populated."
+ }
+ },
+ "required": ["target", "main", "outputPath", "tsConfig", "rspackConfig"],
+ "definitions": {
+ "assetPattern": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "glob": {
+ "type": "string",
+ "description": "The pattern to match."
+ },
+ "input": {
+ "type": "string",
+ "description": "The input directory path in which to apply 'glob'. Defaults to the project root."
+ },
+ "ignore": {
+ "description": "An array of globs to ignore.",
+ "type": "array",
+ "items": { "type": "string" }
+ },
+ "output": {
+ "type": "string",
+ "description": "Absolute path within the output."
+ },
+ "watch": {
+ "type": "boolean",
+ "description": "Enable re-building when files change.",
+ "default": false
+ }
+ },
+ "additionalProperties": false,
+ "required": ["glob", "input", "output"]
+ },
+ { "type": "string" }
+ ]
+ }
+ },
+ "presets": []
+ },
+ "description": "Run Rspack via an executor for a project.",
+ "aliases": [],
+ "hidden": false,
+ "path": "/packages/rspack/src/executors/rspack/schema.json",
+ "type": "executor"
+}
diff --git a/docs/generated/packages/rspack/executors/ssr-dev-server.json b/docs/generated/packages/rspack/executors/ssr-dev-server.json
new file mode 100644
index 0000000000000..cd0f60eca23f7
--- /dev/null
+++ b/docs/generated/packages/rspack/executors/ssr-dev-server.json
@@ -0,0 +1,46 @@
+{
+ "name": "ssr-dev-server",
+ "implementation": "/packages/rspack/src/executors/ssr-dev-server/ssr-dev-server.impl.ts",
+ "schema": {
+ "outputCapture": "direct-nodejs",
+ "title": "Rspack SSR Dev Server",
+ "description": "Serve a SSR application using rspack.",
+ "cli": "nx",
+ "type": "object",
+ "properties": {
+ "browserTarget": {
+ "type": "string",
+ "description": "Target which builds the browser application.",
+ "x-priority": "important"
+ },
+ "serverTarget": {
+ "type": "string",
+ "description": "Target which builds the server application.",
+ "x-priority": "important"
+ },
+ "port": {
+ "type": "number",
+ "description": "The port to be set on `process.env.PORT` for use in the server.",
+ "default": 4200,
+ "x-priority": "important"
+ },
+ "browserTargetOptions": {
+ "type": "object",
+ "description": "Additional options to pass into the browser build target.",
+ "default": {}
+ },
+ "serverTargetOptions": {
+ "type": "object",
+ "description": "Additional options to pass into the server build target.",
+ "default": {}
+ }
+ },
+ "required": ["browserTarget", "serverTarget"],
+ "presets": []
+ },
+ "description": "Serve a SSR application.",
+ "aliases": [],
+ "hidden": false,
+ "path": "/packages/rspack/src/executors/ssr-dev-server/schema.json",
+ "type": "executor"
+}
diff --git a/docs/generated/packages/rspack/generators/application.json b/docs/generated/packages/rspack/generators/application.json
new file mode 100644
index 0000000000000..5671e424cd53d
--- /dev/null
+++ b/docs/generated/packages/rspack/generators/application.json
@@ -0,0 +1,98 @@
+{
+ "name": "application",
+ "factory": "./src/generators/application/application",
+ "schema": {
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Application",
+ "title": "Application generator for React + rspack",
+ "type": "object",
+ "description": "React + Rspack application generator.",
+ "examples": [
+ {
+ "command": "nx g app myapp --directory=myorg",
+ "description": "Generate `apps/myorg/myapp` and `apps/myorg/myapp-e2e`"
+ }
+ ],
+ "properties": {
+ "name": {
+ "description": "The name of the application.",
+ "type": "string",
+ "$default": { "$source": "argv", "index": 0 },
+ "x-prompt": "What name would you like to use for the application?",
+ "pattern": "^[a-zA-Z].*$",
+ "x-priority": "important"
+ },
+ "framework": {
+ "type": "string",
+ "description": "The framework to use for the application.",
+ "x-prompt": "What framework do you want to use when generating this application?",
+ "enum": ["none", "react", "web", "nest"],
+ "alias": ["uiFramework"],
+ "x-priority": "important",
+ "default": "react"
+ },
+ "style": {
+ "description": "The file extension to be used for style files.",
+ "type": "string",
+ "default": "css",
+ "alias": "s",
+ "x-prompt": {
+ "message": "Which stylesheet format would you like to use?",
+ "type": "list",
+ "items": [
+ { "value": "css", "label": "CSS" },
+ {
+ "value": "scss",
+ "label": "SASS(.scss) [ http://sass-lang.com ]"
+ },
+ {
+ "value": "styl",
+ "label": "Stylus(.styl) [ http://stylus-lang.com ]"
+ },
+ {
+ "value": "less",
+ "label": "LESS [ http://lesscss.org ]"
+ },
+ { "value": "none", "label": "None" }
+ ]
+ }
+ },
+ "unitTestRunner": {
+ "type": "string",
+ "description": "The unit test runner to use.",
+ "enum": ["none", "jest"],
+ "default": "jest"
+ },
+ "e2eTestRunner": {
+ "type": "string",
+ "description": "The e2e test runner to use.",
+ "enum": ["none", "cypress"],
+ "default": "cypress"
+ },
+ "directory": {
+ "type": "string",
+ "description": "The directory to nest the app under."
+ },
+ "tags": {
+ "type": "string",
+ "description": "Add tags to the application (used for linting).",
+ "alias": "t"
+ },
+ "monorepo": {
+ "type": "boolean",
+ "description": "Creates an integrated monorepo.",
+ "aliases": ["integrated"]
+ },
+ "rootProject": { "type": "boolean", "x-priority": "internal" }
+ },
+ "required": ["name"],
+ "presets": []
+ },
+ "aliases": ["app"],
+ "x-type": "application",
+ "description": "React application generator.",
+ "implementation": "/packages/rspack/src/generators/application/application.ts",
+ "hidden": false,
+ "path": "/packages/rspack/src/generators/application/schema.json",
+ "type": "generator"
+}
diff --git a/docs/generated/packages/rspack/generators/configuration.json b/docs/generated/packages/rspack/generators/configuration.json
new file mode 100644
index 0000000000000..8b2b66ec0ef6c
--- /dev/null
+++ b/docs/generated/packages/rspack/generators/configuration.json
@@ -0,0 +1,78 @@
+{
+ "name": "configuration",
+ "factory": "./src/generators/configuration/configuration",
+ "schema": {
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Rspack",
+ "title": "Nx Rspack Configuration Generator",
+ "description": "Rspack configuration generator.",
+ "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 set up a rspack for?",
+ "x-priority": "important"
+ },
+ "framework": {
+ "type": "string",
+ "description": "The framework used by the project.",
+ "x-prompt": "What framework is the project you want to convert using?",
+ "enum": ["none", "react", "web", "nest"],
+ "alias": ["uiFramework"],
+ "x-priority": "important"
+ },
+ "main": {
+ "type": "string",
+ "description": "Path relative to the workspace root for the main entry file. Defaults to '/src/main.ts'.",
+ "x-priority": "important"
+ },
+ "tsConfig": {
+ "type": "string",
+ "description": "Path relative to the workspace root for the tsconfig file to build with. Defaults to '/tsconfig.app.json'.",
+ "x-priority": "important"
+ },
+ "target": {
+ "type": "string",
+ "description": "Target platform for the build, same as the rspack config option.",
+ "enum": ["node", "web"],
+ "default": "web"
+ },
+ "devServer": {
+ "type": "boolean",
+ "description": "Add a serve target to run a local rspack dev-server",
+ "default": false
+ },
+ "style": {
+ "type": "string",
+ "description": "The style solution to use.",
+ "enum": ["none", "css", "scss", "less"]
+ },
+ "newProject": {
+ "type": "boolean",
+ "description": "Is this a new project?",
+ "default": false,
+ "hidden": true
+ },
+ "buildTarget": {
+ "type": "string",
+ "description": "The build target of the project to be transformed to use the @nx/vite:build executor."
+ },
+ "serveTarget": {
+ "type": "string",
+ "description": "The serve target of the project to be transformed to use the @nx/vite:dev-server and @nx/vite:preview-server executors."
+ },
+ "rootProject": { "type": "boolean", "x-priority": "internal" }
+ },
+ "required": ["project"],
+ "presets": []
+ },
+ "description": "Rspack configuration generator.",
+ "implementation": "/packages/rspack/src/generators/configuration/configuration.ts",
+ "aliases": [],
+ "hidden": false,
+ "path": "/packages/rspack/src/generators/configuration/schema.json",
+ "type": "generator"
+}
diff --git a/docs/generated/packages/rspack/generators/init.json b/docs/generated/packages/rspack/generators/init.json
new file mode 100644
index 0000000000000..332b01a133fe8
--- /dev/null
+++ b/docs/generated/packages/rspack/generators/init.json
@@ -0,0 +1,39 @@
+{
+ "name": "init",
+ "factory": "./src/generators/init/init",
+ "schema": {
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Init",
+ "title": "Nx Rspack Init Generator",
+ "type": "object",
+ "description": "Rspack init generator.",
+ "properties": {
+ "framework": {
+ "type": "string",
+ "description": "The UI framework used by the project.",
+ "enum": ["none", "react", "web", "nest"],
+ "alias": ["uiFramework"]
+ },
+ "style": {
+ "type": "string",
+ "description": "The style solution to use.",
+ "enum": ["none", "css", "scss", "less", "styl"]
+ },
+ "rootProject": { "type": "boolean", "x-priority": "internal" },
+ "keepExistingVersions": {
+ "type": "boolean",
+ "x-priority": "internal",
+ "description": "Keep existing dependencies versions",
+ "default": false
+ }
+ },
+ "required": [],
+ "presets": []
+ },
+ "description": "Rspack init generator.",
+ "hidden": true,
+ "implementation": "/packages/rspack/src/generators/init/init.ts",
+ "aliases": [],
+ "path": "/packages/rspack/src/generators/init/schema.json",
+ "type": "generator"
+}
diff --git a/docs/generated/packages/rspack/generators/preset.json b/docs/generated/packages/rspack/generators/preset.json
new file mode 100644
index 0000000000000..33703949ce2a4
--- /dev/null
+++ b/docs/generated/packages/rspack/generators/preset.json
@@ -0,0 +1,70 @@
+{
+ "name": "preset",
+ "factory": "./src/generators/preset/preset",
+ "schema": {
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Preset",
+ "title": "Standalone React and rspack preset",
+ "description": "React + Rspack preset generator.",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "",
+ "$default": { "$source": "argv", "index": 0 },
+ "x-priority": "important"
+ },
+ "framework": {
+ "type": "string",
+ "description": "The framework to use for the application.",
+ "enum": ["none", "react", "web", "nest"],
+ "alias": ["uiFramework"],
+ "x-priority": "important",
+ "default": "react"
+ },
+ "less": { "type": "boolean", "description": "Use less for styling." },
+ "sass": { "type": "boolean", "description": "Use sass for styling." },
+ "stylus": { "type": "boolean", "description": "Use stylus for styling." },
+ "unitTestRunner": {
+ "type": "string",
+ "description": "The unit test runner to use.",
+ "enum": ["none", "jest"],
+ "default": "jest"
+ },
+ "e2eTestRunner": {
+ "type": "string",
+ "description": "The e2e test runner to use.",
+ "enum": ["none", "cypress"],
+ "default": "cypress"
+ },
+ "directory": {
+ "type": "string",
+ "description": "The directory to nest the app under."
+ },
+ "tags": {
+ "type": "string",
+ "description": "Add tags to the project (used for linting).",
+ "alias": "t"
+ },
+ "monorepo": {
+ "type": "boolean",
+ "description": "Creates an integrated monorepo.",
+ "default": false,
+ "aliases": ["integrated"]
+ },
+ "rootProject": {
+ "type": "boolean",
+ "x-priority": "internal",
+ "default": true
+ }
+ },
+ "required": ["name"],
+ "presets": []
+ },
+ "description": "React preset generator.",
+ "hidden": true,
+ "implementation": "/packages/rspack/src/generators/preset/preset.ts",
+ "aliases": [],
+ "path": "/packages/rspack/src/generators/preset/schema.json",
+ "type": "generator"
+}
diff --git a/docs/map.json b/docs/map.json
index 04c7f6989a8c1..42d0533b08ce6 100644
--- a/docs/map.json
+++ b/docs/map.json
@@ -2558,6 +2558,19 @@
}
]
},
+ {
+ "name": "rspack",
+ "id": "rspack",
+ "description": "Rspack package.",
+ "itemList": [
+ {
+ "name": "Overview",
+ "id": "overview",
+ "path": "/nx-api/rspack",
+ "file": "shared/packages/rspack/rspack-plugin"
+ }
+ ]
+ },
{
"name": "detox",
"id": "detox",
diff --git a/docs/shared/packages/rspack/rspack-plugin.md b/docs/shared/packages/rspack/rspack-plugin.md
new file mode 100644
index 0000000000000..95e3375773c14
--- /dev/null
+++ b/docs/shared/packages/rspack/rspack-plugin.md
@@ -0,0 +1,98 @@
+---
+title: Overview of the Nx Rspack Plugin
+description: The Nx Plugin for Rspack contains executors, generators, and utilities for managing Rspack projects in an Nx Workspace.
+---
+
+The Nx Plugin for Rspack contains executors, generators, and utilities for managing Rspack projects in an Nx Workspace.
+
+## Setting Up @nx/rspack
+
+### Installation
+
+{% callout type="note" title="Keep Nx Package Versions In Sync" %}
+Make sure to install the `@nx/rspack` version that matches the version of `nx` in your repository. If the version numbers get out of sync, you can encounter some difficult to debug errors. You can [fix Nx version mismatches with this recipe](/recipes/tips-n-tricks/keep-nx-versions-in-sync).
+{% /callout %}
+
+In any Nx workspace, you can install `@nx/rspack` by running the following command:
+
+{% tabs %}
+{% tab label="Nx 18+" %}
+
+```shell {% skipRescope=true %}
+nx add @nx/rspack
+```
+
+This will install the correct version of `@nx/rspack`.
+
+### How @nx/rspack Infers Tasks
+
+The `@nx/rspack` plugin will create a task for any project that has a Rspack configuration file present. Any of the following files will be recognized as a Rspack configuration file:
+
+- `rspack.config.js`
+- `rspack.config.ts`
+- `rspack.config.mjs`
+- `rspack.config.mts`
+- `rspack.config.cjs`
+- `rspack.config.cts`
+
+### View Inferred Tasks
+
+To view inferred tasks for a project, open the [project details view](/concepts/inferred-tasks) in Nx Console or run `nx show project my-project --web` in the command line.
+
+### @nx/rspack Configuration
+
+The `@nx/rspack/plugin` is configured in the `plugins` array in `nx.json`.
+
+```json {% fileName="nx.json" %}
+{
+ "plugins": [
+ {
+ "plugin": "@nx/rspack/plugin",
+ "options": {
+ "buildTargetName": "build",
+ "previewTargetName": "preview",
+ "serveTargetName": "serve",
+ "serveStaticTargetName": "serve-static"
+ }
+ }
+ ]
+}
+```
+
+The `buildTargetName`, `previewTargetName`, `serveTargetName` and `serveStaticTargetName` options control the names of the inferred Rspack tasks. The default names are `build`, `preview`, `serve` and `serve-static`.
+
+{% /tab %}
+{% tab label="Nx < 18" %}
+
+Install the `@nx/rspack` package with your package manager.
+
+```shell
+npm add -D @nx/rspack
+```
+
+{% /tab %}
+{% /tabs %}
+
+## Using @nx/rspack
+
+### Generate a new project using Rspack
+
+You can generate a [React](/nx-api/react) application or library that uses Rspack. The [`@nx/react:app`](/nx-api/react/generators/application) and [`@nx/react:lib`](/nx-api/react/generators/library) generators accept the `bundler` option, where you can pass `rspack`. This will generate a new application configured to use Rspack, and it will also install all the necessary dependencies, including the `@nx/rspack` plugin.
+
+To generate a React application using Rspack, run the following:
+
+```bash
+nx g @nx/react:app my-app --bundler=rspack
+```
+
+To generate a React library using Rspack, run the following:
+
+```bash
+nx g @nx/react:lib my-lib --bundler=rspack
+```
+
+### Modify an existing React project to use Rspack
+
+You can use the `@nx/rspack:configuration` generator to change your React to use Rspack. This generator will modify your project's configuration to use Rspack, and it will also install all the necessary dependencies, including the `@nx/rspack` plugin.
+
+You can read more about this generator on the [`@nx/rspack:configuration`](/nx-api/rspack/generators/configuration) generator page.
diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md
index 59241b6e85b5c..bc9c361125e6e 100644
--- a/docs/shared/reference/sitemap.md
+++ b/docs/shared/reference/sitemap.md
@@ -682,6 +682,21 @@
- [init](/nx-api/rollup/generators/init)
- [configuration](/nx-api/rollup/generators/configuration)
- [convert-to-inferred](/nx-api/rollup/generators/convert-to-inferred)
+ - [rspack](/nx-api/rspack)
+ - [documents](/nx-api/rspack/documents)
+ - [Overview](/nx-api/rspack/documents/overview)
+ - [executors](/nx-api/rspack/executors)
+ - [rspack](/nx-api/rspack/executors/rspack)
+ - [dev-server](/nx-api/rspack/executors/dev-server)
+ - [ssr-dev-server](/nx-api/rspack/executors/ssr-dev-server)
+ - [module-federation-dev-server](/nx-api/rspack/executors/module-federation-dev-server)
+ - [module-federation-ssr-dev-server](/nx-api/rspack/executors/module-federation-ssr-dev-server)
+ - [module-federation-static-server](/nx-api/rspack/executors/module-federation-static-server)
+ - [generators](/nx-api/rspack/generators)
+ - [configuration](/nx-api/rspack/generators/configuration)
+ - [init](/nx-api/rspack/generators/init)
+ - [preset](/nx-api/rspack/generators/preset)
+ - [application](/nx-api/rspack/generators/application)
- [storybook](/nx-api/storybook)
- [documents](/nx-api/storybook/documents)
- [Overview](/nx-api/storybook/documents/overview)
diff --git a/e2e/react/src/react.test.ts b/e2e/react/src/react.test.ts
index 72c282fe97024..e627fda2f5eff 100644
--- a/e2e/react/src/react.test.ts
+++ b/e2e/react/src/react.test.ts
@@ -63,6 +63,41 @@ describe('React Applications', () => {
}
}, 250_000);
+ it('should be able to use Rspack to build and test apps', async () => {
+ const appName = uniq('app');
+ const libName = uniq('lib');
+
+ runCLI(
+ `generate @nx/react:app ${appName} --bundler=rspack --unit-test-runner=vitest --no-interactive --skipFormat`
+ );
+ runCLI(
+ `generate @nx/react:lib ${libName} --bundler=none --no-interactive --unit-test-runner=vitest --skipFormat`
+ );
+
+ // Library generated with Vite
+ checkFilesExist(`${libName}/vite.config.ts`);
+
+ const mainPath = `${appName}/src/main.tsx`;
+ updateFile(
+ mainPath,
+ `
+ import '@${proj}/${libName}';
+ ${readFile(mainPath)}
+ `
+ );
+
+ runCLI(`build ${appName}`);
+
+ checkFilesExist(`dist/${appName}/index.html`);
+
+ if (runE2ETests()) {
+ // TODO(Colum): investigate why webkit is failing
+ const e2eResults = runCLI(`e2e ${appName}-e2e -- --project=chromium`);
+ expect(e2eResults).toContain('Successfully ran target e2e for project');
+ expect(await killPorts()).toBeTruthy();
+ }
+ }, 250_000);
+
it('should be able to generate a react app + lib (with CSR and SSR)', async () => {
const appName = uniq('app');
const libName = uniq('lib');
diff --git a/e2e/rspack/jest.config.ts b/e2e/rspack/jest.config.ts
new file mode 100644
index 0000000000000..75b517b2d57f7
--- /dev/null
+++ b/e2e/rspack/jest.config.ts
@@ -0,0 +1,19 @@
+/* eslint-disable */
+export default {
+ displayName: 'e2e-rspack',
+ preset: '../jest.preset.e2e.js',
+ maxWorkers: 1,
+ globals: {},
+ globalSetup: '../utils/global-setup.ts',
+ globalTeardown: '../utils/global-teardown.ts',
+ transform: {
+ '^.+\\.[tj]s$': [
+ 'ts-jest',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ },
+ ],
+ },
+ moduleFileExtensions: ['ts', 'js', 'html'],
+ coverageDirectory: '../../coverage/e2e/e2e-rspack',
+};
diff --git a/e2e/rspack/project.json b/e2e/rspack/project.json
new file mode 100644
index 0000000000000..51c879eec91cd
--- /dev/null
+++ b/e2e/rspack/project.json
@@ -0,0 +1,10 @@
+{
+ "name": "e2e-rspack",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "e2e/rspack",
+ "// targets": "to see all targets run: nx show project e2e-rspack --web",
+ "targets": {},
+ "tags": [],
+ "implicitDependencies": ["rspack"]
+}
diff --git a/e2e/rspack/tests/rspack.spec.ts b/e2e/rspack/tests/rspack.spec.ts
new file mode 100644
index 0000000000000..abeac82f237a9
--- /dev/null
+++ b/e2e/rspack/tests/rspack.spec.ts
@@ -0,0 +1,145 @@
+import { getPackageManagerCommand } from '@nx/devkit';
+import {
+ checkFilesExist,
+ cleanupProject,
+ listFiles,
+ newProject,
+ tmpProjPath,
+ uniq,
+ updateFile,
+ runCLI,
+ runCommand,
+} from '@nx/e2e/utils';
+import { execSync } from 'child_process';
+import { writeFileSync } from 'fs';
+import { join } from 'path';
+
+describe('rspack e2e', () => {
+ let proj: string;
+
+ // Setting up individual workspaces per
+ // test can cause e2e runs to take a long time.
+ // For this reason, we recommend each suite only
+ // consumes 1 workspace. The tests should each operate
+ // on a unique project in the workspace, such that they
+ // are not dependant on one another.
+ beforeAll(() => {
+ proj = newProject({ packages: ['@nx/rspack'] });
+ });
+
+ afterAll(() => cleanupProject());
+
+ it('should create rspack root project and additional apps', async () => {
+ const project = uniq('myapp');
+ runCLI(
+ `generate @nx/rspack:preset ${project} --framework=react --unitTestRunner=jest --e2eTestRunner=cypress`
+ );
+
+ // Added this so that the nx-ecosystem-ci tests don't throw jest error
+ writeFileSync(
+ join(tmpProjPath(), '.babelrc'),
+ `
+ {
+ "presets": [
+ "@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript",
+ [
+ "@nx/react/babel",
+ {
+ "runtime": "automatic"
+ }
+ ]
+ ],
+ "plugins": ["@babel/plugin-transform-runtime"]
+ }
+ `
+ );
+
+ const pm = getPackageManagerCommand();
+ runCommand(
+ pm.addDev +
+ ' @babel/preset-react @babel/preset-env @babel/preset-typescript'
+ );
+
+ let result = runCLI(`build ${project}`, {
+ env: { NODE_ENV: 'production' },
+ });
+ expect(result).toContain('Successfully ran target build');
+ // Make sure expected files are present.
+ expect(listFiles(`dist/${project}`)).toHaveLength(5);
+
+ result = runCLI(`test ${project}`);
+ expect(result).toContain('Successfully ran target test');
+
+ // TODO(Colum): re-enable when cypress issue is resolved
+ // result = runCLI(`e2e e2e`);
+ // expect(result.stdout).toContain('Successfully ran target e2e');
+
+ // Update app and make sure previous dist files are not present.
+ updateFile(`src/app/app.tsx`, (content) => {
+ return `${content}\nconsole.log('hello');
+ `;
+ });
+ result = runCLI(`build ${project}`, {
+ env: { NODE_ENV: 'production' },
+ });
+ expect(result).toContain('Successfully ran target build');
+ expect(listFiles(`dist/${project}`)).toHaveLength(5); // same length as before
+
+ // Generate a new app and check that the files are correct
+ const app2 = uniq('app2');
+ runCLI(
+ `generate @nx/rspack:app ${app2} --framework=react --unitTestRunner=jest --e2eTestRunner=cypress --style=css`
+ );
+ checkFilesExist(`${app2}/project.json`, `${app2}-e2e/project.json`);
+
+ // Added this so that the nx-ecosystem-ci tests don't throw jest error
+ writeFileSync(
+ join(tmpProjPath(), app2, '.babelrc'),
+ `
+ {
+ "presets": [
+ "@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript",
+ [
+ "@nx/react/babel",
+ {
+ "runtime": "automatic"
+ }
+ ]
+ ],
+ "plugins": ["@babel/plugin-transform-runtime"]
+ }
+ `
+ );
+
+ result = runCLI(`build ${app2}`, {
+ env: { NODE_ENV: 'production' },
+ });
+ expect(result).toContain('Successfully ran target build');
+ // Make sure expected files are present.
+ expect(listFiles(`dist/${app2}`)).toHaveLength(5);
+
+ result = runCLI(`test ${app2}`);
+ expect(result).toContain('Successfully ran target test');
+
+ // TODO(Colum): re-enable when cypress issue is resolved
+ // result = runCLI(`e2e ${app2}-e2e`);
+ // expect(result.stdout).toContain('Successfully ran target e2e');
+
+ // Generate a Nest app and verify build output
+ const app3 = uniq('app3');
+ runCLI(
+ `generate @nx/rspack:app ${app3} --framework=nest --unitTestRunner=jest --no-interactive`
+ );
+ checkFilesExist(`${app3}/project.json`);
+
+ result = runCLI(`build ${app3}`);
+ expect(result).toContain('Successfully ran target build');
+ // Make sure expected files are present.
+ expect(listFiles(`dist/${app3}`)).toHaveLength(2);
+
+ result = runCLI(`build ${app3} --generatePackageJson=true`);
+ expect(result).toContain('Successfully ran target build');
+ // Make sure expected files are present.
+ expect(listFiles(`dist/${app3}`)).toHaveLength(4);
+ }, 200_000);
+});
diff --git a/e2e/rspack/tsconfig.json b/e2e/rspack/tsconfig.json
new file mode 100644
index 0000000000000..f6e253991353b
--- /dev/null
+++ b/e2e/rspack/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "types": ["node", "jest"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ]
+}
diff --git a/e2e/rspack/tsconfig.spec.json b/e2e/rspack/tsconfig.spec.json
new file mode 100644
index 0000000000000..546f12877f7f0
--- /dev/null
+++ b/e2e/rspack/tsconfig.spec.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "module": "commonjs",
+ "types": ["jest", "node"]
+ },
+ "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
+}
diff --git a/e2e/utils/create-project-utils.ts b/e2e/utils/create-project-utils.ts
index ff89a41e51b19..73c41a95d1a16 100644
--- a/e2e/utils/create-project-utils.ts
+++ b/e2e/utils/create-project-utils.ts
@@ -56,6 +56,7 @@ const nxPackages = [
`@nx/rollup`,
`@nx/react`,
`@nx/remix`,
+ `@nx/rspack`,
`@nx/storybook`,
`@nx/vue`,
`@nx/vite`,
diff --git a/nx-dev/nx-dev/pages/nx-api/rspack/documents/index.tsx b/nx-dev/nx-dev/pages/nx-api/rspack/documents/index.tsx
deleted file mode 100644
index 3775bf3c1fa3e..0000000000000
--- a/nx-dev/nx-dev/pages/nx-api/rspack/documents/index.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { getPackagesSections } from '@nx/nx-dev/data-access-menu';
-import { sortCorePackagesFirst } from '@nx/nx-dev/data-access-packages';
-import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
-import { ProcessedPackageMetadata } from '@nx/nx-dev/models-package';
-import { DocumentationHeader, SidebarContainer } from '@nx/nx-dev/ui-common';
-import { PackageSchemaSubList } from '@nx/nx-dev/feature-package-schema-viewer/src/lib/package-schema-sub-list';
-import { menusApi } from '../../../../lib/menus.api';
-import { useNavToggle } from '../../../../lib/navigation-toggle.effect';
-import { pkg } from '../../../../lib/rspack/pkg';
-import { ScrollableContent } from '@nx/ui-scrollable-content';
-
-export default function DocumentsIndex({
- menu,
- pkg,
-}: {
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
-}): JSX.Element {
- const { toggleNav, navIsOpen } = useNavToggle();
-
- const vm: { menu: Menu; package: ProcessedPackageMetadata } = {
- menu: {
- sections: sortCorePackagesFirst(
- getPackagesSections(menu),
- 'id'
- ),
- },
- package: pkg,
- };
-
- /**
- * Show either the docviewer or the package view depending on:
- * - docviewer: it is a documentation document
- * - packageviewer: it is package generated documentation
- */
-
- return (
-
- );
-}
-
-export async function getStaticProps(): Promise<{
- props: {
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
- };
-}> {
- return {
- props: {
- menu: menusApi.getMenu('nx-api', 'nx-api'),
- pkg,
- },
- };
-}
diff --git a/nx-dev/nx-dev/pages/nx-api/rspack/documents/overview.tsx b/nx-dev/nx-dev/pages/nx-api/rspack/documents/overview.tsx
deleted file mode 100644
index 5a9091d157600..0000000000000
--- a/nx-dev/nx-dev/pages/nx-api/rspack/documents/overview.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { getPackagesSections } from '@nx/nx-dev/data-access-menu';
-import { sortCorePackagesFirst } from '@nx/nx-dev/data-access-packages';
-import { DocViewer } from '@nx/nx-dev/feature-doc-viewer';
-import { ProcessedDocument, RelatedDocument } from '@nx/nx-dev/models-document';
-import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
-import { ProcessedPackageMetadata } from '@nx/nx-dev/models-package';
-import { DocumentationHeader, SidebarContainer } from '@nx/nx-dev/ui-common';
-import { menusApi } from '../../../../lib/menus.api';
-import { useNavToggle } from '../../../../lib/navigation-toggle.effect';
-import { content } from '../../../../lib/rspack/content/overview';
-import { pkg } from '../../../../lib/rspack/pkg';
-import { fetchGithubStarCount } from '../../../../lib/githubStars.api';
-import { ScrollableContent } from '@nx/ui-scrollable-content';
-
-export default function Overview({
- document,
- menu,
- relatedDocuments,
- widgetData,
-}: {
- document: ProcessedDocument;
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
- relatedDocuments: RelatedDocument[];
- widgetData: { githubStarsCount: number };
-}): JSX.Element {
- const { toggleNav, navIsOpen } = useNavToggle();
-
- const vm: {
- document: ProcessedDocument;
- menu: Menu;
- relatedDocuments: RelatedDocument[];
- } = {
- document,
- menu: {
- sections: sortCorePackagesFirst(
- getPackagesSections(menu),
- 'id'
- ),
- },
- relatedDocuments,
- };
-
- return (
-
- );
-}
-
-export async function getStaticProps() {
- const document = {
- content: content,
- description: '',
- filePath: '',
- id: 'overview',
- name: 'Overview of the Nx Rspack Plugin',
- relatedDocuments: {},
- tags: [],
- };
-
- return {
- props: {
- pkg,
- document,
- widgetData: {
- githubStarsCount: await fetchGithubStarCount(),
- },
- relatedDocuments: [],
- menu: menusApi.getMenu('nx-api', ''),
- },
- };
-}
diff --git a/nx-dev/nx-dev/pages/nx-api/rspack/documents/rspack-config-setup.tsx b/nx-dev/nx-dev/pages/nx-api/rspack/documents/rspack-config-setup.tsx
deleted file mode 100644
index 251cbbc8d9861..0000000000000
--- a/nx-dev/nx-dev/pages/nx-api/rspack/documents/rspack-config-setup.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import { getPackagesSections } from '@nx/nx-dev/data-access-menu';
-import { sortCorePackagesFirst } from '@nx/nx-dev/data-access-packages';
-import { DocViewer } from '@nx/nx-dev/feature-doc-viewer';
-import { ProcessedDocument, RelatedDocument } from '@nx/nx-dev/models-document';
-import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
-import { ProcessedPackageMetadata } from '@nx/nx-dev/models-package';
-import { DocumentationHeader, SidebarContainer } from '@nx/nx-dev/ui-common';
-import { menusApi } from '../../../../lib/menus.api';
-import { useNavToggle } from '../../../../lib/navigation-toggle.effect';
-import { content } from '../../../../lib/rspack/content/rspack-config-setup';
-import { pkg } from '../../../../lib/rspack/pkg';
-import { fetchGithubStarCount } from '../../../../lib/githubStars.api';
-import { ScrollableContent } from '@nx/ui-scrollable-content';
-
-export default function RspackConfigSetup({
- document,
- menu,
- relatedDocuments,
- widgetData,
-}: {
- document: ProcessedDocument;
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
- relatedDocuments: RelatedDocument[];
- widgetData: { githubStarsCount: number };
-}): JSX.Element {
- const { toggleNav, navIsOpen } = useNavToggle();
-
- const vm: {
- document: ProcessedDocument;
- menu: Menu;
- relatedDocuments: RelatedDocument[];
- } = {
- document,
- menu: {
- sections: sortCorePackagesFirst(
- getPackagesSections(menu),
- 'id'
- ),
- },
- relatedDocuments,
- };
-
- return (
-
- );
-}
-
-export async function getStaticProps() {
- const document = {
- content: content,
- description:
- 'A guide on how to configure Rspack on your Nx workspace, and instructions on how to customize your Rspack configuration.',
- filePath: '',
- id: 'rspack-plugins',
- name: ' How to configure Rspack on your Nx workspace',
- relatedDocuments: {},
- tags: [],
- };
-
- return {
- props: {
- pkg,
- document,
- widgetData: {
- githubStarsCount: await fetchGithubStarCount(),
- },
- relatedDocuments: [],
- menu: menusApi.getMenu('nx-api', ''),
- },
- };
-}
diff --git a/nx-dev/nx-dev/pages/nx-api/rspack/documents/rspack-plugins.tsx b/nx-dev/nx-dev/pages/nx-api/rspack/documents/rspack-plugins.tsx
deleted file mode 100644
index dbf1f2e61fd52..0000000000000
--- a/nx-dev/nx-dev/pages/nx-api/rspack/documents/rspack-plugins.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { getPackagesSections } from '@nx/nx-dev/data-access-menu';
-import { sortCorePackagesFirst } from '@nx/nx-dev/data-access-packages';
-import { DocViewer } from '@nx/nx-dev/feature-doc-viewer';
-import { ProcessedDocument, RelatedDocument } from '@nx/nx-dev/models-document';
-import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
-import { ProcessedPackageMetadata } from '@nx/nx-dev/models-package';
-import { DocumentationHeader, SidebarContainer } from '@nx/nx-dev/ui-common';
-import { menusApi } from '../../../../lib/menus.api';
-import { useNavToggle } from '../../../../lib/navigation-toggle.effect';
-import { content } from '../../../../lib/rspack/content/rspack-plugin';
-import { pkg } from '../../../../lib/rspack/pkg';
-import { fetchGithubStarCount } from '../../../../lib/githubStars.api';
-import { ScrollableContent } from '@nx/ui-scrollable-content';
-
-export default function RspackPlugins({
- document,
- menu,
- relatedDocuments,
- widgetData,
-}: {
- document: ProcessedDocument;
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
- relatedDocuments: RelatedDocument[];
- widgetData: { githubStarsCount: number };
-}): JSX.Element {
- const { toggleNav, navIsOpen } = useNavToggle();
-
- const vm: {
- document: ProcessedDocument;
- menu: Menu;
- relatedDocuments: RelatedDocument[];
- } = {
- document,
- menu: {
- sections: sortCorePackagesFirst(
- getPackagesSections(menu),
- 'id'
- ),
- },
- relatedDocuments,
- };
-
- return (
-
- );
-}
-
-export async function getStaticProps() {
- const document = {
- content: content,
- description: 'Rspack plugins',
- filePath: '',
- id: 'rspack-plugins',
- name: 'Rspack plugins',
- relatedDocuments: {},
- tags: [],
- };
-
- return {
- props: {
- pkg,
- document,
- widgetData: {
- githubStarsCount: await fetchGithubStarCount(),
- },
- relatedDocuments: [],
- menu: menusApi.getMenu('nx-api', ''),
- },
- };
-}
diff --git a/nx-dev/nx-dev/pages/nx-api/rspack/executors/dev-server.tsx b/nx-dev/nx-dev/pages/nx-api/rspack/executors/dev-server.tsx
deleted file mode 100644
index fa20030839bc1..0000000000000
--- a/nx-dev/nx-dev/pages/nx-api/rspack/executors/dev-server.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { PackageSchemaViewer } from '@nx/nx-dev/feature-package-schema-viewer';
-import { getPackagesSections } from '@nx/nx-dev/data-access-menu';
-import { sortCorePackagesFirst } from '@nx/nx-dev/data-access-packages';
-import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
-import {
- ProcessedPackageMetadata,
- SchemaMetadata,
-} from '@nx/nx-dev/models-package';
-import { DocumentationHeader, SidebarContainer } from '@nx/nx-dev/ui-common';
-import { menusApi } from '../../../../lib/menus.api';
-import { useNavToggle } from '../../../../lib/navigation-toggle.effect';
-import { schema } from '../../../../lib/rspack/schema/executors/dev-server';
-import { pkg } from '../../../../lib/rspack/pkg';
-import { ScrollableContent } from '@nx/ui-scrollable-content';
-
-export default function DevServerExecutor({
- menu,
- pkg,
- schema,
-}: {
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
- schema: SchemaMetadata;
-}): JSX.Element {
- const { toggleNav, navIsOpen } = useNavToggle();
-
- const vm: {
- menu: Menu;
- package: ProcessedPackageMetadata;
- schema: SchemaMetadata;
- } = {
- menu: {
- sections: sortCorePackagesFirst(
- getPackagesSections(menu),
- 'id'
- ),
- },
- package: pkg,
- schema: schema,
- };
-
- /**
- * Show either the docviewer or the package view depending on:
- * - docviewer: it is a documentation document
- * - packageviewer: it is package generated documentation
- */
-
- return (
-
- );
-}
-
-export async function getStaticProps() {
- return {
- props: {
- pkg,
- schema,
- menu: menusApi.getMenu('nx-api', 'nx-api'),
- },
- };
-}
diff --git a/nx-dev/nx-dev/pages/nx-api/rspack/executors/index.tsx b/nx-dev/nx-dev/pages/nx-api/rspack/executors/index.tsx
deleted file mode 100644
index 672c136c2270b..0000000000000
--- a/nx-dev/nx-dev/pages/nx-api/rspack/executors/index.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { getPackagesSections } from '@nx/nx-dev/data-access-menu';
-import { sortCorePackagesFirst } from '@nx/nx-dev/data-access-packages';
-import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
-import { ProcessedPackageMetadata } from '@nx/nx-dev/models-package';
-import { DocumentationHeader, SidebarContainer } from '@nx/nx-dev/ui-common';
-import { PackageSchemaSubList } from '@nx/nx-dev/feature-package-schema-viewer/src/lib/package-schema-sub-list';
-import { menusApi } from '../../../../lib/menus.api';
-import { useNavToggle } from '../../../../lib/navigation-toggle.effect';
-import { pkg } from '../../../../lib/rspack/pkg';
-import { ScrollableContent } from '@nx/ui-scrollable-content';
-
-export default function ExecutorsIndex({
- menu,
- pkg,
-}: {
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
-}): JSX.Element {
- const { toggleNav, navIsOpen } = useNavToggle();
-
- const vm: { menu: Menu; package: ProcessedPackageMetadata } = {
- menu: {
- sections: sortCorePackagesFirst(
- getPackagesSections(menu),
- 'id'
- ),
- },
- package: pkg,
- };
-
- /**
- * Show either the docviewer or the package view depending on:
- * - docviewer: it is a documentation document
- * - packageviewer: it is package generated documentation
- */
-
- return (
-
- );
-}
-
-export async function getStaticProps(): Promise<{
- props: {
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
- };
-}> {
- return {
- props: {
- menu: menusApi.getMenu('nx-api', 'nx-api'),
- pkg,
- },
- };
-}
diff --git a/nx-dev/nx-dev/pages/nx-api/rspack/executors/rspack.tsx b/nx-dev/nx-dev/pages/nx-api/rspack/executors/rspack.tsx
deleted file mode 100644
index e5e43ce6a71e2..0000000000000
--- a/nx-dev/nx-dev/pages/nx-api/rspack/executors/rspack.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { PackageSchemaViewer } from '@nx/nx-dev/feature-package-schema-viewer';
-import { getPackagesSections } from '@nx/nx-dev/data-access-menu';
-import { sortCorePackagesFirst } from '@nx/nx-dev/data-access-packages';
-import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
-import {
- ProcessedPackageMetadata,
- SchemaMetadata,
-} from '@nx/nx-dev/models-package';
-import { DocumentationHeader, SidebarContainer } from '@nx/nx-dev/ui-common';
-import { menusApi } from '../../../../lib/menus.api';
-import { useNavToggle } from '../../../../lib/navigation-toggle.effect';
-import { schema } from '../../../../lib/rspack/schema/executors/rspack';
-import { pkg } from '../../../../lib/rspack/pkg';
-import { ScrollableContent } from '@nx/ui-scrollable-content';
-
-export default function RspackExecutor({
- menu,
- pkg,
- schema,
-}: {
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
- schema: SchemaMetadata;
-}): JSX.Element {
- const { toggleNav, navIsOpen } = useNavToggle();
-
- const vm: {
- menu: Menu;
- package: ProcessedPackageMetadata;
- schema: SchemaMetadata;
- } = {
- menu: {
- sections: sortCorePackagesFirst(
- getPackagesSections(menu),
- 'id'
- ),
- },
- package: pkg,
- schema: schema,
- };
-
- /**
- * Show either the docviewer or the package view depending on:
- * - docviewer: it is a documentation document
- * - packageviewer: it is package generated documentation
- */
-
- return (
-
- );
-}
-
-export async function getStaticProps() {
- return {
- props: {
- pkg,
- schema,
- menu: menusApi.getMenu('nx-api', 'nx-api'),
- },
- };
-}
diff --git a/nx-dev/nx-dev/pages/nx-api/rspack/generators/application.tsx b/nx-dev/nx-dev/pages/nx-api/rspack/generators/application.tsx
deleted file mode 100644
index 325c9ea2d3af1..0000000000000
--- a/nx-dev/nx-dev/pages/nx-api/rspack/generators/application.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { PackageSchemaViewer } from '@nx/nx-dev/feature-package-schema-viewer';
-import { getPackagesSections } from '@nx/nx-dev/data-access-menu';
-import { sortCorePackagesFirst } from '@nx/nx-dev/data-access-packages';
-import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
-import {
- ProcessedPackageMetadata,
- SchemaMetadata,
-} from '@nx/nx-dev/models-package';
-import { DocumentationHeader, SidebarContainer } from '@nx/nx-dev/ui-common';
-import { menusApi } from '../../../../lib/menus.api';
-import { useNavToggle } from '../../../../lib/navigation-toggle.effect';
-import { schema } from '../../../../lib/rspack/schema/generators/application';
-import { pkg } from '../../../../lib/rspack/pkg';
-import { ScrollableContent } from '@nx/ui-scrollable-content';
-
-export default function ApplicationGenerator({
- menu,
- pkg,
- schema,
-}: {
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
- schema: SchemaMetadata;
-}): JSX.Element {
- const { toggleNav, navIsOpen } = useNavToggle();
-
- const vm: {
- menu: Menu;
- package: ProcessedPackageMetadata;
- schema: SchemaMetadata;
- } = {
- menu: {
- sections: sortCorePackagesFirst(
- getPackagesSections(menu),
- 'id'
- ),
- },
- package: pkg,
- schema: schema,
- };
-
- /**
- * Show either the docviewer or the package view depending on:
- * - docviewer: it is a documentation document
- * - packageviewer: it is package generated documentation
- */
-
- return (
-
- );
-}
-
-export async function getStaticProps() {
- return {
- props: {
- pkg,
- schema,
- menu: menusApi.getMenu('nx-api', 'nx-api'),
- },
- };
-}
diff --git a/nx-dev/nx-dev/pages/nx-api/rspack/generators/configuration.tsx b/nx-dev/nx-dev/pages/nx-api/rspack/generators/configuration.tsx
deleted file mode 100644
index 6515cd1252854..0000000000000
--- a/nx-dev/nx-dev/pages/nx-api/rspack/generators/configuration.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { PackageSchemaViewer } from '@nx/nx-dev/feature-package-schema-viewer';
-import { getPackagesSections } from '@nx/nx-dev/data-access-menu';
-import { sortCorePackagesFirst } from '@nx/nx-dev/data-access-packages';
-import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
-import {
- ProcessedPackageMetadata,
- SchemaMetadata,
-} from '@nx/nx-dev/models-package';
-import { DocumentationHeader, SidebarContainer } from '@nx/nx-dev/ui-common';
-import { menusApi } from '../../../../lib/menus.api';
-import { useNavToggle } from '../../../../lib/navigation-toggle.effect';
-import { schema } from '../../../../lib/rspack/schema/generators/configuration';
-import { pkg } from '../../../../lib/rspack/pkg';
-import { ScrollableContent } from '@nx/ui-scrollable-content';
-
-export default function ConfigurationGenerator({
- menu,
- pkg,
- schema,
-}: {
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
- schema: SchemaMetadata;
-}): JSX.Element {
- const { toggleNav, navIsOpen } = useNavToggle();
-
- const vm: {
- menu: Menu;
- package: ProcessedPackageMetadata;
- schema: SchemaMetadata;
- } = {
- menu: {
- sections: sortCorePackagesFirst(
- getPackagesSections(menu),
- 'id'
- ),
- },
- package: pkg,
- schema: schema,
- };
-
- /**
- * Show either the docviewer or the package view depending on:
- * - docviewer: it is a documentation document
- * - packageviewer: it is package generated documentation
- */
-
- return (
-
- );
-}
-
-export async function getStaticProps() {
- return {
- props: {
- pkg,
- schema,
- menu: menusApi.getMenu('nx-api', 'nx-api'),
- },
- };
-}
diff --git a/nx-dev/nx-dev/pages/nx-api/rspack/generators/index.tsx b/nx-dev/nx-dev/pages/nx-api/rspack/generators/index.tsx
deleted file mode 100644
index 8c656663f617f..0000000000000
--- a/nx-dev/nx-dev/pages/nx-api/rspack/generators/index.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { getPackagesSections } from '@nx/nx-dev/data-access-menu';
-import { sortCorePackagesFirst } from '@nx/nx-dev/data-access-packages';
-import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
-import { ProcessedPackageMetadata } from '@nx/nx-dev/models-package';
-import { DocumentationHeader, SidebarContainer } from '@nx/nx-dev/ui-common';
-import { PackageSchemaSubList } from '@nx/nx-dev/feature-package-schema-viewer/src/lib/package-schema-sub-list';
-import { menusApi } from '../../../../lib/menus.api';
-import { useNavToggle } from '../../../../lib/navigation-toggle.effect';
-import { pkg } from '../../../../lib/rspack/pkg';
-import { ScrollableContent } from '@nx/ui-scrollable-content';
-
-export default function GeneratorsIndex({
- menu,
- pkg,
-}: {
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
-}): JSX.Element {
- const { toggleNav, navIsOpen } = useNavToggle();
-
- const vm: { menu: Menu; package: ProcessedPackageMetadata } = {
- menu: {
- sections: sortCorePackagesFirst(
- getPackagesSections(menu),
- 'id'
- ),
- },
- package: pkg,
- };
-
- /**
- * Show either the docviewer or the package view depending on:
- * - docviewer: it is a documentation document
- * - packageviewer: it is package generated documentation
- */
-
- return (
-
- );
-}
-
-export async function getStaticProps(): Promise<{
- props: {
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
- };
-}> {
- return {
- props: {
- menu: menusApi.getMenu('nx-api', 'nx-api'),
- pkg,
- },
- };
-}
diff --git a/nx-dev/nx-dev/pages/nx-api/rspack/generators/init.tsx b/nx-dev/nx-dev/pages/nx-api/rspack/generators/init.tsx
deleted file mode 100644
index 3b29972ac104e..0000000000000
--- a/nx-dev/nx-dev/pages/nx-api/rspack/generators/init.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { PackageSchemaViewer } from '@nx/nx-dev/feature-package-schema-viewer';
-import { getPackagesSections } from '@nx/nx-dev/data-access-menu';
-import { sortCorePackagesFirst } from '@nx/nx-dev/data-access-packages';
-import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
-import {
- ProcessedPackageMetadata,
- SchemaMetadata,
-} from '@nx/nx-dev/models-package';
-import { DocumentationHeader, SidebarContainer } from '@nx/nx-dev/ui-common';
-import { menusApi } from '../../../../lib/menus.api';
-import { useNavToggle } from '../../../../lib/navigation-toggle.effect';
-import { schema } from '../../../../lib/rspack/schema/generators/init';
-import { pkg } from '../../../../lib/rspack/pkg';
-import { ScrollableContent } from '@nx/ui-scrollable-content';
-
-export default function InitGenerator({
- menu,
- pkg,
- schema,
-}: {
- menu: MenuItem[];
- pkg: ProcessedPackageMetadata;
- schema: SchemaMetadata;
-}): JSX.Element {
- const { toggleNav, navIsOpen } = useNavToggle();
-
- const vm: {
- menu: Menu;
- package: ProcessedPackageMetadata;
- schema: SchemaMetadata;
- } = {
- menu: {
- sections: sortCorePackagesFirst(
- getPackagesSections(menu),
- 'id'
- ),
- },
- package: pkg,
- schema: schema,
- };
-
- /**
- * Show either the docviewer or the package view depending on:
- * - docviewer: it is a documentation document
- * - packageviewer: it is package generated documentation
- */
-
- return (
-
- );
-}
-
-export async function getStaticProps() {
- return {
- props: {
- pkg,
- schema,
- menu: menusApi.getMenu('nx-api', 'nx-api'),
- },
- };
-}
diff --git a/nx-dev/nx-dev/pages/nx-api/rspack/index.tsx b/nx-dev/nx-dev/pages/nx-api/rspack/index.tsx
deleted file mode 100644
index 1610634f02ebb..0000000000000
--- a/nx-dev/nx-dev/pages/nx-api/rspack/index.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { PackageSchemaList } from '@nx/nx-dev/feature-package-schema-viewer';
-import { getPackagesSections } from '@nx/nx-dev/data-access-menu';
-import { sortCorePackagesFirst } from '@nx/nx-dev/data-access-packages';
-import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
-import { ProcessedPackageMetadata } from '@nx/nx-dev/models-package';
-import { DocumentationHeader, SidebarContainer } from '@nx/nx-dev/ui-common';
-import { menusApi } from '../../../lib/menus.api';
-import { useNavToggle } from '../../../lib/navigation-toggle.effect';
-import { content } from '../../../lib/rspack/content/overview';
-import { pkg } from '../../../lib/rspack/pkg';
-import { ScrollableContent } from '@nx/ui-scrollable-content';
-
-export default function RspackIndex({
- overview,
- menu,
- pkg,
-}: {
- menu: MenuItem[];
- overview: string;
- pkg: ProcessedPackageMetadata;
-}): JSX.Element {
- const { toggleNav, navIsOpen } = useNavToggle();
-
- const vm: { menu: Menu; package: ProcessedPackageMetadata } = {
- menu: {
- sections: sortCorePackagesFirst(
- getPackagesSections(menu),
- 'id'
- ),
- },
- package: pkg,
- };
-
- /**
- * Show either the docviewer or the package view depending on:
- * - docviewer: it is a documentation document
- * - packageviewer: it is package generated documentation
- */
-
- return (
-
- );
-}
-
-export async function getStaticProps() {
- return {
- props: {
- menu: menusApi.getMenu('nx-api', 'nx-api'),
- overview: content,
- pkg,
- },
- };
-}
diff --git a/package.json b/package.json
index d81c5de7970d2..2ab47bca463fc 100644
--- a/package.json
+++ b/package.json
@@ -100,6 +100,10 @@
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-url": "^8.0.2",
+ "@rspack/core": "1.0.5",
+ "@rspack/dev-server": "1.0.5",
+ "@rspack/plugin-minify": "^0.7.5",
+ "@rspack/plugin-react-refresh": "^1.0.0",
"@schematics/angular": "~18.2.0",
"@storybook/addon-essentials": "^8.2.8",
"@storybook/addon-interactions": "^8.2.8",
diff --git a/packages-legacy/rspack/README.md b/packages-legacy/rspack/README.md
new file mode 100644
index 0000000000000..12f09ec1da465
--- /dev/null
+++ b/packages-legacy/rspack/README.md
@@ -0,0 +1,11 @@
+## @nrwl/rspack has been deprecated!
+
+@nrwl/rspack has been deprecated in favor of [@nx/rspack](https://www.npmjs.com/package/@nx/rspack). Please use that instead.
+
+@nrwl/rspack will no longer be published in Nx v17.
+
+
+
+# Nx: Smart, Fast and Extensible Build System
+
+Nx is a next generation build system with first class monorepo support and powerful integrations.
diff --git a/packages-legacy/rspack/executors.json b/packages-legacy/rspack/executors.json
new file mode 100644
index 0000000000000..292b254c513d0
--- /dev/null
+++ b/packages-legacy/rspack/executors.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "executors": {
+ "rspack": {
+ "implementation": "@nx/rspack/src/executors/rspack/rspack.impl",
+ "schema": "@nx/rspack/src/executors/rspack/schema.json",
+ "description": "rspack executor"
+ },
+ "dev-server": {
+ "implementation": "@nx/rspack/src/executors/dev-server/dev-server.impl",
+ "schema": "@nx/rspack/src/executors/dev-server/schema.json",
+ "description": "dev-server executor"
+ }
+ }
+}
diff --git a/packages-legacy/rspack/generators.json b/packages-legacy/rspack/generators.json
new file mode 100644
index 0000000000000..dc70c4a6afd02
--- /dev/null
+++ b/packages-legacy/rspack/generators.json
@@ -0,0 +1,4 @@
+{
+ "extends": ["@nx/rspack"],
+ "schematics": {}
+}
diff --git a/packages-legacy/rspack/index.ts b/packages-legacy/rspack/index.ts
new file mode 100644
index 0000000000000..64c76247e0e10
--- /dev/null
+++ b/packages-legacy/rspack/index.ts
@@ -0,0 +1 @@
+export * from '@nx/rspack';
diff --git a/packages-legacy/rspack/package.json b/packages-legacy/rspack/package.json
new file mode 100644
index 0000000000000..f5e2066f960db
--- /dev/null
+++ b/packages-legacy/rspack/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@nrwl/rspack",
+ "version": "0.0.1",
+ "type": "commonjs",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/nrwl/nx-labs.git",
+ "directory": "packages-legacy/rspack"
+ },
+ "keywords": [
+ "Monorepo",
+ "Next",
+ "Vercel"
+ ],
+ "author": "Jack Hsu",
+ "license": "MIT",
+ "homepage": "https://nx.dev",
+ "main": "src/index.js",
+ "generators": "./generators.json",
+ "executors": "./executors.json",
+ "dependencies": {
+ "@nx/rspack": "file:../../packages/rspack"
+ },
+ "nx-migrations": {
+ "migrations": "@nx/rspack/migrations.json"
+ }
+}
diff --git a/packages-legacy/rspack/project.json b/packages-legacy/rspack/project.json
new file mode 100644
index 0000000000000..9c4f8ea14b46f
--- /dev/null
+++ b/packages-legacy/rspack/project.json
@@ -0,0 +1,38 @@
+{
+ "name": "rspack-legacy",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "packages-legacy/rspack",
+ "projectType": "library",
+ "targets": {
+ "build": {
+ "outputs": ["{workspaceRoot}/build/packages/{projectName}/README.md"],
+ "command": "node ./scripts/copy-readme.js rspack-legacy"
+ },
+ "build-base": {
+ "executor": "@nrwl/js:tsc",
+ "dependsOn": ["^build"],
+ "options": {
+ "main": "packages-legacy/rspack/index.ts",
+ "tsConfig": "packages-legacy/rspack/tsconfig.json",
+ "outputPath": "build/packages/rspack-legacy",
+ "updateBuildableProjectDepsInPackageJson": false,
+ "assets": [
+ "packages-legacy/rspack/*.md",
+ {
+ "input": "packages-legacy/rspack",
+ "glob": "**/*.json",
+ "ignore": ["**/tsconfig*.json", "project.json"],
+ "output": "/"
+ },
+ {
+ "input": "packages-legacy/rspack",
+ "glob": "**/*.d.ts",
+ "output": "/"
+ },
+ "LICENSE"
+ ]
+ }
+ }
+ },
+ "tags": []
+}
diff --git a/packages-legacy/rspack/tsconfig.json b/packages-legacy/rspack/tsconfig.json
new file mode 100644
index 0000000000000..9cf8a29c54160
--- /dev/null
+++ b/packages-legacy/rspack/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "commonjs",
+ "declaration": true
+ },
+ "include": ["**/*.ts"],
+ "files": ["index.ts"]
+}
diff --git a/packages/nx/package.json b/packages/nx/package.json
index 4f532ccfe6685..4b0d2199718d0 100644
--- a/packages/nx/package.json
+++ b/packages/nx/package.json
@@ -142,6 +142,8 @@
"@nrwl/rollup",
"@nx/remix",
"@nrwl/remix",
+ "@nx/rspack",
+ "@nrwl/rspack",
"@nx/storybook",
"@nrwl/storybook",
"@nrwl/tao",
diff --git a/packages/nx/src/utils/plugins/core-plugins.ts b/packages/nx/src/utils/plugins/core-plugins.ts
index 64da8777b8626..aaf628a9aec10 100644
--- a/packages/nx/src/utils/plugins/core-plugins.ts
+++ b/packages/nx/src/utils/plugins/core-plugins.ts
@@ -85,6 +85,10 @@ export const CORE_PLUGINS: CorePlugin[] = [
name: '@nx/rollup',
capabilities: 'executors,generators',
},
+ {
+ name: '@nx/rspack',
+ capabilities: 'executors,generators',
+ },
{
name: '@nx/storybook',
capabilities: 'executors,generators',
diff --git a/packages/rspack/.eslintrc.json b/packages/rspack/.eslintrc.json
new file mode 100644
index 0000000000000..4cf7f347f6a85
--- /dev/null
+++ b/packages/rspack/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.js", "*.jsx"],
+ "rules": {}
+ },
+ {
+ "files": ["./package.json", "./generators.json", "./executors.json"],
+ "parser": "jsonc-eslint-parser",
+ "rules": {
+ "@nx/nx-plugin-checks": "error"
+ }
+ }
+ ]
+}
diff --git a/packages/rspack/README.md b/packages/rspack/README.md
new file mode 100644
index 0000000000000..123c6c283384c
--- /dev/null
+++ b/packages/rspack/README.md
@@ -0,0 +1,67 @@
+
+
+
+
+{{links}}
+
+
+
+# Nx: Smart Monorepos · Fast CI
+
+Nx is a build system, optimized for monorepos, with plugins for popular frameworks and tools and advanced CI capabilities including caching and distribution.
+
+This package is a [Rspack plugin for Nx](https://nx.dev/nx-api/rspack).
+
+{{content}}
+
+
+
+
+
+# Nx: Smart, Fast and Extensible Build System
+
+Nx is a next generation build system with first class monorepo support and powerful integrations.
+
+This package is a Rspack plugin for Nx.
+
+## Getting Started
+
+Use `--preset=@nx/rspack` when creating new workspace.
+
+e.g.
+
+```bash
+npx create-nx-workspace@latest rspack-demo --preset=@nx/rspack
+```
+
+Now, you can go into the `rspack-demo` folder and start development.
+
+```bash
+cd rspack-demo
+npm start
+```
+
+You can also run lint, test, and e2e scripts for the project.
+
+```bash
+npm run lint
+npm run test
+npm run e2e
+```
+
+## Existing workspaces
+
+You can add Rspack to any existing Nx workspace.
+
+First, install the plugin:
+
+```bash
+npm install --save-dev @nx/rspack
+```
+
+Then, r
+
+**Note:** You must restart the server if you make any changes to your library.
diff --git a/packages/rspack/executors.json b/packages/rspack/executors.json
new file mode 100644
index 0000000000000..df7d79d1b8c13
--- /dev/null
+++ b/packages/rspack/executors.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "executors": {
+ "rspack": {
+ "implementation": "./src/executors/rspack/rspack.impl",
+ "schema": "./src/executors/rspack/schema.json",
+ "description": "Run Rspack via an executor for a project."
+ },
+ "dev-server": {
+ "implementation": "./src/executors/dev-server/dev-server.impl",
+ "schema": "./src/executors/dev-server/schema.json",
+ "description": "Run @rspack/dev-server to serve a project."
+ },
+ "ssr-dev-server": {
+ "implementation": "./src/executors/ssr-dev-server/ssr-dev-server.impl",
+ "schema": "./src/executors/ssr-dev-server/schema.json",
+ "description": "Serve a SSR application."
+ },
+ "module-federation-dev-server": {
+ "implementation": "./src/executors/module-federation-dev-server/module-federation-dev-server.impl",
+ "schema": "./src/executors/module-federation-dev-server/schema.json",
+ "description": "Serve a host or remote application."
+ },
+ "module-federation-ssr-dev-server": {
+ "implementation": "./src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl",
+ "schema": "./src/executors/module-federation-ssr-dev-server/schema.json",
+ "description": "Serve a host application along with it's known remotes."
+ },
+ "module-federation-static-server": {
+ "implementation": "./src/executors/module-federation-static-server/module-federation-static-server.impl",
+ "schema": "./src/executors/module-federation-static-server/schema.json",
+ "description": "Serve a host and its remotes statically."
+ }
+ }
+}
diff --git a/packages/rspack/generators.json b/packages/rspack/generators.json
new file mode 100644
index 0000000000000..1d2e2968c4a84
--- /dev/null
+++ b/packages/rspack/generators.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "name": "rspack",
+ "version": "0.0.1",
+ "generators": {
+ "configuration": {
+ "factory": "./src/generators/configuration/configuration",
+ "schema": "./src/generators/configuration/schema.json",
+ "description": "Rspack configuration generator."
+ },
+ "init": {
+ "factory": "./src/generators/init/init",
+ "schema": "./src/generators/init/schema.json",
+ "description": "Rspack init generator.",
+ "hidden": true
+ },
+ "preset": {
+ "factory": "./src/generators/preset/preset",
+ "schema": "./src/generators/preset/schema.json",
+ "description": "React preset generator.",
+ "hidden": true
+ },
+ "application": {
+ "factory": "./src/generators/application/application",
+ "schema": "./src/generators/application/schema.json",
+ "aliases": ["app"],
+ "x-type": "application",
+ "description": "React application generator."
+ }
+ }
+}
diff --git a/packages/rspack/jest.config.ts b/packages/rspack/jest.config.ts
new file mode 100644
index 0000000000000..1223e1f4addd5
--- /dev/null
+++ b/packages/rspack/jest.config.ts
@@ -0,0 +1,16 @@
+/* eslint-disable */
+export default {
+ displayName: 'rspack',
+ preset: '../../jest.preset.js',
+ globals: {},
+ transform: {
+ '^.+\\.[tj]s$': [
+ 'ts-jest',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ },
+ ],
+ },
+ moduleFileExtensions: ['ts', 'js', 'html'],
+ coverageDirectory: '../../coverage/packages/rspack',
+};
diff --git a/packages/rspack/migrations.json b/packages/rspack/migrations.json
new file mode 100644
index 0000000000000..88f20a517d0b2
--- /dev/null
+++ b/packages/rspack/migrations.json
@@ -0,0 +1,98 @@
+{
+ "generators": {
+ "update-16-0-0-add-nx-packages": {
+ "cli": "nx",
+ "version": "16.0.0-beta.1",
+ "description": "Replace @nrwl/rspack with @nx/rspack",
+ "implementation": "./src/migrations/update-16-0-0-add-nx-packages/update-16-0-0-add-nx-packages"
+ }
+ },
+ "packageJsonUpdates": {
+ "16.1.3": {
+ "version": "16.1.3-beta.0",
+ "packages": {
+ "@rspack/core": {
+ "version": "~0.1.12",
+ "alwaysAddToPackageJson": false
+ },
+ "@rspack/dev-server": {
+ "version": "~0.1.12",
+ "alwaysAddToPackageJson": false
+ },
+ "@rspack/plugin-minify": {
+ "version": "~0.1.12",
+ "alwaysAddToPackageJson": false
+ }
+ }
+ },
+ "18.1.0": {
+ "version": "18.1.0-beta.0",
+ "packages": {
+ "@rspack/core": {
+ "version": "~0.5.6",
+ "alwaysAddToPackageJson": false
+ },
+ "@rspack/dev-server": {
+ "version": "~0.5.6",
+ "alwaysAddToPackageJson": false
+ },
+ "@rspack/plugin-minify": {
+ "version": "~0.5.6",
+ "alwaysAddToPackageJson": false
+ }
+ }
+ },
+ "18.1.3": {
+ "version": "18.1.3",
+ "packages": {
+ "@rspack/core": {
+ "version": "^0.6.1",
+ "alwaysAddToPackageJson": false
+ },
+ "@rspack/dev-server": {
+ "version": "^0.6.1",
+ "alwaysAddToPackageJson": false
+ },
+ "@rspack/plugin-minify": {
+ "version": "^0.6.1",
+ "alwaysAddToPackageJson": false
+ }
+ }
+ },
+ "19.3.0": {
+ "version": "19.3.0-beta.0",
+ "packages": {
+ "@rspack/core": {
+ "version": "^0.7.5",
+ "alwaysAddToPackageJson": false
+ },
+ "@rspack/dev-server": {
+ "version": "^0.7.5",
+ "alwaysAddToPackageJson": false
+ },
+ "@rspack/plugin-minify": {
+ "version": "^0.7.5",
+ "alwaysAddToPackageJson": false
+ }
+ }
+ },
+ "19.7.0": {
+ "version": "19.7.0-beta.1",
+ "packages": {
+ "@rspack/core": {
+ "version": "^1.0.0",
+ "alwaysAddToPackageJson": false
+ },
+ "@rspack/dev-server": {
+ "version": "^1.0.0",
+ "alwaysAddToPackageJson": false
+ },
+ "@rspack/plugin-react-refresh": {
+ "version": "^1.0.0",
+ "alwaysAddToPackageJson": false
+ }
+ }
+ }
+ },
+ "version": "0.1"
+}
diff --git a/packages/rspack/module-federation.ts b/packages/rspack/module-federation.ts
new file mode 100644
index 0000000000000..a4cf0dd1a4364
--- /dev/null
+++ b/packages/rspack/module-federation.ts
@@ -0,0 +1 @@
+export * from './src/utils/module-federation/public-api';
diff --git a/packages/rspack/package.json b/packages/rspack/package.json
new file mode 100644
index 0000000000000..48d97a6d6538a
--- /dev/null
+++ b/packages/rspack/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@nx/rspack",
+ "description": "The Nx Plugin for Rspack contains executors and generators that support building applications using Rspack.",
+ "version": "0.0.1",
+ "type": "commonjs",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/nrwl/nx.git",
+ "directory": "packages/rspack"
+ },
+ "bugs": {
+ "url": "https://github.com/nrwl/nx/issues"
+ },
+ "keywords": [
+ "Monorepo",
+ "Rspack",
+ "Bundling",
+ "Module Federation"
+ ],
+ "author": "Jack Hsu",
+ "license": "MIT",
+ "homepage": "https://nx.dev",
+ "main": "src/index.js",
+ "generators": "./generators.json",
+ "executors": "./executors.json",
+ "dependencies": {
+ "@nx/js": "file:../js",
+ "@nx/devkit": "file:../devkit",
+ "@nx/eslint": "file:../eslint",
+ "@phenomnomnominal/tsquery": "~5.0.1",
+ "less-loader": "11.1.0",
+ "license-webpack-plugin": "^4.0.2",
+ "sass-loader": "^12.2.0",
+ "stylus-loader": "^7.1.0",
+ "postcss-loader": "^8.1.1",
+ "@rspack/core": "^1.0.4",
+ "@rspack/plugin-react-refresh": "^1.0.0",
+ "@rspack/plugin-minify": "^0.7.5",
+ "chalk": "~4.1.0"
+ },
+ "peerDependencies": {
+ "@module-federation/enhanced": "~0.6.0",
+ "@module-federation/node": "~2.5.10"
+ },
+ "nx-migrations": {
+ "migrations": "./migrations.json"
+ }
+}
diff --git a/packages/rspack/plugin.ts b/packages/rspack/plugin.ts
new file mode 100644
index 0000000000000..8d3c1a001313d
--- /dev/null
+++ b/packages/rspack/plugin.ts
@@ -0,0 +1,2 @@
+export { createDependencies, createNodesV2 } from './src/plugins/plugin';
+export type { RspackPluginOptions } from './src/plugins/plugin';
diff --git a/packages/rspack/project.json b/packages/rspack/project.json
new file mode 100644
index 0000000000000..54e1479666f75
--- /dev/null
+++ b/packages/rspack/project.json
@@ -0,0 +1,50 @@
+{
+ "name": "rspack",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "packages/rspack/src",
+ "projectType": "library",
+ "targets": {
+ "add-extra-dependencies": {
+ "outputs": ["{workspaceRoot}/build/packages/rspack"],
+ "command": "node ./scripts/add-dependency-to-build.js rspack @nrwl/rspack"
+ },
+ "build": {
+ "executor": "nx:run-commands",
+ "outputs": ["{workspaceRoot}/build/packages/rspack"],
+ "options": {
+ "command": "node ./scripts/copy-readme.js rspack"
+ }
+ },
+ "build-base": {
+ "dependsOn": ["^build-base"],
+ "executor": "@nx/js:tsc",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "build/packages/rspack",
+ "main": "packages/rspack/src/index.ts",
+ "tsConfig": "packages/rspack/tsconfig.lib.json",
+ "assets": [
+ "packages/rspack/*.md",
+ {
+ "input": "./packages/rspack/src",
+ "glob": "**/!(*.ts)",
+ "output": "./src"
+ },
+ {
+ "input": "./packages/rspack/src",
+ "glob": "**/*.d.ts",
+ "output": "./src"
+ },
+ {
+ "input": "./packages/rspack",
+ "glob": "**.json",
+ "output": ".",
+ "ignore": ["**/tsconfig*.json", "project.json", ".eslintrc.json"]
+ },
+ "LICENSE"
+ ]
+ }
+ }
+ },
+ "tags": []
+}
diff --git a/packages/rspack/src/executors/dev-server/dev-server.impl.ts b/packages/rspack/src/executors/dev-server/dev-server.impl.ts
new file mode 100644
index 0000000000000..2f1f1925b0bca
--- /dev/null
+++ b/packages/rspack/src/executors/dev-server/dev-server.impl.ts
@@ -0,0 +1,80 @@
+import {
+ ExecutorContext,
+ logger,
+ parseTargetString,
+ readTargetOptions,
+} from '@nx/devkit';
+import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
+import { Configuration } from '@rspack/core';
+import { RspackDevServer } from '@rspack/dev-server';
+import { createCompiler, isMultiCompiler } from '../../utils/create-compiler';
+import { isMode } from '../../utils/mode-utils';
+import { getDevServerOptions } from './lib/get-dev-server-config';
+import { DevServerExecutorSchema } from './schema';
+
+type DevServer = Configuration['devServer'];
+export default async function* runExecutor(
+ options: DevServerExecutorSchema,
+ context: ExecutorContext
+): AsyncIterableIterator<{ success: boolean; baseUrl?: string }> {
+ process.env.NODE_ENV ??= options.mode ?? 'development';
+
+ if (isMode(process.env.NODE_ENV)) {
+ options.mode = process.env.NODE_ENV;
+ }
+
+ const buildTarget = parseTargetString(
+ options.buildTarget,
+ context.projectGraph
+ );
+
+ const buildOptions = readTargetOptions(buildTarget, context);
+
+ let devServerConfig: DevServer = getDevServerOptions(
+ context.root,
+ options,
+ buildOptions
+ );
+
+ const compiler = await createCompiler(
+ { ...buildOptions, devServer: devServerConfig, mode: options.mode },
+ context
+ );
+
+ // Use the first one if it's MultiCompiler
+ // https://webpack.js.org/configuration/dev-server/#root:~:text=Be%20aware%20that%20when%20exporting%20multiple%20configurations%20only%20the%20devServer%20options%20for%20the%20first%20configuration%20will%20be%20taken%20into%20account%20and%20used%20for%20all%20the%20configurations%20in%20the%20array.
+ const firstCompiler = isMultiCompiler(compiler)
+ ? compiler.compilers[0]
+ : compiler;
+ devServerConfig = {
+ ...devServerConfig,
+ ...firstCompiler.options.devServer,
+ port: devServerConfig.port,
+ };
+
+ const baseUrl = `http://localhost:${options.port ?? 4200}`;
+
+ return yield* createAsyncIterable(({ next }) => {
+ const server = new RspackDevServer(
+ {
+ ...devServerConfig,
+ onListening: () => {
+ next({
+ success: true,
+ baseUrl,
+ });
+ },
+ },
+
+ compiler
+ );
+ server.compiler.hooks.done.tap('NX Rspack Dev Server', (stats) => {
+ if (stats.hasErrors()) {
+ logger.error(`NX Compilation failed. See above for more details.`);
+ } else {
+ logger.info(`NX Server ready at ${baseUrl}`);
+ }
+ });
+ server.start();
+ });
+}
diff --git a/packages/rspack/src/executors/dev-server/lib/get-dev-server-config.ts b/packages/rspack/src/executors/dev-server/lib/get-dev-server-config.ts
new file mode 100644
index 0000000000000..c7ff6a0d8f242
--- /dev/null
+++ b/packages/rspack/src/executors/dev-server/lib/get-dev-server-config.ts
@@ -0,0 +1,84 @@
+import { logger } from '@nx/devkit';
+import type { Configuration as RspackDevServerConfiguration } from '@rspack/dev-server';
+import { readFileSync } from 'fs';
+import * as path from 'path';
+import { RspackExecutorSchema } from '../../rspack/schema';
+import { DevServerExecutorSchema } from '../schema';
+import { buildServePath } from './serve-path';
+
+export function getDevServerOptions(
+ root: string,
+ serveOptions: DevServerExecutorSchema,
+ buildOptions: RspackExecutorSchema
+): RspackDevServerConfiguration {
+ const servePath = buildServePath(buildOptions);
+
+ let scriptsOptimization: boolean;
+ let stylesOptimization: boolean;
+ if (typeof buildOptions.optimization === 'boolean') {
+ scriptsOptimization = stylesOptimization = buildOptions.optimization;
+ } else if (buildOptions.optimization) {
+ scriptsOptimization = buildOptions.optimization.scripts;
+ stylesOptimization = buildOptions.optimization.styles;
+ } else {
+ scriptsOptimization = stylesOptimization = false;
+ }
+
+ const config: RspackDevServerConfiguration = {
+ host: serveOptions.host,
+ port: serveOptions.port,
+ headers: { 'Access-Control-Allow-Origin': '*' },
+ historyApiFallback: {
+ index:
+ buildOptions.index &&
+ `${servePath}${path.basename(buildOptions.index)}`,
+ disableDotRule: true,
+ htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
+ },
+ onListening(server) {
+ const isHttps =
+ server.options.https ||
+ (server.options.server as { type: string })?.type === 'https';
+ logger.info(
+ `NX Web Development Server is listening at ${
+ isHttps ? 'https' : 'http'
+ }://${server.options.host}:${server.options.port}${buildServePath(
+ buildOptions
+ )}`
+ );
+ },
+ open: false,
+ static: false,
+ compress: scriptsOptimization || stylesOptimization,
+ devMiddleware: {
+ publicPath: servePath,
+ stats: false,
+ },
+ client: {
+ webSocketURL: serveOptions.publicHost,
+ overlay: {
+ errors: !(scriptsOptimization || stylesOptimization),
+ warnings: false,
+ },
+ },
+ hot: true,
+ };
+
+ if (serveOptions.ssl) {
+ config.server = {
+ type: 'https',
+ };
+ if (serveOptions.sslKey && serveOptions.sslCert) {
+ config.server.options = getSslConfig(root, serveOptions);
+ }
+ }
+
+ return config;
+}
+
+function getSslConfig(root: string, options: DevServerExecutorSchema) {
+ return {
+ key: readFileSync(path.resolve(root, options.sslKey), 'utf-8'),
+ cert: readFileSync(path.resolve(root, options.sslCert), 'utf-8'),
+ };
+}
diff --git a/packages/rspack/src/executors/dev-server/lib/serve-path.ts b/packages/rspack/src/executors/dev-server/lib/serve-path.ts
new file mode 100644
index 0000000000000..15b1c0ea3f617
--- /dev/null
+++ b/packages/rspack/src/executors/dev-server/lib/serve-path.ts
@@ -0,0 +1,56 @@
+import type { RspackExecutorSchema } from '../../rspack/schema';
+
+export function buildServePath(browserOptions: RspackExecutorSchema) {
+ let servePath =
+ _findDefaultServePath(browserOptions.baseHref, browserOptions.deployUrl) ||
+ '/';
+ if (servePath.endsWith('/')) {
+ servePath = servePath.slice(0, -1);
+ }
+ if (!servePath.startsWith('/')) {
+ servePath = `/${servePath}`;
+ }
+
+ return servePath;
+}
+
+export function _findDefaultServePath(
+ baseHref?: string,
+ deployUrl?: string
+): string | null {
+ if (!baseHref && !deployUrl) {
+ return '';
+ }
+
+ if (
+ /^(\w+:)?\/\//.test(baseHref || '') ||
+ /^(\w+:)?\/\//.test(deployUrl || '')
+ ) {
+ // If baseHref or deployUrl is absolute, unsupported by nx serve
+ return null;
+ }
+
+ // normalize baseHref
+ // for nx serve the starting base is always `/` so a relative
+ // and root relative value are identical
+ const baseHrefParts = (baseHref || '')
+ .split('/')
+ .filter((part) => part !== '');
+ if (baseHref && !baseHref.endsWith('/')) {
+ baseHrefParts.pop();
+ }
+ const normalizedBaseHref =
+ baseHrefParts.length === 0 ? '/' : `/${baseHrefParts.join('/')}/`;
+
+ if (deployUrl && deployUrl[0] === '/') {
+ if (baseHref && baseHref[0] === '/' && normalizedBaseHref !== deployUrl) {
+ // If baseHref and deployUrl are root relative and not equivalent, unsupported by nx serve
+ return null;
+ }
+
+ return deployUrl;
+ }
+
+ // Join together baseHref and deployUrl
+ return `${normalizedBaseHref}${deployUrl || ''}`;
+}
diff --git a/packages/rspack/src/executors/dev-server/schema.d.ts b/packages/rspack/src/executors/dev-server/schema.d.ts
new file mode 100644
index 0000000000000..730b1b66b5aca
--- /dev/null
+++ b/packages/rspack/src/executors/dev-server/schema.d.ts
@@ -0,0 +1,12 @@
+import type { Mode } from '@rspack/core';
+
+export interface DevServerExecutorSchema {
+ buildTarget: string;
+ mode?: Mode;
+ host?: string;
+ port?: number;
+ ssl?: boolean;
+ sslKey?: string;
+ sslCert?: string;
+ publicHost?: string;
+}
diff --git a/packages/rspack/src/executors/dev-server/schema.json b/packages/rspack/src/executors/dev-server/schema.json
new file mode 100644
index 0000000000000..f636780ae0bf4
--- /dev/null
+++ b/packages/rspack/src/executors/dev-server/schema.json
@@ -0,0 +1,45 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "version": 2,
+ "title": "Rspack dev-server executor",
+ "description": "Run @rspack/dev-server to serve a project.",
+ "type": "object",
+ "properties": {
+ "buildTarget": {
+ "type": "string",
+ "description": "The build target for rspack."
+ },
+ "port": {
+ "type": "number",
+ "description": "The port to for the dev-server to listen on."
+ },
+ "mode": {
+ "type": "string",
+ "description": "Mode to run the server in.",
+ "enum": ["development", "production", "none"]
+ },
+ "host": {
+ "type": "string",
+ "description": "Host to listen on.",
+ "default": "localhost"
+ },
+ "ssl": {
+ "type": "boolean",
+ "description": "Serve using `HTTPS`.",
+ "default": false
+ },
+ "sslKey": {
+ "type": "string",
+ "description": "SSL key to use for serving `HTTPS`."
+ },
+ "sslCert": {
+ "type": "string",
+ "description": "SSL certificate to use for serving `HTTPS`."
+ },
+ "publicHost": {
+ "type": "string",
+ "description": "Public URL where the application will be served."
+ }
+ },
+ "required": ["buildTarget"]
+}
diff --git a/packages/rspack/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts b/packages/rspack/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts
new file mode 100644
index 0000000000000..2c77ff2bb940f
--- /dev/null
+++ b/packages/rspack/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts
@@ -0,0 +1,317 @@
+import {
+ ExecutorContext,
+ logger,
+ parseTargetString,
+ readTargetOptions,
+ runExecutor,
+ workspaceRoot,
+} from '@nx/devkit';
+import {
+ combineAsyncIterables,
+ createAsyncIterable,
+} from '@nx/devkit/src/utils/async-iterable';
+import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
+import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
+import { cpSync, existsSync } from 'fs';
+import { extname, join } from 'path';
+import {
+ getModuleFederationConfig,
+ getRemotes,
+} from '../../utils/module-federation';
+import { buildStaticRemotes } from '../../utils/module-federation/build-static.remotes';
+import {
+ parseStaticRemotesConfig,
+ type StaticRemotesConfig,
+} from '../../utils/module-federation/parse-static-remotes-config';
+import { startRemoteProxies } from '../../utils/module-federation/start-remote-proxies';
+import devServerExecutor from '../dev-server/dev-server.impl';
+import { ModuleFederationDevServerOptions } from './schema';
+
+function getBuildOptions(buildTarget: string, context: ExecutorContext) {
+ const target = parseTargetString(buildTarget, context);
+
+ const buildOptions = readTargetOptions(target, context);
+
+ return {
+ ...buildOptions,
+ };
+}
+
+function startStaticRemotesFileServer(
+ staticRemotesConfig: StaticRemotesConfig,
+ context: ExecutorContext,
+ options: ModuleFederationDevServerOptions
+) {
+ if (
+ !staticRemotesConfig.remotes ||
+ staticRemotesConfig.remotes.length === 0
+ ) {
+ return;
+ }
+ let shouldMoveToCommonLocation = false;
+ let commonOutputDirectory: string;
+ for (const app of staticRemotesConfig.remotes) {
+ const remoteBasePath = staticRemotesConfig.config[app].basePath;
+ if (!commonOutputDirectory) {
+ commonOutputDirectory = remoteBasePath;
+ } else if (commonOutputDirectory !== remoteBasePath) {
+ shouldMoveToCommonLocation = true;
+ break;
+ }
+ }
+
+ if (shouldMoveToCommonLocation) {
+ commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes');
+ for (const app of staticRemotesConfig.remotes) {
+ const remoteConfig = staticRemotesConfig.config[app];
+ cpSync(
+ remoteConfig.outputPath,
+ join(commonOutputDirectory, remoteConfig.urlSegment),
+ {
+ force: true,
+ recursive: true,
+ }
+ );
+ }
+ }
+
+ const staticRemotesIter = fileServerExecutor(
+ {
+ cors: true,
+ watch: false,
+ staticFilePath: commonOutputDirectory,
+ parallel: false,
+ spa: false,
+ withDeps: false,
+ host: options.host,
+ port: options.staticRemotesPort,
+ ssl: options.ssl,
+ sslCert: options.sslCert,
+ sslKey: options.sslKey,
+ cacheSeconds: -1,
+ },
+ context
+ );
+
+ return staticRemotesIter;
+}
+
+async function startRemotes(
+ remotes: string[],
+ context: ExecutorContext,
+ options: ModuleFederationDevServerOptions,
+ target: 'serve' | 'serve-static' = 'serve'
+) {
+ const remoteIters: AsyncIterable<{ success: boolean }>[] = [];
+
+ for (const app of remotes) {
+ const remoteProjectServeTarget =
+ context.projectGraph.nodes[app].data.targets[target];
+ const isUsingModuleFederationDevServerExecutor =
+ remoteProjectServeTarget.executor.includes(
+ 'module-federation-dev-server'
+ );
+
+ const configurationOverride = options.devRemotes?.find(
+ (
+ r
+ ): r is {
+ remoteName: string;
+ configuration: string;
+ } => typeof r !== 'string' && r.remoteName === app
+ )?.configuration;
+
+ const defaultOverrides = {
+ ...(options.host ? { host: options.host } : {}),
+ ...(options.ssl ? { ssl: options.ssl } : {}),
+ ...(options.sslCert ? { sslCert: options.sslCert } : {}),
+ ...(options.sslKey ? { sslKey: options.sslKey } : {}),
+ };
+ const overrides =
+ target === 'serve'
+ ? {
+ watch: true,
+ ...(isUsingModuleFederationDevServerExecutor
+ ? { isInitialHost: false }
+ : {}),
+ ...defaultOverrides,
+ }
+ : { ...defaultOverrides };
+
+ remoteIters.push(
+ await runExecutor(
+ {
+ project: app,
+ target,
+ configuration: configurationOverride ?? context.configurationName,
+ },
+ overrides,
+ context
+ )
+ );
+ }
+ return remoteIters;
+}
+
+export default async function* moduleFederationDevServer(
+ options: ModuleFederationDevServerOptions,
+ context: ExecutorContext
+): AsyncIterableIterator<{ success: boolean; baseUrl?: string }> {
+ // Force Node to resolve to look for the nx binary that is inside node_modules
+ const nxBin = require.resolve('nx/bin/nx');
+ const currIter = options.static
+ ? fileServerExecutor(
+ {
+ ...options,
+ parallel: false,
+ withDeps: false,
+ spa: false,
+ cors: true,
+ cacheSeconds: -1,
+ },
+ context
+ )
+ : devServerExecutor(options, context);
+
+ const p = context.projectsConfigurations.projects[context.projectName];
+ const buildOptions = getBuildOptions(options.buildTarget, context);
+
+ let pathToManifestFile = join(
+ context.root,
+ p.sourceRoot,
+ 'assets/module-federation.manifest.json'
+ );
+ if (options.pathToManifestFile) {
+ const userPathToManifestFile = join(
+ context.root,
+ options.pathToManifestFile
+ );
+ if (!existsSync(userPathToManifestFile)) {
+ throw new Error(
+ `The provided Module Federation manifest file path does not exist. Please check the file exists at "${userPathToManifestFile}".`
+ );
+ } else if (extname(options.pathToManifestFile) !== '.json') {
+ throw new Error(
+ `The Module Federation manifest file must be a JSON. Please ensure the file at ${userPathToManifestFile} is a JSON.`
+ );
+ }
+
+ pathToManifestFile = userPathToManifestFile;
+ }
+
+ if (!options.isInitialHost) {
+ return yield* currIter;
+ }
+
+ const moduleFederationConfig = getModuleFederationConfig(
+ buildOptions.tsConfig,
+ context.root,
+ p.root,
+ 'react'
+ );
+
+ const remoteNames = options.devRemotes?.map((r) =>
+ typeof r === 'string' ? r : r.remoteName
+ );
+
+ const remotes = getRemotes(
+ remoteNames,
+ options.skipRemotes,
+ moduleFederationConfig,
+ {
+ projectName: context.projectName,
+ projectGraph: context.projectGraph,
+ root: context.root,
+ },
+ pathToManifestFile
+ );
+ options.staticRemotesPort ??= remotes.staticRemotePort;
+
+ // Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin
+ process.env.NX_MF_DEV_REMOTES = JSON.stringify([
+ ...(remotes.devRemotes.map((r) =>
+ typeof r === 'string' ? r : r.remoteName
+ ) ?? []),
+ p.name,
+ ]);
+
+ const staticRemotesConfig = parseStaticRemotesConfig(
+ [...remotes.staticRemotes, ...remotes.dynamicRemotes],
+ context
+ );
+ const mappedLocationsOfStaticRemotes = await buildStaticRemotes(
+ staticRemotesConfig,
+ nxBin,
+ context,
+ options
+ );
+
+ const devRemoteIters = await startRemotes(
+ remotes.devRemotes,
+ context,
+ options,
+ 'serve'
+ );
+
+ const staticRemotesIter = startStaticRemotesFileServer(
+ staticRemotesConfig,
+ context,
+ options
+ );
+
+ startRemoteProxies(
+ staticRemotesConfig,
+ mappedLocationsOfStaticRemotes,
+ options.ssl
+ ? {
+ pathToCert: join(workspaceRoot, options.sslCert),
+ pathToKey: join(workspaceRoot, options.sslKey),
+ }
+ : undefined
+ );
+
+ return yield* combineAsyncIterables(
+ currIter,
+ ...devRemoteIters,
+ ...(staticRemotesIter ? [staticRemotesIter] : []),
+ createAsyncIterable<{ success: true; baseUrl: string }>(
+ async ({ next, done }) => {
+ if (!options.isInitialHost) {
+ done();
+ return;
+ }
+ if (remotes.remotePorts.length === 0) {
+ done();
+ return;
+ }
+ try {
+ const host = options.host ?? 'localhost';
+ const baseUrl = `http${options.ssl ? 's' : ''}://${host}:${
+ options.port
+ }`;
+ const portsToWaitFor = staticRemotesIter
+ ? [options.staticRemotesPort, ...remotes.remotePorts]
+ : [...remotes.remotePorts];
+ await Promise.all(
+ portsToWaitFor.map((port) =>
+ waitForPortOpen(port, {
+ retries: 480,
+ retryDelay: 2500,
+ host: host,
+ })
+ )
+ );
+
+ logger.info(`NX All remotes started, server ready at ${baseUrl}`);
+ next({ success: true, baseUrl: baseUrl });
+ } catch (err) {
+ throw new Error(
+ `Failed to start remotes. Check above for any errors.`
+ );
+ } finally {
+ done();
+ }
+ }
+ )
+ );
+}
diff --git a/packages/rspack/src/executors/module-federation-dev-server/schema.d.ts b/packages/rspack/src/executors/module-federation-dev-server/schema.d.ts
new file mode 100644
index 0000000000000..61c4c0657eb06
--- /dev/null
+++ b/packages/rspack/src/executors/module-federation-dev-server/schema.d.ts
@@ -0,0 +1,18 @@
+import { DevServerExecutorSchema } from '../dev-server/schema';
+
+export type ModuleFederationDevServerOptions = DevServerExecutorSchema & {
+ // Module Federation Specific Options
+ devRemotes?: (
+ | string
+ | {
+ remoteName: string;
+ configuration: string;
+ }
+ )[];
+ skipRemotes?: string[];
+ static?: boolean;
+ isInitialHost?: boolean;
+ parallel?: number;
+ staticRemotesPort?: number;
+ pathToManifestFile?: string;
+};
diff --git a/packages/rspack/src/executors/module-federation-dev-server/schema.json b/packages/rspack/src/executors/module-federation-dev-server/schema.json
new file mode 100644
index 0000000000000..75b019a5e6654
--- /dev/null
+++ b/packages/rspack/src/executors/module-federation-dev-server/schema.json
@@ -0,0 +1,98 @@
+{
+ "version": 2,
+ "outputCapture": "direct-nodejs",
+ "title": "Rspack Module Federation Dev Server",
+ "description": "Serve a module federation application.",
+ "cli": "nx",
+ "type": "object",
+ "properties": {
+ "devRemotes": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "remoteName": {
+ "type": "string"
+ },
+ "configuration": {
+ "type": "string"
+ }
+ },
+ "required": ["remoteName"],
+ "additionalProperties": false
+ }
+ ]
+ },
+ "description": "List of remote applications to run in development mode (i.e. using serve target).",
+ "x-priority": "important"
+ },
+ "skipRemotes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List of remote applications to not automatically serve, either statically or in development mode. This will not remove the remotes from the `module-federation.config` file, and therefore the application may still try to fetch these remotes.\nThis option is useful if you have other means for serving the `remote` application(s).\n**NOTE:** Remotes that are not in the workspace will be skipped automatically.",
+ "x-priority": "important"
+ },
+ "buildTarget": {
+ "type": "string",
+ "description": "Target which builds the application.",
+ "x-priority": "important"
+ },
+ "port": {
+ "type": "number",
+ "description": "Port to listen on.",
+ "default": 4200,
+ "x-priority": "important"
+ },
+ "host": {
+ "type": "string",
+ "description": "Host to listen on.",
+ "default": "localhost"
+ },
+ "ssl": {
+ "type": "boolean",
+ "description": "Serve using `HTTPS`.",
+ "default": false
+ },
+ "sslKey": {
+ "type": "string",
+ "description": "SSL key to use for serving `HTTPS`."
+ },
+ "sslCert": {
+ "type": "string",
+ "description": "SSL certificate to use for serving `HTTPS`."
+ },
+ "publicHost": {
+ "type": "string",
+ "description": "Public URL where the application will be served."
+ },
+ "static": {
+ "type": "boolean",
+ "description": "Whether to use a static file server instead of the rspack-dev-server. This should be used for remote applications that are also host applications."
+ },
+ "isInitialHost": {
+ "type": "boolean",
+ "description": "Whether the host that is running this executor is the first in the project tree to do so.",
+ "default": true,
+ "x-priority": "internal"
+ },
+ "parallel": {
+ "type": "number",
+ "description": "Max number of parallel processes for building static remotes"
+ },
+ "staticRemotesPort": {
+ "type": "number",
+ "description": "The port at which to serve the file-server for the static remotes."
+ },
+ "pathToManifestFile": {
+ "type": "string",
+ "description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root."
+ }
+ }
+}
diff --git a/packages/rspack/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts b/packages/rspack/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts
new file mode 100644
index 0000000000000..5301a26a4f8d7
--- /dev/null
+++ b/packages/rspack/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts
@@ -0,0 +1,406 @@
+import {
+ ExecutorContext,
+ logger,
+ parseTargetString,
+ readTargetOptions,
+ runExecutor,
+ workspaceRoot,
+} from '@nx/devkit';
+import { extname, join } from 'path';
+import {
+ getModuleFederationConfig,
+ getRemotes,
+} from '../../utils/module-federation';
+import { RspackSsrDevServerOptions } from '../ssr-dev-server/schema';
+import ssrDevServerExecutor from '../ssr-dev-server/ssr-dev-server.impl';
+
+import {
+ combineAsyncIterables,
+ createAsyncIterable,
+} from '@nx/devkit/src/utils/async-iterable';
+import { fork } from 'child_process';
+import { cpSync, createWriteStream, existsSync } from 'fs';
+
+import {
+ parseStaticSsrRemotesConfig,
+ type StaticRemotesConfig,
+} from '../../utils/module-federation/parse-static-remotes-config';
+
+import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
+import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
+import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
+import { startSsrRemoteProxies } from '../../utils/module-federation/start-ssr-remote-proxies';
+
+type ModuleFederationSsrDevServerOptions = RspackSsrDevServerOptions & {
+ devRemotes?: (
+ | string
+ | {
+ remoteName: string;
+ configuration: string;
+ }
+ )[];
+
+ skipRemotes?: string[];
+ host: string;
+ pathToManifestFile?: string;
+ staticRemotesPort?: number;
+ parallel?: number;
+ ssl?: boolean;
+ sslKey?: string;
+ sslCert?: string;
+ isInitialHost?: boolean;
+};
+
+function normalizeOptions(
+ options: ModuleFederationSsrDevServerOptions
+): ModuleFederationSsrDevServerOptions {
+ return {
+ ...options,
+ ssl: options.ssl ?? false,
+ sslCert: options.sslCert ? join(workspaceRoot, options.sslCert) : undefined,
+ sslKey: options.sslKey ? join(workspaceRoot, options.sslKey) : undefined,
+ };
+}
+
+function getBuildOptions(buildTarget: string, context: ExecutorContext) {
+ const target = parseTargetString(buildTarget, context);
+
+ const buildOptions = readTargetOptions(target, context);
+
+ return {
+ ...buildOptions,
+ };
+}
+
+function startSsrStaticRemotesFileServer(
+ ssrStaticRemotesConfig: StaticRemotesConfig,
+ context: ExecutorContext,
+ options: ModuleFederationSsrDevServerOptions
+) {
+ if (ssrStaticRemotesConfig.remotes.length === 0) {
+ return;
+ }
+
+ // The directories are usually generated with /browser and /server suffixes so we need to copy them to a common directory
+ const commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes');
+ for (const app of ssrStaticRemotesConfig.remotes) {
+ const remoteConfig = ssrStaticRemotesConfig.config[app];
+
+ cpSync(
+ remoteConfig.outputPath,
+ join(commonOutputDirectory, remoteConfig.urlSegment),
+ {
+ force: true,
+ recursive: true,
+ }
+ );
+ }
+
+ const staticRemotesIter = fileServerExecutor(
+ {
+ cors: true,
+ watch: false,
+ staticFilePath: commonOutputDirectory,
+ parallel: false,
+ spa: false,
+ withDeps: false,
+ host: options.host,
+ port: options.staticRemotesPort,
+ ssl: options.ssl,
+ sslCert: options.sslCert,
+ sslKey: options.sslKey,
+ cacheSeconds: -1,
+ },
+ context
+ );
+
+ return staticRemotesIter;
+}
+
+async function startRemotes(
+ remotes: string[],
+ context: ExecutorContext,
+ options: ModuleFederationSsrDevServerOptions
+) {
+ const remoteIters: AsyncIterable<{ success: boolean }>[] = [];
+ const target = 'serve';
+ for (const app of remotes) {
+ const remoteProjectServeTarget =
+ context.projectGraph.nodes[app].data.targets[target];
+ const isUsingModuleFederationSsrDevServerExecutor =
+ remoteProjectServeTarget.executor.includes(
+ 'module-federation-ssr-dev-server'
+ );
+
+ const configurationOverride = options.devRemotes?.find(
+ (remote): remote is { remoteName: string; configuration: string } =>
+ typeof remote !== 'string' && remote.remoteName === app
+ )?.configuration;
+ {
+ const defaultOverrides = {
+ ...(options.host ? { host: options.host } : {}),
+ ...(options.ssl ? { ssl: options.ssl } : {}),
+ ...(options.sslCert ? { sslCert: options.sslCert } : {}),
+ ...(options.sslKey ? { sslKey: options.sslKey } : {}),
+ };
+
+ const overrides = {
+ watch: true,
+ ...defaultOverrides,
+ ...(isUsingModuleFederationSsrDevServerExecutor
+ ? { isInitialHost: false }
+ : {}),
+ };
+
+ remoteIters.push(
+ await runExecutor(
+ {
+ project: app,
+ target,
+ configuration: configurationOverride ?? context.configurationName,
+ },
+ overrides,
+ context
+ )
+ );
+ }
+ }
+ return remoteIters;
+}
+
+async function buildSsrStaticRemotes(
+ staticRemotesConfig: StaticRemotesConfig,
+ nxBin,
+ context: ExecutorContext,
+ options: ModuleFederationSsrDevServerOptions
+) {
+ if (!staticRemotesConfig.remotes.length) {
+ return;
+ }
+
+ logger.info(
+ `Nx is building ${staticRemotesConfig.remotes.length} static remotes...`
+ );
+ const mapLocationOfRemotes: Record = {};
+
+ for (const remoteApp of staticRemotesConfig.remotes) {
+ mapLocationOfRemotes[remoteApp] = `http${options.ssl ? 's' : ''}://${
+ options.host
+ }:${options.staticRemotesPort}/${
+ staticRemotesConfig.config[remoteApp].urlSegment
+ }`;
+ }
+
+ await new Promise((resolve) => {
+ const childProcess = fork(
+ nxBin,
+ [
+ 'run-many',
+ '--target=server',
+ '--projects',
+ staticRemotesConfig.remotes.join(','),
+ ...(context.configurationName
+ ? [`--configuration=${context.configurationName}`]
+ : []),
+ ...(options.parallel ? [`--parallel=${options.parallel}`] : []),
+ ],
+ {
+ cwd: context.root,
+ stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
+ }
+ );
+
+ // Add a listener to the child process to capture the build log
+ const remoteBuildLogFile = join(
+ workspaceDataDirectory,
+ // eslint-disable-next-line
+ `${new Date().toISOString().replace(/[:\.]/g, '_')}-build.log`
+ );
+
+ const remoteBuildLogStream = createWriteStream(remoteBuildLogFile);
+
+ childProcess.stdout.on('data', (data) => {
+ const ANSII_CODE_REGEX =
+ // eslint-disable-next-line no-control-regex
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
+ const stdoutString = data.toString().replace(ANSII_CODE_REGEX, '');
+ remoteBuildLogStream.write(stdoutString);
+
+ // in addition to writing into the stdout stream, also show error directly in console
+ // so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output.
+ if (stdoutString.includes('ERROR in')) {
+ logger.log(stdoutString);
+ }
+
+ if (stdoutString.includes('Successfully ran target server')) {
+ childProcess.stdout.removeAllListeners('data');
+ logger.info(
+ `Nx Built ${staticRemotesConfig.remotes.length} static remotes.`
+ );
+ resolve();
+ }
+ });
+
+ process.on('SIGTERM', () => childProcess.kill('SIGTERM'));
+ process.on('exit', () => childProcess.kill('SIGTERM'));
+ });
+ return mapLocationOfRemotes;
+}
+
+export default async function* moduleFederationSsrDevServer(
+ ssrDevServerOptions: ModuleFederationSsrDevServerOptions,
+ context: ExecutorContext
+) {
+ const options = normalizeOptions(ssrDevServerOptions);
+ // Force Node to resolve to look for the nx binary that is inside node_modules
+ const nxBin = require.resolve('nx/bin/nx');
+ const iter = ssrDevServerExecutor(options, context);
+ const projectConfig =
+ context.projectsConfigurations.projects[context.projectName];
+ const buildOptions = getBuildOptions(options.browserTarget, context);
+
+ let pathToManifestFile = join(
+ context.root,
+ projectConfig.sourceRoot,
+ 'assets/module-federation.manifest.json'
+ );
+
+ if (options.pathToManifestFile) {
+ const userPathToManifestFile = join(
+ context.root,
+ options.pathToManifestFile
+ );
+
+ if (!existsSync(userPathToManifestFile)) {
+ throw new Error(
+ `The provided Module Federation manifest file path does not exist. Please check the file exists at "${userPathToManifestFile}".`
+ );
+ } else if (extname(userPathToManifestFile) !== '.json') {
+ throw new Error(
+ `The Module Federation manifest file must be a JSON. Please ensure the file at ${userPathToManifestFile} is a JSON.`
+ );
+ }
+ pathToManifestFile = userPathToManifestFile;
+ }
+
+ if (!options.isInitialHost) {
+ return yield* iter;
+ }
+
+ const moduleFederationConfig = getModuleFederationConfig(
+ buildOptions.tsConfig,
+ context.root,
+ projectConfig.root,
+ 'react'
+ );
+
+ const remoteNames = options.devRemotes?.map((remote) =>
+ typeof remote === 'string' ? remote : remote.remoteName
+ );
+
+ const remotes = getRemotes(
+ remoteNames,
+ options.skipRemotes,
+ moduleFederationConfig,
+ {
+ projectName: context.projectName,
+ projectGraph: context.projectGraph,
+ root: context.root,
+ },
+ pathToManifestFile
+ );
+
+ options.staticRemotesPort ??= remotes.staticRemotePort;
+
+ process.env.NX_MF_DEV_REMOTES = JSON.stringify([
+ ...(remotes.devRemotes.map((r) =>
+ typeof r === 'string' ? r : r.remoteName
+ ) ?? []),
+ projectConfig.name,
+ ]);
+
+ const staticRemotesConfig = parseStaticSsrRemotesConfig(
+ [...remotes.staticRemotes, ...remotes.dynamicRemotes],
+ context
+ );
+
+ const mappedLocationsOfStaticRemotes = await buildSsrStaticRemotes(
+ staticRemotesConfig,
+ nxBin,
+ context,
+ options
+ );
+
+ const devRemoteIters = await startRemotes(
+ remotes.devRemotes,
+ context,
+ options
+ );
+
+ const staticRemotesIter = startSsrStaticRemotesFileServer(
+ staticRemotesConfig,
+ context,
+ options
+ );
+
+ startSsrRemoteProxies(
+ staticRemotesConfig,
+ mappedLocationsOfStaticRemotes,
+ options.ssl
+ ? {
+ pathToCert: options.sslCert,
+ pathToKey: options.sslKey,
+ }
+ : undefined
+ );
+
+ return yield* combineAsyncIterables(
+ iter,
+ ...devRemoteIters,
+ ...(staticRemotesIter ? [staticRemotesIter] : []),
+ createAsyncIterable<{ success: true; baseUrl: string }>(
+ async ({ next, done }) => {
+ if (!options.isInitialHost) {
+ done();
+ return;
+ }
+
+ if (remotes.remotePorts.length === 0) {
+ done();
+ return;
+ }
+
+ try {
+ const host = options.host ?? 'localhost';
+ const baseUrl = `http${options.ssl ? 's' : ''}://${host}:${
+ options.port
+ }`;
+ const portsToWaitFor = staticRemotesIter
+ ? [options.staticRemotesPort, ...remotes.remotePorts]
+ : [...remotes.remotePorts];
+
+ await Promise.all(
+ portsToWaitFor.map((port) =>
+ waitForPortOpen(port, {
+ retries: 480,
+ retryDelay: 2500,
+ host,
+ })
+ )
+ );
+
+ logger.info(
+ `Nx all ssr remotes have started, server ready at ${baseUrl}`
+ );
+ next({ success: true, baseUrl });
+ } catch (error) {
+ throw new Error(
+ `Nx failed to start ssr remotes. Check above for errors.`
+ );
+ } finally {
+ done();
+ }
+ }
+ )
+ );
+}
diff --git a/packages/rspack/src/executors/module-federation-ssr-dev-server/schema.json b/packages/rspack/src/executors/module-federation-ssr-dev-server/schema.json
new file mode 100644
index 0000000000000..8f86f09ec874e
--- /dev/null
+++ b/packages/rspack/src/executors/module-federation-ssr-dev-server/schema.json
@@ -0,0 +1,79 @@
+{
+ "version": 2,
+ "outputCapture": "direct-nodejs",
+ "title": "Module Federation SSR Dev Server",
+ "description": "Serve a SSR host application along with its known remotes.",
+ "cli": "nx",
+ "type": "object",
+ "properties": {
+ "browserTarget": {
+ "type": "string",
+ "description": "Target which builds the browser application.",
+ "x-priority": "important"
+ },
+ "serverTarget": {
+ "type": "string",
+ "description": "Target which builds the server application.",
+ "x-priority": "important"
+ },
+ "port": {
+ "type": "number",
+ "description": "The port to be set on `process.env.PORT` for use in the server.",
+ "default": 4200,
+ "x-priority": "important"
+ },
+ "devRemotes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List of remote applications to run in development mode (i.e. using serve target).",
+ "x-priority": "important"
+ },
+ "skipRemotes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List of remote applications to not automatically serve, either statically or in development mode.",
+ "x-priority": "important"
+ },
+ "host": {
+ "type": "string",
+ "description": "Host to listen on.",
+ "default": "localhost"
+ },
+ "staticRemotesPort": {
+ "type": "number",
+ "description": "The port at which to serve the file-server for the static remotes."
+ },
+ "pathToManifestFile": {
+ "type": "string",
+ "description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root."
+ },
+ "ssl": {
+ "type": "boolean",
+ "description": "Serve using HTTPS.",
+ "default": false
+ },
+ "sslKey": {
+ "type": "string",
+ "description": "SSL key to use for serving HTTPS."
+ },
+ "sslCert": {
+ "type": "string",
+ "description": "SSL certificate to use for serving HTTPS."
+ },
+ "publicHost": {
+ "type": "string",
+ "description": "Public URL where the application will be served."
+ },
+ "isInitialHost": {
+ "type": "boolean",
+ "description": "Whether the host that is running this executor is the first in the project tree to do so.",
+ "default": true,
+ "x-priority": "internal"
+ }
+ },
+ "required": ["browserTarget", "serverTarget"]
+}
diff --git a/packages/rspack/src/executors/module-federation-static-server/module-federation-static-server.impl.ts b/packages/rspack/src/executors/module-federation-static-server/module-federation-static-server.impl.ts
new file mode 100644
index 0000000000000..41b8c549492c6
--- /dev/null
+++ b/packages/rspack/src/executors/module-federation-static-server/module-federation-static-server.impl.ts
@@ -0,0 +1,394 @@
+import {
+ logger,
+ parseTargetString,
+ readTargetOptions,
+ Target,
+ workspaceRoot,
+} from '@nx/devkit';
+import {
+ combineAsyncIterables,
+ createAsyncIterable,
+} from '@nx/devkit/src/utils/async-iterable';
+import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
+import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
+import { fork } from 'child_process';
+import type { Express } from 'express';
+import { cpSync, existsSync, readFileSync, rmSync } from 'fs';
+import { ExecutorContext } from 'nx/src/config/misc-interfaces';
+import { basename, extname, join } from 'path';
+import {
+ getModuleFederationConfig,
+ getRemotes,
+} from '../../utils/module-federation';
+import { buildStaticRemotes } from '../../utils/module-federation/build-static.remotes';
+import {
+ parseStaticRemotesConfig,
+ StaticRemotesConfig,
+} from '../../utils/module-federation/parse-static-remotes-config';
+import { ModuleFederationDevServerOptions } from '../module-federation-dev-server/schema';
+import type { RspackExecutorSchema } from '../rspack/schema';
+import { ModuleFederationStaticServerSchema } from './schema';
+
+function getBuildAndServeOptionsFromServeTarget(
+ serveTarget: string,
+ context: ExecutorContext
+) {
+ const target = parseTargetString(serveTarget, context);
+
+ const serveOptions: ModuleFederationDevServerOptions = readTargetOptions(
+ target,
+ context
+ );
+ const buildTarget = parseTargetString(serveOptions.buildTarget, context);
+
+ const buildOptions: RspackExecutorSchema = readTargetOptions(
+ buildTarget,
+ context
+ );
+
+ let pathToManifestFile = join(
+ context.root,
+ context.projectGraph.nodes[context.projectName].data.sourceRoot,
+ 'assets/module-federation.manifest.json'
+ );
+ if (serveOptions.pathToManifestFile) {
+ const userPathToManifestFile = join(
+ context.root,
+ serveOptions.pathToManifestFile
+ );
+ if (!existsSync(userPathToManifestFile)) {
+ throw new Error(
+ `The provided Module Federation manifest file path does not exist. Please check the file exists at "${userPathToManifestFile}".`
+ );
+ } else if (extname(serveOptions.pathToManifestFile) !== '.json') {
+ throw new Error(
+ `The Module Federation manifest file must be a JSON. Please ensure the file at ${userPathToManifestFile} is a JSON.`
+ );
+ }
+
+ pathToManifestFile = userPathToManifestFile;
+ }
+
+ return {
+ buildTarget,
+ buildOptions,
+ serveOptions,
+ pathToManifestFile,
+ };
+}
+
+async function buildHost(
+ nxBin: string,
+ buildTarget: Target,
+ context: ExecutorContext
+) {
+ await new Promise((res, rej) => {
+ const staticProcess = fork(
+ nxBin,
+ [
+ `run`,
+ `${buildTarget.project}:${buildTarget.target}${
+ buildTarget.configuration
+ ? `:${buildTarget.configuration}`
+ : context.configurationName
+ ? `:${context.configurationName}`
+ : ''
+ }`,
+ ],
+ {
+ cwd: context.root,
+ stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
+ }
+ );
+ staticProcess.stdout.on('data', (data) => {
+ const ANSII_CODE_REGEX =
+ // eslint-disable-next-line no-control-regex
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
+ const stdoutString = data.toString().replace(ANSII_CODE_REGEX, '');
+
+ // in addition to writing into the stdout stream, also show error directly in console
+ // so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output.
+ if (stdoutString.includes('ERROR in')) {
+ logger.log(stdoutString);
+ }
+
+ if (stdoutString.includes('Successfully ran target build')) {
+ staticProcess.stdout.removeAllListeners('data');
+ logger.info(`NX Built host`);
+ res();
+ }
+ });
+ staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
+ staticProcess.once('exit', (code) => {
+ staticProcess.stdout.removeAllListeners('data');
+ staticProcess.stderr.removeAllListeners('data');
+ if (code !== 0) {
+ rej(`Host failed to build. See above for details.`);
+ } else {
+ res();
+ }
+ });
+
+ process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
+ process.on('exit', () => staticProcess.kill('SIGTERM'));
+ });
+}
+
+function moveToTmpDirectory(
+ staticRemotesConfig: StaticRemotesConfig,
+ hostOutputPath: string,
+ hostUrlSegment: string
+) {
+ const commonOutputDirectory = join(
+ workspaceRoot,
+ 'tmp/static-module-federation'
+ );
+ for (const app of staticRemotesConfig.remotes) {
+ const remoteConfig = staticRemotesConfig.config[app];
+ cpSync(
+ remoteConfig.outputPath,
+ join(commonOutputDirectory, remoteConfig.urlSegment),
+ {
+ force: true,
+ recursive: true,
+ }
+ );
+ }
+ cpSync(hostOutputPath, join(commonOutputDirectory, hostUrlSegment), {
+ force: true,
+ recursive: true,
+ });
+
+ const cleanup = () => {
+ rmSync(commonOutputDirectory, { force: true, recursive: true });
+ };
+ process.on('SIGTERM', () => {
+ cleanup();
+ });
+ process.on('exit', () => {
+ cleanup();
+ });
+
+ return commonOutputDirectory;
+}
+
+export function startProxies(
+ staticRemotesConfig: StaticRemotesConfig,
+ hostServeOptions: ModuleFederationDevServerOptions,
+ mappedLocationOfHost: string,
+ mappedLocationsOfRemotes: Record,
+ sslOptions?: { pathToCert: string; pathToKey: string }
+) {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const { createProxyMiddleware } = require('http-proxy-middleware');
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const express = require('express');
+ let sslCert: Buffer;
+ let sslKey: Buffer;
+ if (sslOptions && sslOptions.pathToCert && sslOptions.pathToKey) {
+ if (existsSync(sslOptions.pathToCert) && existsSync(sslOptions.pathToKey)) {
+ sslCert = readFileSync(sslOptions.pathToCert);
+ sslKey = readFileSync(sslOptions.pathToKey);
+ } else {
+ logger.warn(
+ `Encountered SSL options in project.json, however, the certificate files do not exist in the filesystem. Using http.`
+ );
+ logger.warn(
+ `Attempted to find '${sslOptions.pathToCert}' and '${sslOptions.pathToKey}'.`
+ );
+ }
+ }
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const http = require('http');
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const https = require('https');
+
+ logger.info(`NX Starting static remotes proxies...`);
+ for (const app of staticRemotesConfig.remotes) {
+ const expressProxy: Express = express();
+ expressProxy.use(
+ createProxyMiddleware({
+ target: mappedLocationsOfRemotes[app],
+ changeOrigin: true,
+ secure: sslCert ? false : undefined,
+ })
+ );
+ const proxyServer = (sslCert ? https : http)
+ .createServer({ cert: sslCert, key: sslKey }, expressProxy)
+ .listen(staticRemotesConfig.config[app].port);
+ process.on('SIGTERM', () => proxyServer.close());
+ process.on('exit', () => proxyServer.close());
+ }
+ logger.info(`NX Static remotes proxies started successfully`);
+ logger.info(`NX Starting static host proxy...`);
+ const expressProxy: Express = express();
+ expressProxy.use(
+ createProxyMiddleware({
+ target: mappedLocationOfHost,
+ changeOrigin: true,
+ secure: sslCert ? false : undefined,
+ pathRewrite: (path) => {
+ let pathRewrite = path;
+ for (const app of staticRemotesConfig.remotes) {
+ if (path.endsWith(app)) {
+ pathRewrite = '/';
+ break;
+ }
+ }
+ return pathRewrite;
+ },
+ })
+ );
+ const proxyServer = (sslCert ? https : http)
+ .createServer({ cert: sslCert, key: sslKey }, expressProxy)
+ .listen(hostServeOptions.port);
+ process.on('SIGTERM', () => proxyServer.close());
+ process.on('exit', () => proxyServer.close());
+ logger.info('NX Static host proxy started successfully');
+}
+
+export default async function* moduleFederationStaticServer(
+ schema: ModuleFederationStaticServerSchema,
+ context: ExecutorContext
+) {
+ // Force Node to resolve to look for the nx binary that is inside node_modules
+ const nxBin = require.resolve('nx/bin/nx');
+
+ // Get the remotes from the module federation config
+ const p = context.projectsConfigurations.projects[context.projectName];
+ const options = getBuildAndServeOptionsFromServeTarget(
+ schema.serveTarget,
+ context
+ );
+
+ const moduleFederationConfig = getModuleFederationConfig(
+ options.buildOptions.tsConfig,
+ context.root,
+ p.root,
+ 'react'
+ );
+
+ const remotes = getRemotes(
+ [],
+ options.serveOptions.skipRemotes,
+ moduleFederationConfig,
+ {
+ projectName: context.projectName,
+ projectGraph: context.projectGraph,
+ root: context.root,
+ },
+ options.pathToManifestFile
+ );
+
+ const staticRemotesConfig = parseStaticRemotesConfig(
+ [...remotes.staticRemotes, ...remotes.dynamicRemotes],
+ context
+ );
+
+ options.serveOptions.staticRemotesPort ??= remotes.staticRemotePort;
+ const mappedLocationsOfStaticRemotes = await buildStaticRemotes(
+ staticRemotesConfig,
+ nxBin,
+ context,
+ options.serveOptions
+ );
+
+ // Build the host
+ const hostUrlSegment = basename(options.buildOptions.outputPath);
+ const mappedLocationOfHost = `http${options.serveOptions.ssl ? 's' : ''}://${
+ options.serveOptions.host
+ }:${options.serveOptions.staticRemotesPort}/${hostUrlSegment}`;
+ await buildHost(nxBin, options.buildTarget, context);
+
+ // Move to a temporary directory
+ const commonOutputDirectory = moveToTmpDirectory(
+ staticRemotesConfig,
+ options.buildOptions.outputPath,
+ hostUrlSegment
+ );
+
+ // File Serve the temporary directory
+ const staticFileServerIter = fileServerExecutor(
+ {
+ cors: true,
+ watch: false,
+ staticFilePath: commonOutputDirectory,
+ parallel: false,
+ spa: false,
+ withDeps: false,
+ host: options.serveOptions.host,
+ port: options.serveOptions.staticRemotesPort,
+ ssl: options.serveOptions.ssl,
+ sslCert: options.serveOptions.sslCert,
+ sslKey: options.serveOptions.sslKey,
+ cacheSeconds: -1,
+ },
+ context
+ );
+
+ // express proxy all of it
+ startProxies(
+ staticRemotesConfig,
+ options.serveOptions,
+ mappedLocationOfHost,
+ mappedLocationsOfStaticRemotes,
+ options.serveOptions.ssl
+ ? {
+ pathToCert: join(workspaceRoot, options.serveOptions.sslCert),
+ pathToKey: join(workspaceRoot, options.serveOptions.sslKey),
+ }
+ : undefined
+ );
+
+ return yield* combineAsyncIterables(
+ staticFileServerIter,
+ createAsyncIterable<{ success: true; baseUrl: string }>(
+ async ({ next, done }) => {
+ const host = options.serveOptions.host ?? 'localhost';
+ const baseUrl = `http${options.serveOptions.ssl ? 's' : ''}://${host}:${
+ options.serveOptions.port
+ }`;
+
+ if (remotes.remotePorts.length === 0) {
+ const portsToWaitFor = [options.serveOptions.staticRemotesPort];
+ await Promise.all(
+ portsToWaitFor.map((port) =>
+ waitForPortOpen(port, {
+ retries: 480,
+ retryDelay: 2500,
+ host: host,
+ })
+ )
+ );
+
+ logger.info(`NX Server ready at ${baseUrl}`);
+ next({ success: true, baseUrl: baseUrl });
+ done();
+ return;
+ }
+
+ try {
+ const portsToWaitFor = staticFileServerIter
+ ? [options.serveOptions.staticRemotesPort, ...remotes.remotePorts]
+ : [...remotes.remotePorts];
+ await Promise.all(
+ portsToWaitFor.map((port) =>
+ waitForPortOpen(port, {
+ retries: 480,
+ retryDelay: 2500,
+ host: host,
+ })
+ )
+ );
+
+ logger.info(`NX Server ready at ${baseUrl}`);
+ next({ success: true, baseUrl: baseUrl });
+ } catch (err) {
+ throw new Error(`Failed to start. Check above for any errors.`);
+ } finally {
+ done();
+ }
+ }
+ )
+ );
+}
diff --git a/packages/rspack/src/executors/module-federation-static-server/schema.d.ts b/packages/rspack/src/executors/module-federation-static-server/schema.d.ts
new file mode 100644
index 0000000000000..8386a0f2006ed
--- /dev/null
+++ b/packages/rspack/src/executors/module-federation-static-server/schema.d.ts
@@ -0,0 +1,3 @@
+export interface ModuleFederationStaticServerSchema {
+ serveTarget: string;
+}
diff --git a/packages/rspack/src/executors/module-federation-static-server/schema.json b/packages/rspack/src/executors/module-federation-static-server/schema.json
new file mode 100644
index 0000000000000..329ac0a6226d5
--- /dev/null
+++ b/packages/rspack/src/executors/module-federation-static-server/schema.json
@@ -0,0 +1,14 @@
+{
+ "version": 2,
+ "outputCapture": "direct-nodejs",
+ "title": "Module Federation Static Dev Server",
+ "description": "Serve a host application statically along with it's remotes.",
+ "cli": "nx",
+ "type": "object",
+ "properties": {
+ "serveTarget": {
+ "type": "string"
+ }
+ },
+ "required": ["serveTarget"]
+}
diff --git a/packages/rspack/src/executors/rspack/rspack.impl.ts b/packages/rspack/src/executors/rspack/rspack.impl.ts
new file mode 100644
index 0000000000000..afcd885ddc7fd
--- /dev/null
+++ b/packages/rspack/src/executors/rspack/rspack.impl.ts
@@ -0,0 +1,146 @@
+import { ExecutorContext, logger } from '@nx/devkit';
+import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
+import { printDiagnostics, runTypeCheck } from '@nx/js';
+import { Compiler, MultiCompiler, MultiStats, Stats } from '@rspack/core';
+import { rmSync } from 'fs';
+import * as path from 'path';
+import { createCompiler, isMultiCompiler } from '../../utils/create-compiler';
+import { isMode } from '../../utils/mode-utils';
+import { RspackExecutorSchema } from './schema';
+
+export default async function* runExecutor(
+ options: RspackExecutorSchema,
+ context: ExecutorContext
+) {
+ process.env.NODE_ENV ??= options.mode ?? 'production';
+
+ if (isMode(process.env.NODE_ENV)) {
+ options.mode = process.env.NODE_ENV;
+ }
+
+ if (options.typeCheck) {
+ await executeTypeCheck(options, context);
+ }
+
+ // Mimic --clean from webpack.
+ rmSync(path.join(context.root, options.outputPath), {
+ force: true,
+ recursive: true,
+ });
+
+ const compiler = await createCompiler(options, context);
+
+ const iterable = createAsyncIterable<{
+ success: boolean;
+ outfile?: string;
+ }>(async ({ next, done }) => {
+ if (options.watch) {
+ const watcher = compiler.watch(
+ {},
+ async (err, stats: Stats | MultiStats) => {
+ if (err) {
+ logger.error(err);
+ next({ success: false });
+ return;
+ }
+ if (!compiler || !stats) {
+ logger.error(new Error('Compiler or stats not available'));
+ next({ success: false });
+ return;
+ }
+
+ const statsOptions = getStatsOptions(compiler);
+ const printedStats = stats.toString(statsOptions);
+ // Avoid extra empty line when `stats: 'none'`
+ if (printedStats) {
+ console.error(printedStats);
+ }
+ next({
+ success: !stats.hasErrors(),
+ outfile: path.resolve(context.root, options.outputPath, 'main.js'),
+ });
+ }
+ );
+
+ registerCleanupCallback(() => {
+ watcher.close(() => {
+ logger.info('Watcher closed');
+ });
+ });
+ } else {
+ compiler.run(async (err, stats: Stats | MultiStats) => {
+ compiler.close(() => {
+ if (err) {
+ logger.error(err);
+ next({ success: false });
+ return;
+ }
+ if (!compiler || !stats) {
+ logger.error(new Error('Compiler or stats not available'));
+ next({ success: false });
+ return;
+ }
+
+ const statsOptions = getStatsOptions(compiler);
+ const printedStats = stats.toString(statsOptions);
+ // Avoid extra empty line when `stats: 'none'`
+ if (printedStats) {
+ console.error(printedStats);
+ }
+ next({
+ success: !stats.hasErrors(),
+ outfile: path.resolve(context.root, options.outputPath, 'main.js'),
+ });
+ done();
+ });
+ });
+ }
+ });
+
+ yield* iterable;
+}
+
+// copied from packages/esbuild/src/executors/esbuild/esbuild.impl.ts
+function registerCleanupCallback(callback: () => void) {
+ const wrapped = () => {
+ callback();
+ process.off('SIGINT', wrapped);
+ process.off('SIGTERM', wrapped);
+ process.off('exit', wrapped);
+ };
+
+ process.on('SIGINT', wrapped);
+ process.on('SIGTERM', wrapped);
+ process.on('exit', wrapped);
+}
+
+async function executeTypeCheck(
+ options: RspackExecutorSchema,
+ context: ExecutorContext
+) {
+ const projectConfiguration =
+ context.projectGraph.nodes[context.projectName].data;
+ const result = await runTypeCheck({
+ workspaceRoot: path.resolve(projectConfiguration.root),
+ tsConfigPath: options.tsConfig,
+ mode: 'noEmit',
+ });
+
+ await printDiagnostics(result.errors, result.warnings);
+
+ if (result.errors.length > 0) {
+ throw new Error('Found type errors. See above.');
+ }
+}
+
+function getStatsOptions(compiler: Compiler | MultiCompiler) {
+ return isMultiCompiler(compiler)
+ ? {
+ children: compiler.compilers.map((compiler) =>
+ compiler.options ? compiler.options.stats : undefined
+ ),
+ }
+ : compiler.options
+ ? compiler.options.stats
+ : undefined;
+}
diff --git a/packages/rspack/src/executors/rspack/schema.d.ts b/packages/rspack/src/executors/rspack/schema.d.ts
new file mode 100644
index 0000000000000..fbc07402b5096
--- /dev/null
+++ b/packages/rspack/src/executors/rspack/schema.d.ts
@@ -0,0 +1,34 @@
+import type { Mode } from '@rspack/core';
+
+export interface RspackExecutorSchema {
+ target: 'web' | 'node';
+ main: string;
+ index?: string;
+ tsConfig: string;
+ typeCheck?: boolean;
+ outputPath: string;
+ outputFileName?: string;
+ indexHtml?: string;
+ mode?: Mode;
+ watch?: boolean;
+ baseHref?: string;
+ deployUrl?: string;
+
+ rspackConfig: string;
+ optimization?: boolean | OptimizationOptions;
+ sourceMap?: boolean | string;
+ assets?: any[];
+ extractLicenses?: boolean;
+ fileReplacements?: FileReplacement[];
+ generatePackageJson?: boolean;
+}
+
+export interface FileReplacement {
+ replace: string;
+ with: string;
+}
+
+export interface OptimizationOptions {
+ scripts: boolean;
+ styles: boolean;
+}
diff --git a/packages/rspack/src/executors/rspack/schema.json b/packages/rspack/src/executors/rspack/schema.json
new file mode 100644
index 0000000000000..2faa51e02df7c
--- /dev/null
+++ b/packages/rspack/src/executors/rspack/schema.json
@@ -0,0 +1,177 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "version": 2,
+ "title": "Rspack build executor",
+ "description": "Run Rspack via an executor for a project.",
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "string",
+ "description": "The platform to target (e.g. web, node).",
+ "enum": ["web", "node"]
+ },
+ "main": {
+ "type": "string",
+ "description": "The main entry file."
+ },
+ "outputPath": {
+ "type": "string",
+ "description": "The output path for the bundle."
+ },
+ "outputFileName": {
+ "type": "string",
+ "description": "The main output entry file"
+ },
+ "tsConfig": {
+ "type": "string",
+ "description": "The tsconfig file to build the project."
+ },
+ "typeCheck": {
+ "type": "boolean",
+ "description": "Skip the type checking."
+ },
+ "indexHtml": {
+ "type": "string",
+ "description": "The path to the index.html file."
+ },
+ "index": {
+ "type": "string",
+ "description": "HTML File which will be contain the application.",
+ "x-completion-type": "file",
+ "x-completion-glob": "**/*@(.html|.htm)"
+ },
+ "baseHref": {
+ "type": "string",
+ "description": "Base url for the application being built."
+ },
+ "deployUrl": {
+ "type": "string",
+ "description": "URL where the application will be deployed."
+ },
+ "rspackConfig": {
+ "type": "string",
+ "description": "The path to the rspack config file."
+ },
+ "optimization": {
+ "description": "Enables optimization of the build output.",
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "scripts": {
+ "type": "boolean",
+ "description": "Enables optimization of the scripts output.",
+ "default": true
+ },
+ "styles": {
+ "type": "boolean",
+ "description": "Enables optimization of the styles output.",
+ "default": true
+ }
+ },
+ "additionalProperties": false
+ },
+ {
+ "type": "boolean"
+ }
+ ]
+ },
+ "sourceMap": {
+ "description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.",
+ "default": true,
+ "oneOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "assets": {
+ "type": "array",
+ "description": "List of static application assets.",
+ "default": [],
+ "items": {
+ "$ref": "#/definitions/assetPattern"
+ }
+ },
+ "extractLicenses": {
+ "type": "boolean",
+ "description": "Extract all licenses in a separate file.",
+ "default": true
+ },
+ "fileReplacements": {
+ "description": "Replace files with other files in the build.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "replace": {
+ "type": "string",
+ "description": "The file to be replaced.",
+ "x-completion-type": "file"
+ },
+ "with": {
+ "type": "string",
+ "description": "The file to replace with.",
+ "x-completion-type": "file"
+ }
+ },
+ "additionalProperties": false,
+ "required": ["replace", "with"]
+ },
+ "default": []
+ },
+ "mode": {
+ "type": "string",
+ "description": "Mode to run the build in.",
+ "enum": ["development", "production", "none"]
+ },
+ "generatePackageJson": {
+ "type": "boolean",
+ "description": "Generates a `package.json` and pruned lock file with the project's `node_module` dependencies populated for installing in a container. If a `package.json` exists in the project's directory, it will be reused with dependencies populated."
+ }
+ },
+ "required": ["target", "main", "outputPath", "tsConfig", "rspackConfig"],
+ "definitions": {
+ "assetPattern": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "glob": {
+ "type": "string",
+ "description": "The pattern to match."
+ },
+ "input": {
+ "type": "string",
+ "description": "The input directory path in which to apply 'glob'. Defaults to the project root."
+ },
+ "ignore": {
+ "description": "An array of globs to ignore.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "output": {
+ "type": "string",
+ "description": "Absolute path within the output."
+ },
+ "watch": {
+ "type": "boolean",
+ "description": "Enable re-building when files change.",
+ "default": false
+ }
+ },
+ "additionalProperties": false,
+ "required": ["glob", "input", "output"]
+ },
+ {
+ "type": "string"
+ }
+ ]
+ }
+ }
+}
diff --git a/packages/rspack/src/executors/ssr-dev-server/lib/wait-until-server-is-listening.ts b/packages/rspack/src/executors/ssr-dev-server/lib/wait-until-server-is-listening.ts
new file mode 100644
index 0000000000000..35de9978cddd0
--- /dev/null
+++ b/packages/rspack/src/executors/ssr-dev-server/lib/wait-until-server-is-listening.ts
@@ -0,0 +1,38 @@
+import * as net from 'net';
+
+export function waitUntilServerIsListening(port: number): Promise {
+ const allowedErrorCodes = ['ECONNREFUSED', 'ECONNRESET'];
+ const maxAttempts = 25;
+ let attempts = 0;
+ const client = new net.Socket();
+ const cleanup = () => {
+ client.removeAllListeners('connect');
+ client.removeAllListeners('error');
+ client.end();
+ client.destroy();
+ client.unref();
+ };
+
+ return new Promise((resolve, reject) => {
+ const listen = () => {
+ client.once('connect', () => {
+ cleanup();
+ resolve();
+ });
+ client.on('error', (err) => {
+ if (
+ attempts > maxAttempts ||
+ !allowedErrorCodes.includes(err['code'])
+ ) {
+ cleanup();
+ reject(err);
+ } else {
+ attempts++;
+ setTimeout(listen, 100 * attempts);
+ }
+ });
+ client.connect({ port, host: 'localhost' });
+ };
+ listen();
+ });
+}
diff --git a/packages/rspack/src/executors/ssr-dev-server/schema.d.ts b/packages/rspack/src/executors/ssr-dev-server/schema.d.ts
new file mode 100644
index 0000000000000..ee03668413a03
--- /dev/null
+++ b/packages/rspack/src/executors/ssr-dev-server/schema.d.ts
@@ -0,0 +1,11 @@
+interface TargetOptions {
+ [key: string]: string | boolean | number | TargetOptions;
+}
+
+export interface RspackSsrDevServerOptions {
+ browserTarget: string;
+ serverTarget: string;
+ port: number;
+ browserTargetOptions: TargetOptions;
+ serverTargetOptions: TargetOptions;
+}
diff --git a/packages/rspack/src/executors/ssr-dev-server/schema.json b/packages/rspack/src/executors/ssr-dev-server/schema.json
new file mode 100644
index 0000000000000..fc22ad14d251a
--- /dev/null
+++ b/packages/rspack/src/executors/ssr-dev-server/schema.json
@@ -0,0 +1,36 @@
+{
+ "outputCapture": "direct-nodejs",
+ "title": "Rspack SSR Dev Server",
+ "description": "Serve a SSR application using rspack.",
+ "cli": "nx",
+ "type": "object",
+ "properties": {
+ "browserTarget": {
+ "type": "string",
+ "description": "Target which builds the browser application.",
+ "x-priority": "important"
+ },
+ "serverTarget": {
+ "type": "string",
+ "description": "Target which builds the server application.",
+ "x-priority": "important"
+ },
+ "port": {
+ "type": "number",
+ "description": "The port to be set on `process.env.PORT` for use in the server.",
+ "default": 4200,
+ "x-priority": "important"
+ },
+ "browserTargetOptions": {
+ "type": "object",
+ "description": "Additional options to pass into the browser build target.",
+ "default": {}
+ },
+ "serverTargetOptions": {
+ "type": "object",
+ "description": "Additional options to pass into the server build target.",
+ "default": {}
+ }
+ },
+ "required": ["browserTarget", "serverTarget"]
+}
diff --git a/packages/rspack/src/executors/ssr-dev-server/ssr-dev-server.impl.ts b/packages/rspack/src/executors/ssr-dev-server/ssr-dev-server.impl.ts
new file mode 100644
index 0000000000000..92f614abb07a4
--- /dev/null
+++ b/packages/rspack/src/executors/ssr-dev-server/ssr-dev-server.impl.ts
@@ -0,0 +1,75 @@
+import {
+ ExecutorContext,
+ parseTargetString,
+ readTargetOptions,
+ runExecutor,
+} from '@nx/devkit';
+import { combineAsyncIterables } from '@nx/devkit/src/utils/async-iterable';
+import * as chalk from 'chalk';
+
+import { RspackExecutorSchema } from '../rspack/schema';
+import { waitUntilServerIsListening } from './lib/wait-until-server-is-listening';
+import { RspackSsrDevServerOptions, TargetOptions } from './schema';
+
+export async function* ssrDevServerExecutor(
+ options: RspackSsrDevServerOptions,
+ context: ExecutorContext
+) {
+ const browserTarget = parseTargetString(
+ options.browserTarget,
+ context.projectGraph
+ );
+ const serverTarget = parseTargetString(options.serverTarget, context);
+ const browserOptions = readTargetOptions(
+ browserTarget,
+ context
+ );
+ const serverOptions = readTargetOptions(
+ serverTarget,
+ context
+ );
+
+ const runBrowser = await runExecutor<{
+ success: boolean;
+ baseUrl?: string;
+ options: TargetOptions;
+ }>(
+ browserTarget,
+ { ...browserOptions, ...options.browserTargetOptions },
+ context
+ );
+ const runServer = await runExecutor<{
+ success: boolean;
+ baseUrl?: string;
+ options: TargetOptions;
+ }>(
+ serverTarget,
+ { ...serverOptions, ...options.serverTargetOptions },
+ context
+ );
+ let browserBuilt = false;
+ let nodeStarted = false;
+ const combined = combineAsyncIterables(runBrowser, runServer);
+
+ for await (const output of combined) {
+ if (!output.success) throw new Error('Could not build application');
+ if (output.options?.target === 'node') {
+ nodeStarted = true;
+ } else if (output.options?.target === 'web') {
+ browserBuilt = true;
+ }
+
+ if (nodeStarted && browserBuilt) {
+ await waitUntilServerIsListening(options.port);
+ console.log(
+ `[ ${chalk.green('ready')} ] on http://localhost:${options.port}`
+ );
+ yield {
+ ...output,
+ baseUrl: `http://localhost:${options.port}`,
+ };
+ }
+ }
+}
+
+export default ssrDevServerExecutor;
diff --git a/packages/rspack/src/generators/application/application.ts b/packages/rspack/src/generators/application/application.ts
new file mode 100644
index 0000000000000..10e65626395a3
--- /dev/null
+++ b/packages/rspack/src/generators/application/application.ts
@@ -0,0 +1,105 @@
+import { ensurePackage, formatFiles, runTasksInSerial, Tree } from '@nx/devkit';
+import { version as nxVersion } from 'nx/package.json';
+import configurationGenerator from '../configuration/configuration';
+import rspackInitGenerator from '../init/init';
+import { normalizeOptions } from './lib/normalize-options';
+import { ApplicationGeneratorSchema } from './schema';
+
+export default async function (
+ tree: Tree,
+ _options: ApplicationGeneratorSchema
+) {
+ const tasks = [];
+ const initTask = await rspackInitGenerator(tree, {
+ ..._options,
+ // TODO: Crystalize the default rspack.config.js file.
+ // The default setup isn't crystalized so don't add plugin.
+ addPlugin: false,
+ });
+ tasks.push(initTask);
+
+ const options = normalizeOptions(tree, _options);
+
+ options.style ??= 'css';
+
+ if (options.framework === 'nest') {
+ const { applicationGenerator: nestAppGenerator } = ensurePackage(
+ '@nx/nest',
+ nxVersion
+ );
+ const createAppTask = await nestAppGenerator(tree, {
+ ...options,
+ skipFormat: true,
+ tags: options.tags ?? '',
+ addPlugin: false,
+ });
+
+ const convertAppTask = await configurationGenerator(tree, {
+ project: options.name,
+ target: 'node',
+ newProject: false,
+ buildTarget: 'build',
+ framework: 'nest',
+ });
+
+ tasks.push(createAppTask, convertAppTask);
+ } else if (options.framework === 'web') {
+ const { applicationGenerator: webAppGenerator } = ensurePackage(
+ '@nx/web',
+ nxVersion
+ );
+ const createAppTask = await webAppGenerator(tree, {
+ bundler: 'webpack',
+ name: options.name,
+ style: options.style,
+ directory: options.directory,
+ tags: options.tags ?? '',
+ unitTestRunner: options.unitTestRunner,
+ e2eTestRunner: options.e2eTestRunner,
+ rootProject: options.rootProject,
+ skipFormat: true,
+ addPlugin: false,
+ });
+ const convertAppTask = await configurationGenerator(tree, {
+ project: options.name,
+ target: 'web',
+ newProject: false,
+ buildTarget: 'build',
+ serveTarget: 'serve',
+ framework: 'web',
+ addPlugin: false,
+ });
+ tasks.push(createAppTask, convertAppTask);
+ } else {
+ // default to react
+ const { applicationGenerator: reactAppGenerator } = ensurePackage(
+ '@nx/react',
+ nxVersion
+ );
+ const createAppTask = await reactAppGenerator(tree, {
+ bundler: 'webpack',
+ name: options.name,
+ style: options.style,
+ directory: options.directory,
+ tags: options.tags ?? '',
+ unitTestRunner: options.unitTestRunner,
+ e2eTestRunner: options.e2eTestRunner,
+ rootProject: options.rootProject,
+ skipFormat: true,
+ addPlugin: false,
+ });
+ const convertAppTask = await configurationGenerator(tree, {
+ project: options.name,
+ target: 'web',
+ newProject: false,
+ buildTarget: 'build',
+ serveTarget: 'serve',
+ framework: 'react',
+ });
+ tasks.push(createAppTask, convertAppTask);
+ }
+
+ await formatFiles(tree);
+
+ return runTasksInSerial(...tasks);
+}
diff --git a/packages/rspack/src/generators/application/lib/create-ts-config.ts b/packages/rspack/src/generators/application/lib/create-ts-config.ts
new file mode 100644
index 0000000000000..4bbfa2b836565
--- /dev/null
+++ b/packages/rspack/src/generators/application/lib/create-ts-config.ts
@@ -0,0 +1,55 @@
+import { Tree, workspaceRoot, writeJson } from '@nx/devkit';
+import { relative } from 'path';
+import { Framework } from '../../init/schema';
+
+export function editTsConfig(
+ tree: Tree,
+ projectRoot: string,
+ framework: Framework,
+ relativePathToRootTsConfig: string
+) {
+ // Nx 15.8 moved util to @nx/js, but it is in @nx/workspace in 15.7
+ let shared: any;
+ try {
+ shared = require('@nx/js/src/utils/typescript/create-ts-config');
+ } catch {
+ shared = require('@nx/workspace/src/utils/create-ts-config');
+ }
+
+ if (framework === 'react') {
+ const json = {
+ compilerOptions: {
+ jsx: 'react-jsx',
+ allowJs: false,
+ esModuleInterop: false,
+ allowSyntheticDefaultImports: true,
+ strict: true,
+ },
+ files: [],
+ include: [],
+ references: [
+ {
+ path: './tsconfig.app.json',
+ },
+ ],
+ } as any;
+
+ // inline tsconfig.base.json into the project
+ if (projectIsRootProjectInStandaloneWorkspace(projectRoot)) {
+ json.compileOnSave = false;
+ json.compilerOptions = {
+ ...shared.tsConfigBaseOptions,
+ ...json.compilerOptions,
+ };
+ json.exclude = ['node_modules', 'tmp'];
+ } else {
+ json.extends = relativePathToRootTsConfig;
+ }
+
+ writeJson(tree, `${projectRoot}/tsconfig.json`, json);
+ }
+}
+
+function projectIsRootProjectInStandaloneWorkspace(projectRoot: string) {
+ return relative(workspaceRoot, projectRoot).length === 0;
+}
diff --git a/packages/rspack/src/generators/application/lib/normalize-options.spec.ts b/packages/rspack/src/generators/application/lib/normalize-options.spec.ts
new file mode 100644
index 0000000000000..3e560296fc41e
--- /dev/null
+++ b/packages/rspack/src/generators/application/lib/normalize-options.spec.ts
@@ -0,0 +1,51 @@
+import { Tree } from '@nx/devkit';
+import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
+import { normalizeOptions } from './normalize-options';
+
+describe('normalizeOptions', () => {
+ let tree: Tree;
+
+ beforeEach(() => {
+ tree = createTreeWithEmptyWorkspace();
+ });
+
+ it('should set { rootProject: true } when --rootProject=true is passed', () => {
+ expect(
+ normalizeOptions(tree, {
+ name: 'demo',
+ style: 'css',
+ rootProject: true,
+ }).rootProject
+ ).toBeTruthy();
+ });
+
+ it('should set { rootProject: false } when --rootProject=undefined is passed', () => {
+ expect(
+ normalizeOptions(tree, {
+ name: 'demo',
+ style: 'css',
+ }).rootProject
+ ).toBeFalsy();
+ });
+
+ it('should set { rootProject: false } when --rootProject=false is passed', () => {
+ expect(
+ normalizeOptions(tree, {
+ name: 'demo',
+ style: 'css',
+ rootProject: false,
+ }).rootProject
+ ).toBeFalsy();
+ });
+
+ it('should set { rootProject: false } when --monorepo=true and --rootProject=true is passed', () => {
+ expect(
+ normalizeOptions(tree, {
+ name: 'demo',
+ style: 'css',
+ monorepo: true,
+ rootProject: true,
+ }).rootProject
+ ).toBeFalsy();
+ });
+});
diff --git a/packages/rspack/src/generators/application/lib/normalize-options.ts b/packages/rspack/src/generators/application/lib/normalize-options.ts
new file mode 100644
index 0000000000000..72c03ffec87c2
--- /dev/null
+++ b/packages/rspack/src/generators/application/lib/normalize-options.ts
@@ -0,0 +1,54 @@
+import {
+ extractLayoutDirectory,
+ getWorkspaceLayout,
+ names,
+ normalizePath,
+ Tree,
+} from '@nx/devkit';
+import { ApplicationGeneratorSchema, NormalizedSchema } from '../schema';
+
+export function normalizeDirectory(options: ApplicationGeneratorSchema) {
+ const { projectDirectory } = extractLayoutDirectory(options.directory);
+ return projectDirectory
+ ? `${names(projectDirectory).fileName}/${names(options.name).fileName}`
+ : names(options.name).fileName;
+}
+
+export function normalizeProjectName(options: ApplicationGeneratorSchema) {
+ return normalizeDirectory(options).replace(new RegExp('/', 'g'), '-');
+}
+
+export function normalizeOptions(
+ host: Tree,
+ options: ApplicationGeneratorSchema
+): NormalizedSchema {
+ // --monorepo takes precedence over --rootProject
+ // This won't be needed once we add --bundler=rspack to the @nx/react:app preset
+ const rootProject = !options.monorepo && options.rootProject;
+ const appDirectory = normalizeDirectory(options);
+ const appProjectName = normalizeProjectName(options);
+ const e2eProjectName = options.rootProject
+ ? 'e2e'
+ : `${names(options.name).fileName}-e2e`;
+
+ const { layoutDirectory } = extractLayoutDirectory(options.directory);
+ const appsDir = layoutDirectory ?? getWorkspaceLayout(host).appsDir;
+ const appProjectRoot = rootProject
+ ? '.'
+ : normalizePath(`${appsDir}/${appDirectory}`);
+
+ const normalized = {
+ ...options,
+ rootProject,
+ name: names(options.name).fileName,
+ projectName: appProjectName,
+ appProjectRoot,
+ e2eProjectName,
+ fileName: 'app',
+ } as NormalizedSchema;
+
+ normalized.unitTestRunner ??= 'jest';
+ normalized.e2eTestRunner ??= 'cypress';
+
+ return normalized;
+}
diff --git a/packages/rspack/src/generators/application/schema.d.ts b/packages/rspack/src/generators/application/schema.d.ts
new file mode 100644
index 0000000000000..c14d74bf346f7
--- /dev/null
+++ b/packages/rspack/src/generators/application/schema.d.ts
@@ -0,0 +1,16 @@
+export interface ApplicationGeneratorSchema {
+ name: string;
+ framework?: Framework;
+ style: 'css' | 'scss' | 'less' | 'styl';
+ unitTestRunner?: 'none' | 'jest';
+ e2eTestRunner?: 'none' | 'cypress';
+ directory?: string;
+ tags?: string;
+ rootProject?: boolean;
+ monorepo?: boolean;
+}
+
+export interface NormalizedSchema extends ApplicationGeneratorSchema {
+ appProjectRoot: string;
+ e2eProjectName: string;
+}
diff --git a/packages/rspack/src/generators/application/schema.json b/packages/rspack/src/generators/application/schema.json
new file mode 100644
index 0000000000000..aac817b81adef
--- /dev/null
+++ b/packages/rspack/src/generators/application/schema.json
@@ -0,0 +1,98 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Application",
+ "title": "Application generator for React + rspack",
+ "type": "object",
+ "description": "React + Rspack application generator.",
+ "examples": [
+ {
+ "command": "nx g app myapp --directory=myorg",
+ "description": "Generate `apps/myorg/myapp` and `apps/myorg/myapp-e2e`"
+ }
+ ],
+ "properties": {
+ "name": {
+ "description": "The name of the application.",
+ "type": "string",
+ "$default": {
+ "$source": "argv",
+ "index": 0
+ },
+ "x-prompt": "What name would you like to use for the application?",
+ "pattern": "^[a-zA-Z].*$",
+ "x-priority": "important"
+ },
+ "framework": {
+ "type": "string",
+ "description": "The framework to use for the application.",
+ "x-prompt": "What framework do you want to use when generating this application?",
+ "enum": ["none", "react", "web", "nest"],
+ "alias": ["uiFramework"],
+ "x-priority": "important",
+ "default": "react"
+ },
+ "style": {
+ "description": "The file extension to be used for style files.",
+ "type": "string",
+ "default": "css",
+ "alias": "s",
+ "x-prompt": {
+ "message": "Which stylesheet format would you like to use?",
+ "type": "list",
+ "items": [
+ {
+ "value": "css",
+ "label": "CSS"
+ },
+ {
+ "value": "scss",
+ "label": "SASS(.scss) [ http://sass-lang.com ]"
+ },
+ {
+ "value": "styl",
+ "label": "Stylus(.styl) [ http://stylus-lang.com ]"
+ },
+ {
+ "value": "less",
+ "label": "LESS [ http://lesscss.org ]"
+ },
+ {
+ "value": "none",
+ "label": "None"
+ }
+ ]
+ }
+ },
+ "unitTestRunner": {
+ "type": "string",
+ "description": "The unit test runner to use.",
+ "enum": ["none", "jest"],
+ "default": "jest"
+ },
+ "e2eTestRunner": {
+ "type": "string",
+ "description": "The e2e test runner to use.",
+ "enum": ["none", "cypress"],
+ "default": "cypress"
+ },
+ "directory": {
+ "type": "string",
+ "description": "The directory to nest the app under."
+ },
+ "tags": {
+ "type": "string",
+ "description": "Add tags to the application (used for linting).",
+ "alias": "t"
+ },
+ "monorepo": {
+ "type": "boolean",
+ "description": "Creates an integrated monorepo.",
+ "aliases": ["integrated"]
+ },
+ "rootProject": {
+ "type": "boolean",
+ "x-priority": "internal"
+ }
+ },
+ "required": ["name"]
+}
diff --git a/packages/rspack/src/generators/configuration/configuration.ts b/packages/rspack/src/generators/configuration/configuration.ts
new file mode 100644
index 0000000000000..4a977e450f320
--- /dev/null
+++ b/packages/rspack/src/generators/configuration/configuration.ts
@@ -0,0 +1,153 @@
+import {
+ formatFiles,
+ joinPathFragments,
+ offsetFromRoot,
+ readProjectConfiguration,
+ Tree,
+} from '@nx/devkit';
+import {
+ addOrChangeBuildTarget,
+ addOrChangeServeTarget,
+ deleteWebpackConfig,
+ determineFrameworkAndTarget,
+ findExistingTargetsInProject,
+ handleUnknownExecutors,
+ handleUnsupportedUserProvidedTargets,
+ TargetFlags,
+ UserProvidedTargetName,
+ writeRspackConfigFile,
+} from '../../utils/generator-utils';
+import { editTsConfig } from '../application/lib/create-ts-config';
+import rspackInitGenerator from '../init/init';
+import { ConfigurationSchema } from './schema';
+
+export async function configurationGenerator(
+ tree: Tree,
+ options: ConfigurationSchema
+) {
+ const task = await rspackInitGenerator(tree, {
+ ...options,
+ // TODO: Crystalize the default rspack.config.js file.
+ // The default setup isn't crystalized so don't add plugin.
+ addPlugin: false,
+ });
+ const { targets, root, projectType } = readProjectConfiguration(
+ tree,
+ options.project
+ );
+
+ const { target, framework } = determineFrameworkAndTarget(
+ tree,
+ options,
+ root,
+ targets
+ );
+ options.framework = framework;
+ options.target = target;
+
+ let foundStylePreprocessorOptions: { includePaths?: string[] } | undefined;
+
+ let buildTargetName = 'build';
+ let serveTargetName = 'serve';
+
+ /**
+ * This is for when we are converting an existing project
+ * to use the vite executors.
+ */
+ let projectAlreadyHasRspackTargets: TargetFlags = {};
+
+ if (!options.newProject) {
+ const userProvidedTargetName: UserProvidedTargetName = {
+ build: options.buildTarget,
+ serve: options.serveTarget,
+ };
+
+ const {
+ validFoundTargetName,
+ projectContainsUnsupportedExecutor,
+ userProvidedTargetIsUnsupported,
+ alreadyHasNxRspackTargets,
+ } = findExistingTargetsInProject(targets, userProvidedTargetName);
+ projectAlreadyHasRspackTargets = alreadyHasNxRspackTargets;
+
+ if (
+ alreadyHasNxRspackTargets.build &&
+ (alreadyHasNxRspackTargets.serve ||
+ projectType === 'library' ||
+ options.framework === 'nest')
+ ) {
+ throw new Error(
+ `The project ${options.project} is already configured to use the @nx/rspack executors.
+ Please try a different project, or remove the existing targets
+ and re-run this generator to reset the existing Rspack Configuration.
+ `
+ );
+ }
+
+ if (!validFoundTargetName.build && projectContainsUnsupportedExecutor) {
+ throw new Error(
+ `The project ${options.project} cannot be converted to use the @nx/rspack executors.`
+ );
+ }
+
+ if (
+ !projectContainsUnsupportedExecutor &&
+ !validFoundTargetName.build &&
+ !validFoundTargetName.serve
+ ) {
+ await handleUnknownExecutors(options.project);
+ }
+
+ await handleUnsupportedUserProvidedTargets(
+ userProvidedTargetIsUnsupported,
+ userProvidedTargetName,
+ validFoundTargetName,
+ options.framework
+ );
+
+ /**
+ * Once the user is at this stage, then they can go ahead and convert.
+ */
+
+ buildTargetName = validFoundTargetName.build ?? buildTargetName;
+ serveTargetName = validFoundTargetName.serve ?? serveTargetName;
+
+ // Not needed atm
+ // if (projectType === 'application' && options.target !== 'node') {
+ // moveAndEditIndexHtml(tree, options, buildTargetName);
+ // }
+
+ foundStylePreprocessorOptions =
+ targets?.[buildTargetName]?.options?.stylePreprocessorOptions;
+
+ deleteWebpackConfig(
+ tree,
+ root,
+ targets?.[buildTargetName]?.options?.webpackConfig
+ );
+
+ editTsConfig(
+ tree,
+ root,
+ options.framework,
+ joinPathFragments(offsetFromRoot(root), 'tsconfig.base.json')
+ );
+ }
+
+ if (!projectAlreadyHasRspackTargets.build) {
+ addOrChangeBuildTarget(tree, options, buildTargetName);
+ }
+
+ if (
+ (options.framework !== 'none' || options.devServer) &&
+ options.framework !== 'nest' &&
+ !projectAlreadyHasRspackTargets.serve
+ ) {
+ addOrChangeServeTarget(tree, options, serveTargetName);
+ }
+ writeRspackConfigFile(tree, options, foundStylePreprocessorOptions);
+ await formatFiles(tree);
+ return task;
+}
+
+export default configurationGenerator;
diff --git a/packages/rspack/src/generators/configuration/schema.d.ts b/packages/rspack/src/generators/configuration/schema.d.ts
new file mode 100644
index 0000000000000..0d4ee469bfea9
--- /dev/null
+++ b/packages/rspack/src/generators/configuration/schema.d.ts
@@ -0,0 +1,12 @@
+import { InitGeneratorSchema } from '../init/schema';
+
+export interface ConfigurationSchema extends InitGeneratorSchema {
+ project: string;
+ main?: string;
+ tsConfig?: string;
+ target?: 'node' | 'web';
+ skipValidation?: boolean;
+ newProject?: boolean;
+ buildTarget?: string;
+ serveTarget?: string;
+}
diff --git a/packages/rspack/src/generators/configuration/schema.json b/packages/rspack/src/generators/configuration/schema.json
new file mode 100644
index 0000000000000..79ec519f70d8d
--- /dev/null
+++ b/packages/rspack/src/generators/configuration/schema.json
@@ -0,0 +1,73 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Rspack",
+ "title": "Nx Rspack Configuration Generator",
+ "description": "Rspack configuration generator.",
+ "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 set up a rspack for?",
+ "x-priority": "important"
+ },
+ "framework": {
+ "type": "string",
+ "description": "The framework used by the project.",
+ "x-prompt": "What framework is the project you want to convert using?",
+ "enum": ["none", "react", "web", "nest"],
+ "alias": ["uiFramework"],
+ "x-priority": "important"
+ },
+ "main": {
+ "type": "string",
+ "description": "Path relative to the workspace root for the main entry file. Defaults to '/src/main.ts'.",
+ "x-priority": "important"
+ },
+ "tsConfig": {
+ "type": "string",
+ "description": "Path relative to the workspace root for the tsconfig file to build with. Defaults to '/tsconfig.app.json'.",
+ "x-priority": "important"
+ },
+ "target": {
+ "type": "string",
+ "description": "Target platform for the build, same as the rspack config option.",
+ "enum": ["node", "web"],
+ "default": "web"
+ },
+ "devServer": {
+ "type": "boolean",
+ "description": "Add a serve target to run a local rspack dev-server",
+ "default": false
+ },
+ "style": {
+ "type": "string",
+ "description": "The style solution to use.",
+ "enum": ["none", "css", "scss", "less"]
+ },
+ "newProject": {
+ "type": "boolean",
+ "description": "Is this a new project?",
+ "default": false,
+ "hidden": true
+ },
+ "buildTarget": {
+ "type": "string",
+ "description": "The build target of the project to be transformed to use the @nx/vite:build executor."
+ },
+ "serveTarget": {
+ "type": "string",
+ "description": "The serve target of the project to be transformed to use the @nx/vite:dev-server and @nx/vite:preview-server executors."
+ },
+ "rootProject": {
+ "type": "boolean",
+ "x-priority": "internal"
+ }
+ },
+ "required": ["project"]
+}
diff --git a/packages/rspack/src/generators/init/init.ts b/packages/rspack/src/generators/init/init.ts
new file mode 100644
index 0000000000000..f32af80d9c696
--- /dev/null
+++ b/packages/rspack/src/generators/init/init.ts
@@ -0,0 +1,113 @@
+import {
+ addDependenciesToPackageJson,
+ convertNxGenerator,
+ createProjectGraphAsync,
+ GeneratorCallback,
+ readNxJson,
+ runTasksInSerial,
+ Tree,
+} from '@nx/devkit';
+import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
+import { initGenerator } from '@nx/js';
+import { createNodesV2 } from '../../../plugin';
+import {
+ lessLoaderVersion,
+ reactRefreshVersion,
+ rspackCoreVersion,
+ rspackDevServerVersion,
+ rspackPluginMinifyVersion,
+ rspackPluginReactRefreshVersion,
+} from '../../utils/versions';
+import { InitGeneratorSchema } from './schema';
+
+export async function rspackInitGenerator(
+ tree: Tree,
+ schema: InitGeneratorSchema
+) {
+ const tasks: GeneratorCallback[] = [];
+
+ const nxJson = readNxJson(tree);
+ const addPluginDefault =
+ process.env.NX_ADD_PLUGINS !== 'false' &&
+ nxJson.useInferencePlugins !== false;
+ schema.addPlugin ??= addPluginDefault;
+
+ if (schema.addPlugin) {
+ await addPlugin(
+ tree,
+ await createProjectGraphAsync(),
+ '@nx/rspack/plugin',
+ createNodesV2,
+ {
+ buildTargetName: [
+ 'build',
+ 'rspack:build',
+ 'build:rspack',
+ 'rspack-build',
+ 'build-rspack',
+ ],
+ serveTargetName: [
+ 'serve',
+ 'rspack:serve',
+ 'serve:rspack',
+ 'rspack-serve',
+ 'serve-rspack',
+ ],
+ previewTargetName: [
+ 'preview',
+ 'rspack:preview',
+ 'preview:rspack',
+ 'rspack-preview',
+ 'preview-rspack',
+ ],
+ },
+ schema.updatePackageScripts
+ );
+ }
+
+ const jsInitTask = await initGenerator(tree, {
+ ...schema,
+ tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json',
+ skipFormat: true,
+ });
+
+ tasks.push(jsInitTask);
+
+ const devDependencies = {
+ '@rspack/core': rspackCoreVersion,
+ '@rspack/cli': rspackCoreVersion,
+ '@rspack/plugin-minify': rspackPluginMinifyVersion,
+ '@rspack/plugin-react-refresh': rspackPluginReactRefreshVersion,
+ 'react-refresh': reactRefreshVersion,
+ };
+
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const version = require('../../../package.json').version;
+ if (version !== '0.0.1') {
+ // Ignored for local dev / e2e tests.
+ devDependencies['@nx/rspack'] = version;
+ }
+
+ if (schema.style === 'less') {
+ devDependencies['less-loader'] = lessLoaderVersion;
+ }
+
+ if (schema.framework !== 'none' || schema.devServer) {
+ devDependencies['@rspack/dev-server'] = rspackDevServerVersion;
+ }
+
+ const installTask = addDependenciesToPackageJson(
+ tree,
+ {},
+ devDependencies,
+ undefined,
+ schema.keepExistingVersions
+ );
+ tasks.push(installTask);
+
+ return runTasksInSerial(...tasks);
+}
+
+export default rspackInitGenerator;
+
+export const rspackInitSchematic = convertNxGenerator(rspackInitGenerator);
diff --git a/packages/rspack/src/generators/init/schema.d.ts b/packages/rspack/src/generators/init/schema.d.ts
new file mode 100644
index 0000000000000..cd48a7882b497
--- /dev/null
+++ b/packages/rspack/src/generators/init/schema.d.ts
@@ -0,0 +1,11 @@
+export type Framework = 'none' | 'react' | 'web' | 'nest';
+
+export interface InitGeneratorSchema {
+ addPlugin?: boolean;
+ devServer?: boolean;
+ framework?: Framework;
+ keepExistingVersions?: boolean;
+ rootProject?: boolean;
+ style?: 'none' | 'css' | 'scss' | 'less' | 'styl';
+ updatePackageScripts?: boolean;
+}
diff --git a/packages/rspack/src/generators/init/schema.json b/packages/rspack/src/generators/init/schema.json
new file mode 100644
index 0000000000000..5242ea96dfc67
--- /dev/null
+++ b/packages/rspack/src/generators/init/schema.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Init",
+ "title": "Nx Rspack Init Generator",
+ "type": "object",
+ "description": "Rspack init generator.",
+ "properties": {
+ "framework": {
+ "type": "string",
+ "description": "The UI framework used by the project.",
+ "enum": ["none", "react", "web", "nest"],
+ "alias": ["uiFramework"]
+ },
+ "style": {
+ "type": "string",
+ "description": "The style solution to use.",
+ "enum": ["none", "css", "scss", "less", "styl"]
+ },
+ "rootProject": {
+ "type": "boolean",
+ "x-priority": "internal"
+ },
+ "keepExistingVersions": {
+ "type": "boolean",
+ "x-priority": "internal",
+ "description": "Keep existing dependencies versions",
+ "default": false
+ }
+ },
+ "required": []
+}
diff --git a/packages/rspack/src/generators/preset/preset.ts b/packages/rspack/src/generators/preset/preset.ts
new file mode 100644
index 0000000000000..69bec32baa5c0
--- /dev/null
+++ b/packages/rspack/src/generators/preset/preset.ts
@@ -0,0 +1,36 @@
+import { Tree, updateJson } from '@nx/devkit';
+
+import applicationGenerator from '../application/application';
+import { PresetGeneratorSchema } from './schema';
+
+export default async function (tree: Tree, options: PresetGeneratorSchema) {
+ const appTask = applicationGenerator(tree, {
+ ...options,
+ // Since `--style` is not passed down to custom preset, we're using individual flags for now.
+ style: options.sass
+ ? 'scss'
+ : options.less
+ ? 'less'
+ : options.stylus
+ ? 'styl'
+ : 'css',
+ });
+
+ updateJson(tree, 'package.json', (json) => {
+ json.scripts ??= {};
+ json.scripts.build ??= 'npx nx build';
+ json.scripts.start ??= 'npx nx serve';
+ json.scripts.lint ??= 'npx nx lint';
+ json.scripts.test ??= 'npx nx test';
+ json.scripts.e2e ??= 'npx nx e2e e2e';
+ return json;
+ });
+
+ if (options.rootProject) {
+ // Remove these folders so projects will be generated at the root.
+ tree.delete('apps');
+ tree.delete('libs');
+ }
+
+ return appTask;
+}
diff --git a/packages/rspack/src/generators/preset/schema.d.ts b/packages/rspack/src/generators/preset/schema.d.ts
new file mode 100644
index 0000000000000..f06b33bced746
--- /dev/null
+++ b/packages/rspack/src/generators/preset/schema.d.ts
@@ -0,0 +1,18 @@
+export interface PresetGeneratorSchema {
+ name: string;
+ framework?: Framework;
+ less?: boolean;
+ sass?: boolean;
+ stylus?: boolean;
+ unitTestRunner?: 'none' | 'jest';
+ e2eTestRunner?: 'none' | 'cypress';
+ directory?: string;
+ tags?: string;
+ rootProject?: boolean;
+ monorepo?: boolean;
+}
+
+export interface NormalizedSchema extends PresetGeneratorSchema {
+ appProjectRoot: string;
+ e2eProjectName: string;
+}
diff --git a/packages/rspack/src/generators/preset/schema.json b/packages/rspack/src/generators/preset/schema.json
new file mode 100644
index 0000000000000..f9216a4fccbe7
--- /dev/null
+++ b/packages/rspack/src/generators/preset/schema.json
@@ -0,0 +1,71 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "Preset",
+ "title": "Standalone React and rspack preset",
+ "description": "React + Rspack preset generator.",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "",
+ "$default": {
+ "$source": "argv",
+ "index": 0
+ },
+ "x-priority": "important"
+ },
+ "framework": {
+ "type": "string",
+ "description": "The framework to use for the application.",
+ "enum": ["none", "react", "web", "nest"],
+ "alias": ["uiFramework"],
+ "x-priority": "important",
+ "default": "react"
+ },
+ "less": {
+ "type": "boolean",
+ "description": "Use less for styling."
+ },
+ "sass": {
+ "type": "boolean",
+ "description": "Use sass for styling."
+ },
+ "stylus": {
+ "type": "boolean",
+ "description": "Use stylus for styling."
+ },
+ "unitTestRunner": {
+ "type": "string",
+ "description": "The unit test runner to use.",
+ "enum": ["none", "jest"],
+ "default": "jest"
+ },
+ "e2eTestRunner": {
+ "type": "string",
+ "description": "The e2e test runner to use.",
+ "enum": ["none", "cypress"],
+ "default": "cypress"
+ },
+ "directory": {
+ "type": "string",
+ "description": "The directory to nest the app under."
+ },
+ "tags": {
+ "type": "string",
+ "description": "Add tags to the project (used for linting).",
+ "alias": "t"
+ },
+ "monorepo": {
+ "type": "boolean",
+ "description": "Creates an integrated monorepo.",
+ "default": false,
+ "aliases": ["integrated"]
+ },
+ "rootProject": {
+ "type": "boolean",
+ "x-priority": "internal",
+ "default": true
+ }
+ },
+ "required": ["name"]
+}
diff --git a/packages/rspack/src/index.ts b/packages/rspack/src/index.ts
new file mode 100644
index 0000000000000..10a9306018e7d
--- /dev/null
+++ b/packages/rspack/src/index.ts
@@ -0,0 +1,6 @@
+export * from './generators/configuration/configuration';
+export * from './generators/init/init';
+export * from './utils/config';
+export * from './utils/with-nx';
+export * from './utils/with-react';
+export * from './utils/with-web';
diff --git a/packages/rspack/src/migrations/update-16-0-0-add-nx-packages/update-16-0-0-add-nx-packages.spec.ts b/packages/rspack/src/migrations/update-16-0-0-add-nx-packages/update-16-0-0-add-nx-packages.spec.ts
new file mode 100644
index 0000000000000..8fc2324fb4e19
--- /dev/null
+++ b/packages/rspack/src/migrations/update-16-0-0-add-nx-packages/update-16-0-0-add-nx-packages.spec.ts
@@ -0,0 +1,37 @@
+import { readJson, Tree, updateJson } from '@nx/devkit';
+import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
+import replacePackage from './update-16-0-0-add-nx-packages';
+
+describe('update-16-0-0-add-nx-packages', () => {
+ let tree: Tree;
+ beforeEach(() => {
+ tree = createTreeWithEmptyWorkspace();
+
+ updateJson(tree, 'package.json', (json) => {
+ json.devDependencies['@nrwl/rspack'] = '16.0.0';
+ return json;
+ });
+ });
+
+ it('should remove the dependency on @nrwl/rspack', async () => {
+ await replacePackage(tree);
+
+ expect(
+ readJson(tree, 'package.json').dependencies['@nrwl/rspack']
+ ).not.toBeDefined();
+ expect(
+ readJson(tree, 'package.json').devDependencies['@nrwl/rspack']
+ ).not.toBeDefined();
+ });
+
+ it('should add a dependency on @nx/rspack', async () => {
+ await replacePackage(tree);
+
+ const packageJson = readJson(tree, 'package.json');
+ const newDependencyVersion =
+ packageJson.devDependencies['@nx/rspack'] ??
+ packageJson.dependencies['@nx/rspack'];
+
+ expect(newDependencyVersion).toBeDefined();
+ });
+});
diff --git a/packages/rspack/src/migrations/update-16-0-0-add-nx-packages/update-16-0-0-add-nx-packages.ts b/packages/rspack/src/migrations/update-16-0-0-add-nx-packages/update-16-0-0-add-nx-packages.ts
new file mode 100644
index 0000000000000..bf44165b3c4d4
--- /dev/null
+++ b/packages/rspack/src/migrations/update-16-0-0-add-nx-packages/update-16-0-0-add-nx-packages.ts
@@ -0,0 +1,8 @@
+import { formatFiles, Tree } from '@nx/devkit';
+import { replaceNrwlPackageWithNxPackage } from '@nx/devkit/src/utils/replace-package';
+
+export default async function replacePackage(tree: Tree): Promise {
+ await replaceNrwlPackageWithNxPackage(tree, '@nrwl/rspack', '@nx/rspack');
+
+ await formatFiles(tree);
+}
diff --git a/packages/rspack/src/plugins/generate-package-json-plugin.ts b/packages/rspack/src/plugins/generate-package-json-plugin.ts
new file mode 100644
index 0000000000000..51454264db292
--- /dev/null
+++ b/packages/rspack/src/plugins/generate-package-json-plugin.ts
@@ -0,0 +1,87 @@
+import {
+ ExecutorContext,
+ detectPackageManager,
+ serializeJson,
+ type ProjectGraph,
+} from '@nx/devkit';
+import {
+ HelperDependency,
+ createLockFile,
+ createPackageJson,
+ getHelperDependenciesFromProjectGraph,
+ getLockFileName,
+ readTsConfig,
+} from '@nx/js';
+import { type Compiler, type RspackPluginInstance } from '@rspack/core';
+import { RawSource } from 'webpack-sources';
+
+const pluginName = 'GeneratePackageJsonPlugin';
+
+export class GeneratePackageJsonPlugin implements RspackPluginInstance {
+ private readonly projectGraph: ProjectGraph;
+
+ constructor(
+ private readonly options: { tsConfig: string; outputFileName: string },
+ private readonly context: ExecutorContext
+ ) {
+ this.projectGraph = context.projectGraph;
+ }
+
+ apply(compiler: Compiler): void {
+ compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
+ compilation.hooks.processAssets.tap(
+ {
+ name: pluginName,
+ stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
+ },
+ () => {
+ const helperDependencies = getHelperDependenciesFromProjectGraph(
+ this.context.root,
+ this.context.projectName,
+ this.projectGraph
+ );
+
+ const importHelpers = !!readTsConfig(this.options.tsConfig).options
+ .importHelpers;
+ const shouldAddHelperDependency =
+ importHelpers &&
+ helperDependencies.every(
+ (dep) => dep.target !== HelperDependency.tsc
+ );
+
+ if (shouldAddHelperDependency) {
+ helperDependencies.push({
+ type: 'static',
+ source: this.context.projectName,
+ target: HelperDependency.tsc,
+ });
+ }
+
+ const packageJson = createPackageJson(
+ this.context.projectName,
+ this.projectGraph,
+ {
+ target: this.context.targetName,
+ root: this.context.root,
+ isProduction: true,
+ helperDependencies: helperDependencies.map((dep) => dep.target),
+ }
+ );
+ packageJson.main = packageJson.main ?? this.options.outputFileName;
+
+ compilation.emitAsset(
+ 'package.json',
+ new RawSource(serializeJson(packageJson))
+ );
+ const packageManager = detectPackageManager(this.context.root);
+ compilation.emitAsset(
+ getLockFileName(packageManager),
+ new RawSource(
+ createLockFile(packageJson, this.projectGraph, packageManager)
+ )
+ );
+ }
+ );
+ });
+ }
+}
diff --git a/packages/rspack/src/plugins/plugin.ts b/packages/rspack/src/plugins/plugin.ts
new file mode 100644
index 0000000000000..1e05fdaafeee2
--- /dev/null
+++ b/packages/rspack/src/plugins/plugin.ts
@@ -0,0 +1,231 @@
+import {
+ CreateDependencies,
+ CreateNodesContext,
+ createNodesFromFiles,
+ CreateNodesV2,
+ detectPackageManager,
+ ProjectConfiguration,
+ readJsonFile,
+ workspaceRoot,
+ writeJsonFile,
+} from '@nx/devkit';
+import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
+import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
+import { getLockFileName, getRootTsConfigPath } from '@nx/js';
+import { existsSync, readdirSync } from 'fs';
+import { hashObject } from 'nx/src/hasher/file-hasher';
+import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
+import { dirname, isAbsolute, join, relative, resolve } from 'path';
+import { readRspackOptions } from '../utils/read-rspack-options';
+import { resolveUserDefinedRspackConfig } from '../utils/resolve-user-defined-rspack-config';
+
+export interface RspackPluginOptions {
+ buildTargetName?: string;
+ serveTargetName?: string;
+ serveStaticTargetName?: string;
+ previewTargetName?: string;
+}
+
+type RspackTargets = Pick;
+
+function readTargetsCache(cachePath: string): Record {
+ return existsSync(cachePath) ? readJsonFile(cachePath) : {};
+}
+
+function writeTargetsToCache(
+ cachePath,
+ results?: Record
+) {
+ writeJsonFile(cachePath, results);
+}
+
+export const createDependencies: CreateDependencies = () => {
+ return [];
+};
+
+const rspackConfigGlob = '**/rspack.config.{js,ts,mjs,mts,cjs,cts}';
+
+export const createNodesV2: CreateNodesV2 = [
+ rspackConfigGlob,
+ async (configFilePaths, options, context) => {
+ const optionsHash = hashObject(options);
+ const cachePath = join(
+ workspaceDataDirectory,
+ `rspack-${optionsHash}.hash`
+ );
+ const targetsCache = readTargetsCache(cachePath);
+ try {
+ return await createNodesFromFiles(
+ (configFile, options, context) =>
+ createNodesInternal(configFile, options, context, targetsCache),
+ configFilePaths,
+ options,
+ context
+ );
+ } finally {
+ writeTargetsToCache(cachePath, targetsCache);
+ }
+ },
+];
+
+async function createNodesInternal(
+ configFilePath: string,
+ options: RspackPluginOptions,
+ context: CreateNodesContext,
+ targetsCache: Record
+) {
+ const projectRoot = dirname(configFilePath);
+ // Do not create a project if package.json and project.json isn't there.
+ const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
+ if (
+ !siblingFiles.includes('package.json') &&
+ !siblingFiles.includes('project.json')
+ ) {
+ return {};
+ }
+
+ const normalizedOptions = normalizeOptions(options);
+
+ // We do not want to alter how the hash is calculated, so appending the config file path to the hash
+ // to prevent vite/vitest files overwriting the target cache created by the other
+ const hash =
+ (await calculateHashForCreateNodes(
+ projectRoot,
+ normalizedOptions,
+ context,
+ [getLockFileName(detectPackageManager(context.workspaceRoot))]
+ )) + configFilePath;
+
+ targetsCache[hash] ??= await createRspackTargets(
+ configFilePath,
+ projectRoot,
+ normalizedOptions,
+ context
+ );
+
+ const { targets, metadata } = targetsCache[hash];
+
+ return {
+ projects: {
+ [projectRoot]: {
+ root: projectRoot,
+ targets,
+ metadata,
+ },
+ },
+ };
+}
+
+async function createRspackTargets(
+ configFilePath: string,
+ projectRoot: string,
+ options: RspackPluginOptions,
+ context: CreateNodesContext
+): Promise {
+ const namedInputs = getNamedInputs(projectRoot, context);
+
+ const rspackConfig = resolveUserDefinedRspackConfig(
+ join(context.workspaceRoot, configFilePath),
+ getRootTsConfigPath(),
+ true
+ );
+
+ const rspackOptions = await readRspackOptions(rspackConfig);
+
+ const outputPath = normalizeOutputPath(
+ rspackOptions.output?.path,
+ projectRoot
+ );
+
+ const targets = {};
+
+ targets[options.buildTargetName] = {
+ command: `rspack build`,
+ options: { cwd: projectRoot, args: ['--node-env=production'] },
+ cache: true,
+ dependsOn: [`^${options.buildTargetName}`],
+ inputs:
+ 'production' in namedInputs
+ ? [
+ 'production',
+ '^production',
+ {
+ externalDependencies: ['@rspack/cli'],
+ },
+ ]
+ : [
+ 'default',
+ '^default',
+ {
+ externalDependencies: ['@rspack/cli'],
+ },
+ ],
+ outputs: [outputPath],
+ };
+
+ targets[options.serveTargetName] = {
+ command: `rspack serve`,
+ options: {
+ cwd: projectRoot,
+ args: ['--node-env=development'],
+ },
+ };
+
+ targets[options.previewTargetName] = {
+ command: `rspack serve`,
+ options: {
+ cwd: projectRoot,
+ args: ['--node-env=production'],
+ },
+ };
+
+ targets[options.serveStaticTargetName] = {
+ executor: '@nx/web:file-server',
+ options: {
+ buildTarget: options.buildTargetName,
+ spa: true,
+ },
+ };
+
+ return { targets, metadata: {} };
+}
+
+function normalizeOptions(options: RspackPluginOptions): RspackPluginOptions {
+ options ??= {};
+ options.buildTargetName ??= 'build';
+ options.serveTargetName ??= 'serve';
+ options.previewTargetName ??= 'preview';
+ options.serveStaticTargetName ??= 'serve-static';
+ return options;
+}
+
+function normalizeOutputPath(
+ outputPath: string | undefined,
+ projectRoot: string
+): string | undefined {
+ if (!outputPath) {
+ // If outputPath is undefined, use rspack's default `dist` directory.
+ if (projectRoot === '.') {
+ return `{projectRoot}/dist`;
+ } else {
+ return `{workspaceRoot}/dist/{projectRoot}`;
+ }
+ } else {
+ if (isAbsolute(outputPath)) {
+ /**
+ * If outputPath is absolute, we need to resolve it relative to the workspaceRoot first.
+ * After that, we can use the relative path to the workspaceRoot token {workspaceRoot} to generate the output path.
+ */
+ return `{workspaceRoot}/${relative(
+ workspaceRoot,
+ resolve(workspaceRoot, outputPath)
+ )}`;
+ } else {
+ if (outputPath.startsWith('..')) {
+ return join('{workspaceRoot}', join(projectRoot, outputPath));
+ } else {
+ return join('{projectRoot}', outputPath);
+ }
+ }
+ }
+}
diff --git a/packages/rspack/src/utils/config.ts b/packages/rspack/src/utils/config.ts
new file mode 100644
index 0000000000000..848d2b56a7300
--- /dev/null
+++ b/packages/rspack/src/utils/config.ts
@@ -0,0 +1,49 @@
+import type { ExecutorContext } from '@nx/devkit';
+import type { Configuration } from '@rspack/core';
+
+import { SharedConfigContext } from './model';
+
+export const nxRspackComposablePlugin = 'nxRspackComposablePlugin';
+
+export function isNxRspackComposablePlugin(
+ a: unknown
+): a is AsyncNxComposableRspackPlugin {
+ return a?.[nxRspackComposablePlugin] === true;
+}
+
+export interface NxRspackExecutionContext {
+ options: unknown;
+ context: ExecutorContext;
+ configuration?: string;
+}
+
+export interface NxComposableRspackPlugin {
+ (config: Configuration, ctx: NxRspackExecutionContext): Configuration;
+}
+
+export interface AsyncNxComposableRspackPlugin {
+ (config: Configuration, ctx: NxRspackExecutionContext):
+ | Configuration
+ | Promise;
+}
+
+export function composePlugins(...plugins: any[]) {
+ return Object.defineProperty(
+ async function combined(
+ config: Configuration,
+ ctx: SharedConfigContext
+ ): Promise {
+ for (const plugin of plugins) {
+ const fn = await plugin;
+ config = await fn(config, ctx);
+ }
+ return config;
+ },
+ nxRspackComposablePlugin,
+ {
+ value: true,
+ enumerable: false,
+ writable: false,
+ }
+ );
+}
diff --git a/packages/rspack/src/utils/create-compiler.ts b/packages/rspack/src/utils/create-compiler.ts
new file mode 100644
index 0000000000000..cbe05992d0315
--- /dev/null
+++ b/packages/rspack/src/utils/create-compiler.ts
@@ -0,0 +1,51 @@
+import { ExecutorContext } from '@nx/devkit';
+import {
+ Compiler,
+ type Configuration,
+ MultiCompiler,
+ rspack,
+} from '@rspack/core';
+import * as path from 'path';
+import { RspackExecutorSchema } from '../executors/rspack/schema';
+import { resolveUserDefinedRspackConfig } from './resolve-user-defined-rspack-config';
+
+export async function createCompiler(
+ options: RspackExecutorSchema & {
+ devServer?: any;
+ },
+ context: ExecutorContext
+): Promise {
+ const pathToConfig = path.join(context.root, options.rspackConfig);
+ let userDefinedConfig: any = {};
+ if (options.tsConfig) {
+ userDefinedConfig = resolveUserDefinedRspackConfig(
+ pathToConfig,
+ options.tsConfig
+ );
+ } else {
+ userDefinedConfig = await import(pathToConfig).then((x) => x.default || x);
+ }
+
+ if (typeof userDefinedConfig.then === 'function') {
+ userDefinedConfig = await userDefinedConfig;
+ }
+
+ let config: Configuration = {};
+ if (typeof userDefinedConfig === 'function') {
+ config = await userDefinedConfig(
+ { devServer: options.devServer },
+ { options, context }
+ );
+ } else {
+ config = userDefinedConfig;
+ config.devServer ??= options.devServer;
+ }
+
+ return rspack(config);
+}
+
+export function isMultiCompiler(
+ compiler: Compiler | MultiCompiler
+): compiler is MultiCompiler {
+ return 'compilers' in compiler;
+}
diff --git a/packages/rspack/src/utils/generator-utils.ts b/packages/rspack/src/utils/generator-utils.ts
new file mode 100644
index 0000000000000..a89a4d5e9cfd8
--- /dev/null
+++ b/packages/rspack/src/utils/generator-utils.ts
@@ -0,0 +1,619 @@
+import {
+ joinPathFragments,
+ logger,
+ readProjectConfiguration,
+ TargetConfiguration,
+ Tree,
+ updateProjectConfiguration,
+} from '@nx/devkit';
+import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
+import { RspackExecutorSchema } from '../executors/rspack/schema';
+import { ConfigurationSchema } from '../generators/configuration/schema';
+import { Framework } from '../generators/init/schema';
+
+export type Target = 'build' | 'serve';
+export type TargetFlags = Partial>;
+export type UserProvidedTargetName = Partial>;
+export type ValidFoundTargetName = Partial>;
+
+export function findExistingTargetsInProject(
+ targets: {
+ [targetName: string]: TargetConfiguration;
+ },
+ userProvidedTargets?: UserProvidedTargetName
+): {
+ validFoundTargetName: ValidFoundTargetName;
+ projectContainsUnsupportedExecutor: boolean;
+ userProvidedTargetIsUnsupported: TargetFlags;
+ alreadyHasNxRspackTargets: TargetFlags;
+} {
+ const output: ReturnType = {
+ validFoundTargetName: {},
+ projectContainsUnsupportedExecutor: false,
+ userProvidedTargetIsUnsupported: {},
+ alreadyHasNxRspackTargets: {},
+ };
+
+ const supportedExecutors = {
+ build: [
+ '@nxext/vite:build',
+ '@nrwl/webpack:webpack',
+ '@nrwl/rollup:rollup',
+ '@nrwl/web:rollup',
+ '@nrwl/vite:build',
+ '@nx/webpack:webpack',
+ '@nx/rollup:rollup',
+ '@nx/web:rollup',
+ '@nx/vite:build',
+ ],
+ serve: [
+ '@nxext/vite:dev',
+ '@nrwl/webpack:dev-server',
+ '@nrwl/vite:dev-server',
+ '@nx/webpack:dev-server',
+ '@nx/vite:dev-server',
+ ],
+ };
+
+ const unsupportedExecutors = [
+ '@nx/js:babel',
+ '@nx/js:node',
+ '@nx/js:swc',
+ '@nx/react-native:run-ios',
+ '@nx/react-native:start',
+ '@nx/react-native:run-android',
+ '@nx/react-native:bundle',
+ '@nx/react-native:build-android',
+ '@nx/react-native:bundle',
+ '@nx/next:build',
+ '@nx/next:server',
+ '@nx/js:tsc',
+ '@nx/angular:ng-packagr-lite',
+ '@nx/angular:package',
+ '@nx/angular:webpack-browser',
+ '@nx/esbuild:esbuild',
+ '@nrwl/js:babel',
+ '@nrwl/js:node',
+ '@nrwl/js:swc',
+ '@nrwl/react-native:run-ios',
+ '@nrwl/react-native:start',
+ '@nrwl/react-native:run-android',
+ '@nrwl/react-native:bundle',
+ '@nrwl/react-native:build-android',
+ '@nrwl/react-native:bundle',
+ '@nrwl/next:build',
+ '@nrwl/next:server',
+ '@nrwl/js:tsc',
+ '@nrwl/angular:ng-packagr-lite',
+ '@nrwl/angular:package',
+ '@nrwl/angular:webpack-browser',
+ '@nrwl/esbuild:esbuild',
+ '@angular-devkit/build-angular:browser',
+ '@angular-devkit/build-angular:dev-server',
+ ];
+
+ // First, we check if the user has provided a target
+ // If they have, we check if the executor the target is using is supported
+ // If it's not supported, then we set the unsupported flag to true for that target
+
+ function checkUserProvidedTarget(target: Target) {
+ if (userProvidedTargets?.[target]) {
+ if (
+ supportedExecutors[target].includes(
+ targets[userProvidedTargets[target]]?.executor
+ )
+ ) {
+ output.validFoundTargetName[target] = userProvidedTargets[target];
+ } else {
+ output.userProvidedTargetIsUnsupported[target] = true;
+ }
+ }
+ }
+
+ checkUserProvidedTarget('build');
+ checkUserProvidedTarget('serve');
+
+ // Returns early when we have a build, serve, and test targets.
+ if (output.validFoundTargetName.build && output.validFoundTargetName.serve) {
+ return output;
+ }
+
+ // We try to find the targets that are using the supported executors
+ // for build, serve and test, since these are the ones we will be converting
+ for (const target in targets) {
+ const executorName = targets[target].executor;
+
+ const hasRspackTargets = output.alreadyHasNxRspackTargets;
+ hasRspackTargets.build ||= executorName === '@nx/rspack:rspack';
+ hasRspackTargets.serve ||= executorName === '@nx/rspack:dev-server';
+
+ const foundTargets = output.validFoundTargetName;
+ if (
+ !foundTargets.build &&
+ supportedExecutors.build.includes(executorName)
+ ) {
+ foundTargets.build = target;
+ }
+ if (
+ !foundTargets.serve &&
+ supportedExecutors.serve.includes(executorName)
+ ) {
+ foundTargets.serve = target;
+ }
+
+ output.projectContainsUnsupportedExecutor ||=
+ unsupportedExecutors.includes(executorName);
+ }
+
+ return output;
+}
+
+export function addOrChangeBuildTarget(
+ tree: Tree,
+ options: ConfigurationSchema,
+ target: string
+) {
+ const project = readProjectConfiguration(tree, options.project);
+ const assets = [];
+ if (
+ options.target === 'web' &&
+ tree.exists(joinPathFragments(project.root, 'src/favicon.ico'))
+ ) {
+ assets.push(joinPathFragments(project.root, 'src/favicon.ico'));
+ }
+ if (tree.exists(joinPathFragments(project.root, 'src/assets'))) {
+ assets.push(joinPathFragments(project.root, 'src/assets'));
+ }
+
+ const buildOptions: RspackExecutorSchema = {
+ target: options.target ?? 'web',
+ outputPath: joinPathFragments(
+ 'dist',
+ // If standalone project then use the project's name in dist.
+ project.root === '.' ? project.name : project.root
+ ),
+ main: determineMain(tree, options),
+ tsConfig: determineTsConfig(tree, options),
+ rspackConfig: joinPathFragments(project.root, 'rspack.config.js'),
+ assets,
+ };
+
+ project.targets ??= {};
+
+ project.targets[target] = {
+ executor: '@nx/rspack:rspack',
+ outputs: ['{options.outputPath}'],
+ defaultConfiguration: 'production',
+ options: buildOptions,
+ configurations: {
+ development: {
+ mode: 'development',
+ },
+ production: {
+ mode: 'production',
+ optimization: options.target === 'web' ? true : undefined,
+ sourceMap: false,
+ },
+ },
+ };
+
+ updateProjectConfiguration(tree, options.project, project);
+}
+
+export function addOrChangeServeTarget(
+ tree: Tree,
+ options: ConfigurationSchema,
+ target: string
+) {
+ const project = readProjectConfiguration(tree, options.project);
+
+ project.targets ??= {};
+
+ project.targets[target] = {
+ executor: '@nx/rspack:dev-server',
+ options: {
+ buildTarget: `${options.project}:build:development`,
+ },
+ configurations: {
+ development: {},
+ production: {
+ buildTarget: `${options.project}:build:production`,
+ },
+ },
+ };
+
+ updateProjectConfiguration(tree, options.project, project);
+}
+
+export function writeRspackConfigFile(
+ tree: Tree,
+ options: ConfigurationSchema,
+ stylePreprocessorOptions?: { includePaths?: string[] }
+) {
+ const project = readProjectConfiguration(tree, options.project);
+
+ tree.write(
+ joinPathFragments(project.root, 'rspack.config.js'),
+ createConfig(options, stylePreprocessorOptions)
+ );
+}
+
+function createConfig(
+ options: ConfigurationSchema,
+ stylePreprocessorOptions?: { includePaths?: string[] }
+) {
+ if (options.framework === 'react') {
+ return `
+ const { composePlugins, withNx, withReact } = require('@nx/rspack');
+
+ module.exports = composePlugins(withNx(), withReact(${
+ stylePreprocessorOptions
+ ? `
+ {
+ stylePreprocessorOptions: ${JSON.stringify(stylePreprocessorOptions)},
+ }
+ `
+ : ''
+ }), (config) => {
+ return config;
+ });
+ `;
+ } else if (options.framework === 'web' || options.target === 'web') {
+ return `
+ const { composePlugins, withNx, withWeb } = require('@nx/rspack');
+
+ module.exports = composePlugins(withNx(), withWeb(${
+ stylePreprocessorOptions
+ ? `
+ {
+ stylePreprocessorOptions: ${JSON.stringify(stylePreprocessorOptions)},
+ }
+ `
+ : ''
+ }), (config) => {
+ return config;
+ });
+ `;
+ } else if (options.framework === 'nest') {
+ return `
+ const { composePlugins, withNx } = require('@nx/rspack');
+
+ module.exports = composePlugins(withNx(), (config) => {
+ return config;
+ });
+ `;
+ } else {
+ return `
+ const { composePlugins, withNx${
+ stylePreprocessorOptions ? ', withWeb' : ''
+ } } = require('@nx/rspack');
+
+ module.exports = composePlugins(withNx()${
+ stylePreprocessorOptions
+ ? `,
+ withWeb({
+ stylePreprocessorOptions: ${JSON.stringify(stylePreprocessorOptions)},
+ })`
+ : ''
+ }, (config) => {
+ return config;
+ });
+ `;
+ }
+}
+
+export function deleteWebpackConfig(
+ tree: Tree,
+ projectRoot: string,
+ webpackConfigFilePath?: string
+) {
+ const webpackConfigPath =
+ webpackConfigFilePath && tree.exists(webpackConfigFilePath)
+ ? webpackConfigFilePath
+ : tree.exists(`${projectRoot}/webpack.config.js`)
+ ? `${projectRoot}/webpack.config.js`
+ : tree.exists(`${projectRoot}/webpack.config.ts`)
+ ? `${projectRoot}/webpack.config.ts`
+ : null;
+ if (webpackConfigPath) {
+ tree.delete(webpackConfigPath);
+ }
+}
+
+// Maybe add delete vite config?
+
+export function moveAndEditIndexHtml(
+ tree: Tree,
+ options: ConfigurationSchema,
+ buildTarget: string
+) {
+ const projectConfig = readProjectConfiguration(tree, options.project);
+
+ let indexHtmlPath =
+ projectConfig.targets?.[buildTarget]?.options?.index ??
+ `${projectConfig.root}/src/index.html`;
+ let mainPath =
+ projectConfig.targets?.[buildTarget]?.options?.main ??
+ `${projectConfig.root}/src/main.ts${
+ options.framework === 'react' ? 'x' : ''
+ }`;
+
+ if (projectConfig.root !== '.') {
+ mainPath = mainPath.replace(projectConfig.root, '');
+ }
+
+ if (
+ !tree.exists(indexHtmlPath) &&
+ tree.exists(`${projectConfig.root}/index.html`)
+ ) {
+ indexHtmlPath = `${projectConfig.root}/index.html`;
+ }
+
+ if (tree.exists(indexHtmlPath)) {
+ const indexHtmlContent = tree.read(indexHtmlPath, 'utf8');
+ if (
+ !indexHtmlContent.includes(
+ ``
+ )
+ ) {
+ tree.write(
+ `${projectConfig.root}/index.html`,
+ indexHtmlContent.replace(
+ '
+
+
+ ',
+ `
+ `
+ )
+ );
+
+ if (tree.exists(`${projectConfig.root}/src/index.html`)) {
+ tree.delete(`${projectConfig.root}/src/index.html`);
+ }
+ }
+ } else {
+ tree.write(
+ `${projectConfig.root}/index.html`,
+ `
+
+