feat(rspack): move rspack into main nx repo (#27969)

- feat: add rspack plugin (#143)
- feat: add rspack plugin (#143)
- feat(rspack): update to latest rspack version (#159)
- feat(rspack): add missing features (less/sass/stylus, assets, etc.)
(#160)
- feat(rspack): add missing features (less/sass/stylus, assets, etc.)
(#160)
- feat(rspack): clean-up project setup (#161)
- feat(rspack): clean-up project setup (#161)
- fix(rspack): use correct app dir when generating non-root projects
(#162)
- fix(rspack): use correct app dir when generating non-root projects
(#162)
- chore(repo): migrate to Nx 15.8.5 (#169)
- feat(rspack): update and pin rspack to 0.1.0 (#173)
- fix(rspack): fix rspack build
- chore(rspack): remove comment (#175)
- feat(rspack): set mode in configuration and expose option (#177)
- fix(rspack): handle existing stylePreprocessorOptions (#182)
- fix(rspack): add dependency to ajv-keywords that match the version
used by rspack (#187)
- fix(rspack): pass devServer options to devServer (#193)
- fix(rspack): set externals for target node (#194)
- feat(rspack): install latest patch when configuring (#195)
- fix(rspack): add withWeb if web app (#200)
- chore(repo): fix release script (#202)
- chore(repo): fix release script (#202)
- feat(rspack): configuration generator better ux (#201)
- feat(rspack): builder returns outfile (#207)
- fix(rspack): use ensureTypescript before tsquery (#215)
- feat(rspack): simplify app generator (#212)
- feat(rspack): simplify app generator (#212)
- fix(rspack): implement watch mode (#217)
- fix(rspack): do not force cssmodules (#222)
- fix(rspack): use builtin minify instead (#172)
- fix(rspack): use built-in tsconfig paths support (#227)
- fix(rspack): add back `resolve.alias` configuration since
`resolve.tsConfigPaths` seem to be incorrect in some scenarios (#229)
- feat(misc): update to Nx 16 and rescoped packages (#235)
- feat(misc): update to Nx 16 and rescoped packages (#235)
- fix(misc): replace missed references to @nrwl scope (#239)
- chore(repo): add legacy packages for nx rescope (#238)
- chore(repo): add legacy packages for nx rescope (#238)
- fix(repo): fix publishing for legacy packages (#240)
- fix(repo): fix publishing for legacy packages (#240)
- fix(misc): target commonjs for legacy packages (#241)
- fix(repo): add json files to assets (#243)
- chore(repo): update to 16.0.3 (#244)
- chore(repo): update to nx 16.2.1 (#271)
- fix(rspack): lock version to 0.1.11 (#279)
- fix(rspack): refine output filename patterns (#280)
- chore(rspack): update to latest (#278)
- feat(rspack): Add extractLicenses option to rspack's project
configuration (#230)
- feat(rspack): Add extractLicenses option to rspack's project
configuration (#230)
- fix(rspack): add missing license-webpack-plugin dependency (#301)
- chore(repo): upgrade to nx 16.6.0 (#319)
- fix(rspack): add fileReplacements support (#231)
- chore(reop): update nx to 16.7.1 (#325)
- chore(rspack): add jest babel config to e2e (#321)
- chore: don't use rspack internal module (#328)
- chore(repo): update nx to 16.8.1 (#335)
- feat(rspack): add typecheck (#338)
- chore(repo): update nx to 17.0.1 (#342)
- feat(rspack): add generatePackageJson plugin (#341)
- feat(rspack): add generatePackageJson plugin (#341)
- feat: upgrade rspack to 0.4.4 (#352)
- fix(rspack): Add missing peer dep (#372)
- chore(repo): migrate to latest nx (#376)
- chore(repo): migrate to latest nx (#376)
- feat(rspack): update rspack to install the latest version (#379)
- feat(rspack): add option to keep existing versions of packages for
init generator (#378)
- fix(rspack): do not depend directly on ajv to allow for correct
hoisting (#384)
- fix(rspack): ensure react-refresh is installed (#385)
- fix(rspack): User port should be respected. (#387)
- feat(rspack_: update rspack to install latest version (#389)
- feat(rspack): support object configs (#402)
- feat(rspack): add crystal plugin for inferring projects (#407)
- feat(rspack): add crystal plugin for inferring projects (#407)
- feat(rspack): bump to latest rspack (#412)
- fix(rspack): add postcss-loader for css files (#415)
- feat(rspack): add module federation support (#416)
- feat(rspack): add module federation support (#416)
- fix(rspack): add hook for dev server to log when compilation completed
(#417)
- feat(rspack): add module-federation-static-server (#418)
- fix(rspack): ensure process is default import (#420)
- chore(repo): move packages/rspack to packages/rspack to prepare to be
imported
- chore(repo): move packages-legacy/rspack to packages-legacy/rspack to
prepare to be imported
- chore(repo): move e2e/rspack-e2e to e2e/rspack to prepare to be
imported
- chore(repo): fix e2e setup
- chore(repo): add rspack commit scope
- chore(rspack): configure correctly
- chore(rspack): final fixes
- docs(rspack): add docs for rspack
- chore(rspack): fix rspack e2e
- chore(react): add rspack bundler test

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

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

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

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

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

Fixes #
This commit is contained in:
Jason Jean 2024-09-26 14:30:11 -04:00 committed by GitHub
commit e76c7d1428
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
138 changed files with 9735 additions and 1000 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 '<projectRoot>/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 '<projectRoot>/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"
}

View File

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

View File

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

View File

@ -2559,6 +2559,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",

View File

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

View File

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

View File

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

19
e2e/rspack/jest.config.ts Normal file
View File

@ -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: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/e2e/e2e-rspack',
};

10
e2e/rspack/project.json Normal file
View File

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

View File

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

13
e2e/rspack/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node", "jest"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.spec.json"
}
]
}

View File

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

View File

@ -56,6 +56,7 @@ const nxPackages = [
`@nx/rollup`,
`@nx/react`,
`@nx/remix`,
`@nx/rspack`,
`@nx/storybook`,
`@nx/vue`,
`@nx/vite`,

View File

@ -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<MenuSection>(
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 (
<div id="shell" className="flex h-full flex-col">
<div className="w-full flex-shrink-0">
<DocumentationHeader isNavOpen={navIsOpen} toggleNav={toggleNav} />
</div>
<main
id="main"
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<SidebarContainer
menu={vm.menu}
navIsOpen={navIsOpen}
toggleNav={toggleNav}
/>
<ScrollableContent resetScrollOnNavigation={true}>
<PackageSchemaSubList pkg={vm.package} type={'document'} />
</ScrollableContent>
</main>
</div>
);
}
export async function getStaticProps(): Promise<{
props: {
menu: MenuItem[];
pkg: ProcessedPackageMetadata;
};
}> {
return {
props: {
menu: menusApi.getMenu('nx-api', 'nx-api'),
pkg,
},
};
}

View File

@ -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<MenuSection>(
getPackagesSections(menu),
'id'
),
},
relatedDocuments,
};
return (
<div id="shell" className="flex h-full flex-col">
<div className="w-full flex-shrink-0">
<DocumentationHeader isNavOpen={navIsOpen} toggleNav={toggleNav} />
</div>
<main
id="main"
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<SidebarContainer
menu={vm.menu}
navIsOpen={navIsOpen}
toggleNav={toggleNav}
/>
<ScrollableContent resetScrollOnNavigation={true}>
<DocViewer
document={vm.document}
relatedDocuments={vm.relatedDocuments}
widgetData={widgetData}
/>
</ScrollableContent>
</main>
</div>
);
}
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', ''),
},
};
}

View File

@ -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<MenuSection>(
getPackagesSections(menu),
'id'
),
},
relatedDocuments,
};
return (
<div id="shell" className="flex h-full flex-col">
<div className="w-full flex-shrink-0">
<DocumentationHeader isNavOpen={navIsOpen} toggleNav={toggleNav} />
</div>
<main
id="main"
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<SidebarContainer
menu={vm.menu}
navIsOpen={navIsOpen}
toggleNav={toggleNav}
/>
<ScrollableContent resetScrollOnNavigation={true}>
<DocViewer
document={vm.document}
relatedDocuments={vm.relatedDocuments}
widgetData={widgetData}
/>
</ScrollableContent>
</main>
</div>
);
}
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', ''),
},
};
}

View File

@ -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<MenuSection>(
getPackagesSections(menu),
'id'
),
},
relatedDocuments,
};
return (
<div id="shell" className="flex h-full flex-col">
<div className="w-full flex-shrink-0">
<DocumentationHeader isNavOpen={navIsOpen} toggleNav={toggleNav} />
</div>
<main
id="main"
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<SidebarContainer
menu={vm.menu}
navIsOpen={navIsOpen}
toggleNav={toggleNav}
/>
<ScrollableContent resetScrollOnNavigation={true}>
<DocViewer
document={vm.document}
relatedDocuments={vm.relatedDocuments}
widgetData={widgetData}
/>
</ScrollableContent>
</main>
</div>
);
}
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', ''),
},
};
}

View File

@ -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<MenuSection>(
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 (
<div id="shell" className="flex h-full flex-col">
<div className="w-full flex-shrink-0">
<DocumentationHeader isNavOpen={navIsOpen} toggleNav={toggleNav} />
</div>
<main
id="main"
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<SidebarContainer
menu={vm.menu}
navIsOpen={navIsOpen}
toggleNav={toggleNav}
/>
<ScrollableContent resetScrollOnNavigation={true}>
<PackageSchemaViewer pkg={vm.package} schema={vm.schema} />
</ScrollableContent>
</main>
</div>
);
}
export async function getStaticProps() {
return {
props: {
pkg,
schema,
menu: menusApi.getMenu('nx-api', 'nx-api'),
},
};
}

View File

@ -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<MenuSection>(
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 (
<div id="shell" className="flex h-full flex-col">
<div className="w-full flex-shrink-0">
<DocumentationHeader isNavOpen={navIsOpen} toggleNav={toggleNav} />
</div>
<main
id="main"
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<SidebarContainer
menu={vm.menu}
navIsOpen={navIsOpen}
toggleNav={toggleNav}
/>
<ScrollableContent resetScrollOnNavigation={true}>
<PackageSchemaSubList pkg={vm.package} type={'executor'} />
</ScrollableContent>
</main>
</div>
);
}
export async function getStaticProps(): Promise<{
props: {
menu: MenuItem[];
pkg: ProcessedPackageMetadata;
};
}> {
return {
props: {
menu: menusApi.getMenu('nx-api', 'nx-api'),
pkg,
},
};
}

View File

@ -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<MenuSection>(
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 (
<div id="shell" className="flex h-full flex-col">
<div className="w-full flex-shrink-0">
<DocumentationHeader isNavOpen={navIsOpen} toggleNav={toggleNav} />
</div>
<main
id="main"
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<SidebarContainer
menu={vm.menu}
navIsOpen={navIsOpen}
toggleNav={toggleNav}
/>
<ScrollableContent resetScrollOnNavigation={true}>
<PackageSchemaViewer pkg={vm.package} schema={vm.schema} />
</ScrollableContent>
</main>
</div>
);
}
export async function getStaticProps() {
return {
props: {
pkg,
schema,
menu: menusApi.getMenu('nx-api', 'nx-api'),
},
};
}

View File

@ -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<MenuSection>(
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 (
<div id="shell" className="flex h-full flex-col">
<div className="w-full flex-shrink-0">
<DocumentationHeader isNavOpen={navIsOpen} toggleNav={toggleNav} />
</div>
<main
id="main"
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<SidebarContainer
menu={vm.menu}
navIsOpen={navIsOpen}
toggleNav={toggleNav}
/>
<ScrollableContent resetScrollOnNavigation={true}>
<PackageSchemaViewer pkg={vm.package} schema={vm.schema} />
</ScrollableContent>
</main>
</div>
);
}
export async function getStaticProps() {
return {
props: {
pkg,
schema,
menu: menusApi.getMenu('nx-api', 'nx-api'),
},
};
}

View File

@ -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<MenuSection>(
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 (
<div id="shell" className="flex h-full flex-col">
<div className="w-full flex-shrink-0">
<DocumentationHeader isNavOpen={navIsOpen} toggleNav={toggleNav} />
</div>
<main
id="main"
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<SidebarContainer
menu={vm.menu}
navIsOpen={navIsOpen}
toggleNav={toggleNav}
/>
<ScrollableContent resetScrollOnNavigation={true}>
<PackageSchemaViewer pkg={vm.package} schema={vm.schema} />
</ScrollableContent>
</main>
</div>
);
}
export async function getStaticProps() {
return {
props: {
pkg,
schema,
menu: menusApi.getMenu('nx-api', 'nx-api'),
},
};
}

View File

@ -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<MenuSection>(
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 (
<div id="shell" className="flex h-full flex-col">
<div className="w-full flex-shrink-0">
<DocumentationHeader isNavOpen={navIsOpen} toggleNav={toggleNav} />
</div>
<main
id="main"
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<SidebarContainer
menu={vm.menu}
navIsOpen={navIsOpen}
toggleNav={toggleNav}
/>
<ScrollableContent resetScrollOnNavigation={true}>
<PackageSchemaSubList pkg={vm.package} type={'generator'} />
</ScrollableContent>
</main>
</div>
);
}
export async function getStaticProps(): Promise<{
props: {
menu: MenuItem[];
pkg: ProcessedPackageMetadata;
};
}> {
return {
props: {
menu: menusApi.getMenu('nx-api', 'nx-api'),
pkg,
},
};
}

View File

@ -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<MenuSection>(
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 (
<div id="shell" className="flex h-full flex-col">
<div className="w-full flex-shrink-0">
<DocumentationHeader isNavOpen={navIsOpen} toggleNav={toggleNav} />
</div>
<main
id="main"
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<SidebarContainer
menu={vm.menu}
navIsOpen={navIsOpen}
toggleNav={toggleNav}
/>
<ScrollableContent resetScrollOnNavigation={true}>
<PackageSchemaViewer pkg={vm.package} schema={vm.schema} />
</ScrollableContent>
</main>
</div>
);
}
export async function getStaticProps() {
return {
props: {
pkg,
schema,
menu: menusApi.getMenu('nx-api', 'nx-api'),
},
};
}

View File

@ -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<MenuSection>(
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 (
<div id="shell" className="flex h-full flex-col">
<div className="w-full flex-shrink-0">
<DocumentationHeader isNavOpen={navIsOpen} toggleNav={toggleNav} />
</div>
<main
id="main"
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<SidebarContainer
menu={vm.menu}
navIsOpen={navIsOpen}
toggleNav={toggleNav}
/>
<ScrollableContent resetScrollOnNavigation={true}>
<PackageSchemaList pkg={vm.package} overview={overview} />
</ScrollableContent>
</main>
</div>
);
}
export async function getStaticProps() {
return {
props: {
menu: menusApi.getMenu('nx-api', 'nx-api'),
overview: content,
pkg,
},
};
}

View File

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

View File

@ -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.
<p style="text-align: center;"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx.png" width="600" alt="Nx - Smart, Fast and Extensible Build System"></p>
# Nx: Smart, Fast and Extensible Build System
Nx is a next generation build system with first class monorepo support and powerful integrations.

View File

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

View File

@ -0,0 +1,4 @@
{
"extends": ["@nx/rspack"],
"schematics": {}
}

View File

@ -0,0 +1 @@
export * from '@nx/rspack';

View File

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

View File

@ -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": []
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"declaration": true
},
"include": ["**/*.ts"],
"files": ["index.ts"]
}

View File

@ -142,6 +142,8 @@
"@nrwl/rollup",
"@nx/remix",
"@nrwl/remix",
"@nx/rspack",
"@nrwl/rspack",
"@nx/storybook",
"@nrwl/storybook",
"@nrwl/tao",

View File

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

View File

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

67
packages/rspack/README.md Normal file
View File

@ -0,0 +1,67 @@
<p style="text-align: center;">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-dark.svg">
<img alt="Nx - Smart Monorepos · Fast CI" src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-light.svg" width="100%">
</picture>
</p>
{{links}}
<hr>
# 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}}
<p style="text-align: center;"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx.png" width="600" alt="Nx - Smart, Fast and Extensible Build System"></p>
<hr>
# 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.

View File

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

View File

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

View File

@ -0,0 +1,16 @@
/* eslint-disable */
export default {
displayName: 'rspack',
preset: '../../jest.preset.js',
globals: {},
transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/packages/rspack',
};

View File

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

View File

@ -0,0 +1 @@
export * from './src/utils/module-federation/public-api';

View File

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

View File

@ -0,0 +1,2 @@
export { createDependencies, createNodesV2 } from './src/plugins/plugin';
export type { RspackPluginOptions } from './src/plugins/plugin';

View File

@ -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": []
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<string, string> = {};
for (const remoteApp of staticRemotesConfig.remotes) {
mapLocationOfRemotes[remoteApp] = `http${options.ssl ? 's' : ''}://${
options.host
}:${options.staticRemotesPort}/${
staticRemotesConfig.config[remoteApp].urlSegment
}`;
}
await new Promise<void>((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();
}
}
)
);
}

View File

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

View File

@ -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<void>((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<string, string>,
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();
}
}
)
);
}

View File

@ -0,0 +1,3 @@
export interface ModuleFederationStaticServerSchema {
serveTarget: string;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,38 @@
import * as net from 'net';
export function waitUntilServerIsListening(port: number): Promise<void> {
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<void>((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();
});
}

View File

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

View File

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

View File

@ -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<RspackExecutorSchema>(
browserTarget,
context
);
const serverOptions = readTargetOptions<RspackExecutorSchema>(
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 '<projectRoot>/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 '<projectRoot>/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"]
}

View File

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

View File

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

View File

@ -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": []
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<void> {
await replaceNrwlPackageWithNxPackage(tree, '@nrwl/rspack', '@nx/rspack');
await formatFiles(tree);
}

View File

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

View File

@ -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<ProjectConfiguration, 'targets' | 'metadata'>;
function readTargetsCache(cachePath: string): Record<string, RspackTargets> {
return existsSync(cachePath) ? readJsonFile(cachePath) : {};
}
function writeTargetsToCache(
cachePath,
results?: Record<string, RspackTargets>
) {
writeJsonFile(cachePath, results);
}
export const createDependencies: CreateDependencies = () => {
return [];
};
const rspackConfigGlob = '**/rspack.config.{js,ts,mjs,mts,cjs,cts}';
export const createNodesV2: CreateNodesV2<RspackPluginOptions> = [
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<string, RspackTargets>
) {
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<RspackTargets> {
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);
}
}
}
}

View File

@ -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<Configuration>;
}
export function composePlugins(...plugins: any[]) {
return Object.defineProperty(
async function combined(
config: Configuration,
ctx: SharedConfigContext
): Promise<Configuration> {
for (const plugin of plugins) {
const fn = await plugin;
config = await fn(config, ctx);
}
return config;
},
nxRspackComposablePlugin,
{
value: true,
enumerable: false,
writable: false,
}
);
}

View File

@ -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<Compiler | MultiCompiler> {
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;
}

View File

@ -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<Record<Target, boolean>>;
export type UserProvidedTargetName = Partial<Record<Target, string>>;
export type ValidFoundTargetName = Partial<Record<Target, string>>;
export function findExistingTargetsInProject(
targets: {
[targetName: string]: TargetConfiguration;
},
userProvidedTargets?: UserProvidedTargetName
): {
validFoundTargetName: ValidFoundTargetName;
projectContainsUnsupportedExecutor: boolean;
userProvidedTargetIsUnsupported: TargetFlags;
alreadyHasNxRspackTargets: TargetFlags;
} {
const output: ReturnType<typeof findExistingTargetsInProject> = {
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(
`<script type="module" src="${mainPath}"></script>`
)
) {
tree.write(
`${projectConfig.root}/index.html`,
indexHtmlContent.replace(
'</body>',
`<script type="module" src="${mainPath}"></script>
</body>`
)
);
if (tree.exists(`${projectConfig.root}/src/index.html`)) {
tree.delete(`${projectConfig.root}/src/index.html`);
}
}
} else {
tree.write(
`${projectConfig.root}/index.html`,
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="${mainPath}"></script>
</body>
</html>`
);
}
}
export function normalizeViteConfigFilePathWithTree(
tree: Tree,
projectRoot: string,
configFile?: string
): string {
return configFile && tree.exists(configFile)
? configFile
: tree.exists(joinPathFragments(`${projectRoot}/rspack.config.ts`))
? joinPathFragments(`${projectRoot}/rspack.config.ts`)
: tree.exists(joinPathFragments(`${projectRoot}/rspack.config.js`))
? joinPathFragments(`${projectRoot}/rspack.config.js`)
: undefined;
}
export function getViteConfigPathForProject(
tree: Tree,
projectName: string,
target?: string
) {
let viteConfigPath: string | undefined;
const { targets, root } = readProjectConfiguration(tree, projectName);
if (target) {
viteConfigPath = targets?.[target]?.options?.configFile;
} else {
const config = Object.values(targets).find(
(config) => config.executor === '@nx/rspack:build'
);
viteConfigPath = config?.options?.configFile;
}
return normalizeViteConfigFilePathWithTree(tree, root, viteConfigPath);
}
export async function handleUnsupportedUserProvidedTargets(
userProvidedTargetIsUnsupported: TargetFlags,
userProvidedTargetName: UserProvidedTargetName,
validFoundTargetName: ValidFoundTargetName,
framework: Framework
) {
if (userProvidedTargetIsUnsupported.build && validFoundTargetName.build) {
await handleUnsupportedUserProvidedTargetsErrors(
userProvidedTargetName.build,
validFoundTargetName.build,
'build',
'rspack'
);
}
if (
framework !== 'nest' &&
userProvidedTargetIsUnsupported.serve &&
validFoundTargetName.serve
) {
await handleUnsupportedUserProvidedTargetsErrors(
userProvidedTargetName.serve,
validFoundTargetName.serve,
'serve',
'dev-server'
);
}
}
async function handleUnsupportedUserProvidedTargetsErrors(
userProvidedTargetName: string,
validFoundTargetName: string,
target: Target,
executor: 'rspack' | 'dev-server'
) {
logger.warn(
`The custom ${target} target you provided (${userProvidedTargetName}) cannot be converted to use the @nx/rspack:${executor} executor.
However, we found the following ${target} target in your project that can be converted: ${validFoundTargetName}
Please note that converting a potentially non-compatible project to use Vite.js may result in unexpected behavior. Always commit
your changes before converting a project to use Vite.js, and test the converted project thoroughly before deploying it.
`
);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { Confirm } = require('enquirer');
const prompt = new Confirm({
name: 'question',
message: `Should we convert the ${validFoundTargetName} target to use the @nx/rspack:${executor} executor?`,
initial: true,
});
const shouldConvert = await prompt.run();
if (!shouldConvert) {
throw new Error(
`The ${target} target ${userProvidedTargetName} cannot be converted to use the @nx/rspack:${executor} executor.
Please try again, either by providing a different ${target} target or by not providing a target at all (Nx will
convert the first one it finds, most probably this one: ${validFoundTargetName})
Please note that converting a potentially non-compatible project to use Vite.js may result in unexpected behavior. Always commit
your changes before converting a project to use Vite.js, and test the converted project thoroughly before deploying it.
`
);
}
}
export async function handleUnknownExecutors(projectName: string) {
logger.warn(
`
We could not find any targets in project ${projectName} that use executors which
can be converted to the @nx/rspack executors.
This either means that your project may not have a target
for building, serving, or testing at all, or that your targets are
using executors that are not known to Nx.
If you still want to convert your project to use the @nx/rspack executors,
please make sure to commit your changes before running this generator.
`
);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { Confirm } = require('enquirer');
const prompt = new Confirm({
name: 'question',
message: `Should Nx convert your project to use the @nx/rspack executors?`,
initial: true,
});
const shouldConvert = await prompt.run();
if (!shouldConvert) {
throw new Error(`
Nx could not verify that the executors you are using can be converted to the @nx/rspack executors.
Please try again with a different project.
`);
}
}
export function determineFrameworkAndTarget(
tree: Tree,
options: ConfigurationSchema,
projectRoot: string,
targets: {
[targetName: string]: TargetConfiguration<any>;
}
): { target: 'node' | 'web'; framework?: Framework } {
ensureTypescript();
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { tsquery } = require('@phenomnomnominal/tsquery');
// First try to infer if the target is node
if (options.target !== 'node') {
// Try to infer from jest config if the env is node
let jestConfigPath: string;
if (
targets?.test?.executor !== '@nx/jest:jest' &&
targets?.test?.options?.jestConfig
) {
jestConfigPath = targets?.test?.options?.jestConfig;
} else {
jestConfigPath = joinPathFragments(projectRoot, 'jest.config.ts');
}
if (!tree.exists(jestConfigPath)) {
return { target: options.target, framework: options.framework };
}
const appFileContent = tree.read(jestConfigPath, 'utf-8');
const file = tsquery.ast(appFileContent);
// find testEnvironment: 'node' in jest config
const testEnvironment = tsquery(
file,
`PropertyAssignment:has(Identifier[name="testEnvironment"]) > StringLiteral[value="node"]`
);
if (testEnvironment.length > 0) {
return { target: 'node', framework: options.framework };
}
if (tree.exists(joinPathFragments(projectRoot, 'src/main.ts'))) {
const appFileContent = tree.read(
joinPathFragments(projectRoot, 'src/main.ts'),
'utf-8'
);
const file = tsquery.ast(appFileContent);
const hasNestJsDependency = tsquery(
file,
`ImportDeclaration:has(StringLiteral[value="@nestjs/common"])`
);
if (hasNestJsDependency?.length > 0) {
return { target: 'node', framework: 'nest' };
}
}
}
if (options.framework === 'nest') {
return { target: 'node', framework: 'nest' };
}
if (options.framework !== 'react' && options.target === 'web') {
// Look if React is used in the project
let tsConfigPath = joinPathFragments(projectRoot, 'tsconfig.json');
if (!tree.exists(tsConfigPath)) {
tsConfigPath = determineTsConfig(tree, options);
}
const tsConfig = JSON.parse(tree.read(tsConfigPath).toString());
if (tsConfig?.compilerOptions?.jsx?.includes('react')) {
return { target: 'web', framework: 'react' };
} else {
return { target: options.target, framework: options.framework };
}
}
return { target: options.target, framework: options.framework };
}
export function determineMain(tree: Tree, options: ConfigurationSchema) {
if (options.main) return options.main;
const project = readProjectConfiguration(tree, options.project);
const mainTsx = joinPathFragments(project.root, 'src/main.tsx');
if (tree.exists(mainTsx)) return mainTsx;
return joinPathFragments(project.root, 'src/main.ts');
}
export function determineTsConfig(tree: Tree, options: ConfigurationSchema) {
if (options.tsConfig) return options.tsConfig;
const project = readProjectConfiguration(tree, options.project);
const appJson = joinPathFragments(project.root, 'tsconfig.app.json');
if (tree.exists(appJson)) return appJson;
const libJson = joinPathFragments(project.root, 'tsconfig.lib.json');
if (tree.exists(libJson)) return libJson;
return joinPathFragments(project.root, 'tsconfig.json');
}

View File

@ -0,0 +1,19 @@
export function getCopyPatterns(assets: any[]) {
return assets.map((asset) => {
return {
context: asset.input,
// Now we remove starting slash to make Webpack place it from the output root.
to: asset.output,
from: asset.glob,
globOptions: {
ignore: [
'.gitkeep',
'**/.DS_Store',
'**/Thumbs.db',
...(asset.ignore ?? []),
],
dot: true,
},
};
});
}

Some files were not shown because too many files have changed in this diff Show More