@nx/rsbuild 23.0.0 → 23.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,13 +7,12 @@
7
7
 
8
8
  <div style="text-align: center;">
9
9
 
10
- [![CircleCI](https://circleci.com/gh/nrwl/nx.svg?style=svg)](https://circleci.com/gh/nrwl/nx)
11
10
  [![License](https://img.shields.io/npm/l/@nx/workspace.svg?style=flat-square)]()
12
11
  [![NPM Version](https://badge.fury.io/js/nx.svg)](https://www.npmjs.com/package/nx)
13
12
  [![Semantic Release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)]()
14
13
  [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
15
- [![Join the chat at https://gitter.im/nrwl-nx/community](https://badges.gitter.im/nrwl-nx/community.svg)](https://gitter.im/nrwl-nx/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
16
14
  [![Join us on the Official Nx Discord Server](https://img.shields.io/discord/1143497901675401286?label=discord)](https://go.nx.dev/community)
15
+ [![Nx Sandboxing](https://staging.nx.app/workspaces/62d013ea0852fe0a2df74438/sandbox-badge.svg)](https://nx.dev/docs/features/ci-features/sandboxing)
17
16
 
18
17
  </div>
19
18
 
@@ -43,10 +43,34 @@ async function configurationGenerator(tree, schema) {
43
43
  }
44
44
  const rsbuildVersions = (0, version_utils_1.getRsbuildVersionsForInstalledMajor)(tree);
45
45
  tasks.push((0, devkit_1.addDependenciesToPackageJson)(tree, {}, { '@rsbuild/core': rsbuildVersions.rsbuildVersion }, undefined, true));
46
+ // @rsbuild/core@2 emits ESM output by default for both web and Node
47
+ // targets. There are two ways to make that output runnable:
48
+ // 1. `"type": "module"` on the project package.json — rsbuild's own
49
+ // `create-rsbuild` convention. Used when the project owns a
50
+ // package.json (fabricating one, or landing `type` on the
51
+ // workspace root, would flip every .js file in the repo to ESM).
52
+ // 2. `.mjs` output filenames — used for Node builds when the
53
+ // project has no package.json to carry `"type": "module"`.
54
+ // `.mjs` is unconditionally ESM regardless of the nearest
55
+ // package.json. Web output doesn't need this; the browser loads
56
+ // it via `<script type="module">`.
57
+ // Both are v2-only — on v1 rsbuild still emits CommonJS.
58
+ const installedRsbuildMajor = (0, version_utils_1.getInstalledRsbuildMajorVersion)(tree);
59
+ const isV2 = installedRsbuildMajor === undefined || installedRsbuildMajor >= 2;
60
+ const projectPackageJsonPath = (0, path_1.join)(options.projectRoot, 'package.json');
61
+ const projectHasPackageJson = tree.exists(projectPackageJsonPath);
62
+ const useMjsOutput = isV2 && options.target === 'node' && !projectHasPackageJson;
46
63
  (0, devkit_1.generateFiles)(tree, (0, path_1.join)(__dirname, 'files'), options.projectRoot, {
47
64
  ...options,
65
+ useMjsOutput,
48
66
  tpl: '',
49
67
  });
68
+ if (isV2 && projectHasPackageJson) {
69
+ (0, devkit_1.updateJson)(tree, projectPackageJsonPath, (json) => {
70
+ json.type ??= 'module';
71
+ return json;
72
+ });
73
+ }
50
74
  return (0, devkit_1.runTasksInSerial)(...tasks);
51
75
  }
52
76
  exports.default = configurationGenerator;
@@ -11,7 +11,12 @@ export default defineConfig({
11
11
  port: <%= devServerPort %>
12
12
  },
13
13
  output: {
14
- target: '<%= target %>',
14
+ target: '<%= target %>',<% if (useMjsOutput) { %>
15
+ // @rsbuild/core@2 emits ESM for Node targets. Emit .mjs so the
16
+ // output runs without a `"type": "module"` package.json.
17
+ filename: {
18
+ js: '[name].mjs',
19
+ },<% } %>
15
20
  distPath: {
16
21
  root: 'dist',
17
22
  },
@@ -0,0 +1,92 @@
1
+ #### Make Node-target rsbuild builds runnable under `@rsbuild/core@2`
2
+
3
+ `@rsbuild/core@2` changed the default output format for Node targets. In v1,
4
+ `output.target: 'node'` produced **minified CommonJS**. In v2 it produces
5
+ **unminified ESM** (`output.module` defaults to `true`, `output.minify` to
6
+ `false`).
7
+
8
+ A v2 ESM bundle does not run as plain `node dist/index.js` unless one of the
9
+ following is true:
10
+
11
+ - a `package.json` with `"type": "module"` governs the output, **or**
12
+ - the output files use the `.mjs` extension.
13
+
14
+ Without one of those, the build succeeds but the process crashes at runtime
15
+ with `Cannot use import statement outside a module`.
16
+
17
+ This is workspace-specific — it depends on whether the project owns a
18
+ `package.json`, what runs or references the bundle (a `main` field, a
19
+ Dockerfile, a deploy script, a `serve` target), and whether other `.js`
20
+ files in the project are CommonJS. So it ships as an AI-assisted
21
+ instruction rather than a blind codemod.
22
+
23
+ #### What needs to change
24
+
25
+ For every project with an `rsbuild.config.{ts,js,mjs,cjs}` whose
26
+ `output.target` is `'node'`:
27
+
28
+ 1. **Decide how the ESM output should be made runnable.** Prefer, in order:
29
+ - **The project owns its own `package.json`** → set `"type": "module"`
30
+ on it. This is rsbuild's recommended convention. Check first that no
31
+ sibling `.js` files in the project rely on CommonJS (`require`,
32
+ `module.exports`, `__dirname`); if any do, rename them to `.cjs` or
33
+ convert them to ESM.
34
+
35
+ - **The project has no `package.json`** → add `.mjs` output to the
36
+ rsbuild config instead, so the output is ESM regardless of any
37
+ ancestor `package.json`:
38
+
39
+ ```ts title="rsbuild.config.ts"
40
+ export default defineConfig({
41
+ output: {
42
+ target: 'node',
43
+ filename: { js: '[name].mjs' },
44
+ },
45
+ });
46
+ ```
47
+
48
+ 2. **Update anything that references the output filename.** If you chose
49
+ `.mjs`, fix references in `package.json#main`, Dockerfiles, `serve`
50
+ targets, deploy scripts, and any `import`/`require` of the built file.
51
+
52
+ 3. **Do not pin back to CommonJS** unless the project genuinely cannot move
53
+ to ESM. If you must, restore the v1 behavior explicitly:
54
+
55
+ ```ts title="rsbuild.config.ts"
56
+ export default defineConfig({
57
+ output: {
58
+ target: 'node',
59
+ module: false,
60
+ minify: true,
61
+ },
62
+ });
63
+ ```
64
+
65
+ #### Sample Code Changes
66
+
67
+ ##### Before — Node project, no project package.json
68
+
69
+ ```ts title="apps/api/rsbuild.config.ts" {4}
70
+ export default defineConfig({
71
+ output: {
72
+ target: 'node',
73
+ distPath: { root: 'dist' },
74
+ },
75
+ });
76
+ ```
77
+
78
+ ##### After — `.mjs` output so the ESM bundle runs
79
+
80
+ ```ts title="apps/api/rsbuild.config.ts" {4}
81
+ export default defineConfig({
82
+ output: {
83
+ target: 'node',
84
+ filename: { js: '[name].mjs' },
85
+ distPath: { root: 'dist' },
86
+ },
87
+ });
88
+ ```
89
+
90
+ #### Reference
91
+
92
+ [`@rsbuild/core` v1 → v2 upgrade guide — Node output](https://rsbuild.rs/guide/upgrade/v1-to-v2#node-output)
@@ -0,0 +1,182 @@
1
+ #### Migrate `performance` options for `@rsbuild/core@2`
2
+
3
+ `@rsbuild/core@2` removed two `performance` options and deprecated a third.
4
+ rsbuild v2 does not hard-error on the stale keys — it warns and ignores the
5
+ removed ones, and the deprecated one still works — so the changes here are
6
+ about adopting the replacements rather than fixing a broken build. None
7
+ have a drop-in key rename, so this ships as an AI-assisted instruction.
8
+
9
+ This only affects projects whose `rsbuild.config.{ts,js,mjs,cjs}` sets one
10
+ of the options below. If none of your configs use them, there is nothing
11
+ to do.
12
+
13
+ #### `performance.bundleAnalyze` was removed
14
+
15
+ rsbuild v1 bundled `webpack-bundle-analyzer` and exposed it through
16
+ `performance.bundleAnalyze`. v2 removed it to cut install size.
17
+
18
+ For every config that sets `performance.bundleAnalyze`, remove that option
19
+ and pick one replacement:
20
+
21
+ - **Preferred — Rsdoctor.** Add the [Rsdoctor](https://rsdoctor.dev) plugin;
22
+ it covers bundle-size analysis and more. See
23
+ [rsbuild.rs/guide/debug/rsdoctor](https://rsbuild.rs/guide/debug/rsdoctor).
24
+
25
+ - **Keep webpack-bundle-analyzer.** Install it and register it via
26
+ `tools.rspack`:
27
+
28
+ ```ts title="rsbuild.config.ts"
29
+ import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
30
+
31
+ export default {
32
+ tools: {
33
+ rspack: {
34
+ plugins: [new BundleAnalyzerPlugin({ analyzerMode: 'static' })],
35
+ },
36
+ },
37
+ };
38
+ ```
39
+
40
+ If `performance` is left empty after removing the option, delete the empty
41
+ `performance: {}` block too.
42
+
43
+ #### `performance.profile` was removed
44
+
45
+ `performance.profile` emitted an rspack stats JSON file. v2 removed it; emit
46
+ the file from a small custom plugin instead.
47
+
48
+ For every config that sets `performance.profile: true`, remove that option
49
+ and add:
50
+
51
+ ```ts title="rsbuild.config.ts"
52
+ import { writeFileSync } from 'node:fs';
53
+ import { join } from 'node:path';
54
+
55
+ const statsJsonPlugin = {
56
+ name: 'stats-json-plugin',
57
+ setup(api) {
58
+ api.onAfterBuild(({ stats }) => {
59
+ writeFileSync(
60
+ join(api.context.distPath, 'stats.json'),
61
+ JSON.stringify(stats?.toJson({}), null, 2)
62
+ );
63
+ });
64
+ },
65
+ };
66
+
67
+ export default {
68
+ plugins: [statsJsonPlugin],
69
+ };
70
+ ```
71
+
72
+ #### `performance.chunkSplit` is deprecated
73
+
74
+ `performance.chunkSplit` still works in v2 but is deprecated. Migrate it to
75
+ the new top-level `splitChunks` option, which aligns with Rspack's
76
+ `optimization.splitChunks`.
77
+
78
+ For every config that sets `performance.chunkSplit`, translate `strategy`:
79
+
80
+ | `chunkSplit.strategy` | `splitChunks` replacement |
81
+ | -------------------------------------------- | ----------------------------------------------- |
82
+ | `split-by-experience` | `{ preset: 'default' }` |
83
+ | `split-by-module` | `{ preset: 'per-package' }` |
84
+ | `single-vendor` | `{ preset: 'single-vendor' }` |
85
+ | `all-in-one` | `false` (chunk splitting disabled) |
86
+ | `custom` (with a nested `splitChunks`) | `{ preset: 'none', ...the nested splitChunks }` |
87
+ | `split-by-size` (with `minSize` / `maxSize`) | `{ preset: 'none', minSize, maxSize }` |
88
+
89
+ Then translate the remaining `chunkSplit` keys:
90
+
91
+ - `override` → spread its contents directly into the top-level `splitChunks`.
92
+ - `forceSplitting` → `splitChunks.cacheGroups`. Each `forceSplitting` entry
93
+ `name: <RegExp>` expands to a full cache group:
94
+
95
+ ```ts
96
+ cacheGroups: {
97
+ <name>: {
98
+ test: <RegExp>,
99
+ name: '<name>',
100
+ chunks: 'all',
101
+ // priority 0 normally; use 1 when the `single-vendor` preset is set
102
+ priority: 0,
103
+ enforce: true,
104
+ },
105
+ }
106
+ ```
107
+
108
+ ##### `forceSplitting` example
109
+
110
+ ```diff title="rsbuild.config.ts"
111
+ export default {
112
+ - performance: {
113
+ - chunkSplit: {
114
+ - forceSplitting: {
115
+ - axios: /node_modules[\\/]axios/,
116
+ - },
117
+ - },
118
+ - },
119
+ + splitChunks: {
120
+ + cacheGroups: {
121
+ + axios: {
122
+ + test: /node_modules[\\/]axios/,
123
+ + name: 'axios',
124
+ + chunks: 'all',
125
+ + priority: 0,
126
+ + enforce: true,
127
+ + },
128
+ + },
129
+ + },
130
+ };
131
+ ```
132
+
133
+ If `performance` is empty after removing `chunkSplit`, delete the empty
134
+ `performance: {}` block.
135
+
136
+ #### Sample Code Changes
137
+
138
+ ##### Before
139
+
140
+ ```ts title="apps/web/rsbuild.config.ts" {3-5}
141
+ export default defineConfig({
142
+ performance: {
143
+ bundleAnalyze: {},
144
+ profile: true,
145
+ },
146
+ });
147
+ ```
148
+
149
+ ##### After
150
+
151
+ ```ts title="apps/web/rsbuild.config.ts"
152
+ import { writeFileSync } from 'node:fs';
153
+ import { join } from 'node:path';
154
+ import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
155
+
156
+ const statsJsonPlugin = {
157
+ name: 'stats-json-plugin',
158
+ setup(api) {
159
+ api.onAfterBuild(({ stats }) => {
160
+ writeFileSync(
161
+ join(api.context.distPath, 'stats.json'),
162
+ JSON.stringify(stats?.toJson({}), null, 2)
163
+ );
164
+ });
165
+ },
166
+ };
167
+
168
+ export default defineConfig({
169
+ plugins: [statsJsonPlugin],
170
+ tools: {
171
+ rspack: {
172
+ plugins: [new BundleAnalyzerPlugin({ analyzerMode: 'static' })],
173
+ },
174
+ },
175
+ });
176
+ ```
177
+
178
+ #### Reference
179
+
180
+ - [`@rsbuild/core` v1 → v2 upgrade guide — Remove `performance.bundleAnalyze`](https://rsbuild.rs/guide/upgrade/v1-to-v2#remove-performancebundleanalyze)
181
+ - [`@rsbuild/core` v1 → v2 upgrade guide — Remove `performance.profile`](https://rsbuild.rs/guide/upgrade/v1-to-v2#remove-performanceprofile)
182
+ - [`@rsbuild/core` v1 → v2 upgrade guide — Migrate `performance.chunkSplit`](https://rsbuild.rs/guide/upgrade/v1-to-v2#migrate-performancechunksplit)
@@ -0,0 +1,86 @@
1
+ import { type Tree } from '@nx/devkit';
2
+ /**
3
+ * Rewrite user-authored `rsbuild.config.{js,ts,mjs,cjs}` files for
4
+ * @rsbuild/core@2 compatibility. Five transformations:
5
+ *
6
+ * 1. `preview.setupMiddlewares` → `server.setupMiddlewares`. v2 moved
7
+ * middleware setup off the preview-only namespace.
8
+ *
9
+ * 2. `performance.removeMomentLocale: true|false` is dropped — v2
10
+ * removed the option entirely. moment locale stripping in v2 is
11
+ * handled via standard externals/aliases.
12
+ *
13
+ * 3. `source.alias` / `source.aliasStrategy` → `resolve.alias` /
14
+ * `resolve.aliasStrategy`. v2 removed the deprecated `source`-level
15
+ * options in favor of the `resolve` block.
16
+ *
17
+ * 4. `server.proxy` entry `context` → `pathFilter`. v2's
18
+ * http-proxy-middleware@4 renamed the matcher option.
19
+ *
20
+ * 5. `server.proxy` entry `on{Open,Close,Error,ProxyReq,ProxyRes}`
21
+ * handlers → a unified `on: { open, close, error, proxyReq,
22
+ * proxyRes }` object.
23
+ *
24
+ * Other v2 changes (default `host` flip from 0.0.0.0 to localhost,
25
+ * default `loadConfig: 'auto'`, Node target ESM default) are behavior
26
+ * changes that don't have a deterministic config-rewrite recipe — they
27
+ * live in the docs / release notes instead.
28
+ *
29
+ * All transforms are AST-based: walk the TypeScript parse tree and
30
+ * patch source ranges so quoted keys, spreads, and surrounding comments
31
+ * are preserved.
32
+ */
33
+ export default function migrateRsbuildConfigToV2(tree: Tree): Promise<void>;
34
+ /**
35
+ * Exported for unit testing. Rewrites a single-key
36
+ * `preview: { setupMiddlewares: [...] }` literal to
37
+ * `server: { setupMiddlewares: [...] }`.
38
+ *
39
+ * Narrowed to the single-key shape only. For any other shape — preview
40
+ * with host/port/headers, multiple keys, or anything beyond the
41
+ * setupMiddlewares array — the file is left untouched. A blanket
42
+ * rename would move options that still belong under `preview` in v2,
43
+ * or duplicate a `server:` key the user already has elsewhere.
44
+ */
45
+ export declare function renameSetupMiddlewares(source: string): string;
46
+ /**
47
+ * Exported for unit testing. Drops `removeMomentLocale: true|false`
48
+ * property assignments anywhere in the config — v2 removed the option,
49
+ * so the value is irrelevant.
50
+ */
51
+ export declare function dropRemoveMomentLocale(source: string): string;
52
+ /**
53
+ * Exported for unit testing. Relocates `source.alias` and
54
+ * `source.aliasStrategy` into a sibling `resolve` block — v2 removed
55
+ * the deprecated `source`-level forms.
56
+ *
57
+ * Three shapes are handled per config object:
58
+ * - `source` has only the alias keys, no `resolve` → rename the
59
+ * `source` key to `resolve` (one-token edit).
60
+ * - `source` has alias keys plus others, with an existing `resolve`
61
+ * block → splice the alias properties into the `resolve` block.
62
+ * - same, but no `resolve` block → create one immediately before
63
+ * `source` carrying the alias properties.
64
+ *
65
+ * `formatFiles` tidies whitespace afterward, so the inserts only need
66
+ * to be syntactically valid, not pretty.
67
+ */
68
+ export declare function moveSourceAliasToResolve(source: string): string;
69
+ /**
70
+ * Exported for unit testing. Renames the `context` matcher option to
71
+ * `pathFilter` inside `server.proxy` entries — http-proxy-middleware@4
72
+ * (pulled in by @rsbuild/core@2) renamed it. Anchored to a proxy entry
73
+ * so an unrelated `context` key elsewhere is left alone.
74
+ */
75
+ export declare function renameProxyContext(source: string): string;
76
+ /**
77
+ * Exported for unit testing. Consolidates the per-event proxy handlers
78
+ * (`onOpen`, `onClose`, `onError`, `onProxyReq`, `onProxyRes`) into a
79
+ * single `on: { open, close, error, proxyReq, proxyRes }` object —
80
+ * http-proxy-middleware@4's unified event API.
81
+ *
82
+ * Skips any proxy entry that already has an `on` property (already
83
+ * migrated). Anchored to proxy entries so unrelated `on*` keys are
84
+ * untouched.
85
+ */
86
+ export declare function groupProxyEventHandlers(source: string): string;
@@ -0,0 +1,370 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = migrateRsbuildConfigToV2;
4
+ exports.renameSetupMiddlewares = renameSetupMiddlewares;
5
+ exports.dropRemoveMomentLocale = dropRemoveMomentLocale;
6
+ exports.moveSourceAliasToResolve = moveSourceAliasToResolve;
7
+ exports.renameProxyContext = renameProxyContext;
8
+ exports.groupProxyEventHandlers = groupProxyEventHandlers;
9
+ const tslib_1 = require("tslib");
10
+ const devkit_1 = require("@nx/devkit");
11
+ const tsquery_1 = require("@phenomnomnominal/tsquery");
12
+ const ts = tslib_1.__importStar(require("typescript"));
13
+ /**
14
+ * Rewrite user-authored `rsbuild.config.{js,ts,mjs,cjs}` files for
15
+ * @rsbuild/core@2 compatibility. Five transformations:
16
+ *
17
+ * 1. `preview.setupMiddlewares` → `server.setupMiddlewares`. v2 moved
18
+ * middleware setup off the preview-only namespace.
19
+ *
20
+ * 2. `performance.removeMomentLocale: true|false` is dropped — v2
21
+ * removed the option entirely. moment locale stripping in v2 is
22
+ * handled via standard externals/aliases.
23
+ *
24
+ * 3. `source.alias` / `source.aliasStrategy` → `resolve.alias` /
25
+ * `resolve.aliasStrategy`. v2 removed the deprecated `source`-level
26
+ * options in favor of the `resolve` block.
27
+ *
28
+ * 4. `server.proxy` entry `context` → `pathFilter`. v2's
29
+ * http-proxy-middleware@4 renamed the matcher option.
30
+ *
31
+ * 5. `server.proxy` entry `on{Open,Close,Error,ProxyReq,ProxyRes}`
32
+ * handlers → a unified `on: { open, close, error, proxyReq,
33
+ * proxyRes }` object.
34
+ *
35
+ * Other v2 changes (default `host` flip from 0.0.0.0 to localhost,
36
+ * default `loadConfig: 'auto'`, Node target ESM default) are behavior
37
+ * changes that don't have a deterministic config-rewrite recipe — they
38
+ * live in the docs / release notes instead.
39
+ *
40
+ * All transforms are AST-based: walk the TypeScript parse tree and
41
+ * patch source ranges so quoted keys, spreads, and surrounding comments
42
+ * are preserved.
43
+ */
44
+ async function migrateRsbuildConfigToV2(tree) {
45
+ const configFiles = [];
46
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '.', (filePath) => {
47
+ if (filePath.endsWith('rsbuild.config.ts') ||
48
+ filePath.endsWith('rsbuild.config.js') ||
49
+ filePath.endsWith('rsbuild.config.mjs') ||
50
+ filePath.endsWith('rsbuild.config.cjs')) {
51
+ configFiles.push(filePath);
52
+ }
53
+ });
54
+ for (const configFile of configFiles) {
55
+ const original = tree.read(configFile, 'utf-8');
56
+ if (!original)
57
+ continue;
58
+ let next = renameSetupMiddlewares(original);
59
+ next = dropRemoveMomentLocale(next);
60
+ next = moveSourceAliasToResolve(next);
61
+ next = renameProxyContext(next);
62
+ next = groupProxyEventHandlers(next);
63
+ if (next !== original) {
64
+ tree.write(configFile, next);
65
+ }
66
+ }
67
+ await (0, devkit_1.formatFiles)(tree);
68
+ }
69
+ function applyEdits(source, edits) {
70
+ if (edits.length === 0)
71
+ return source;
72
+ const sorted = [...edits].sort((a, b) => b.start - a.start);
73
+ return sorted.reduce((acc, e) => acc.slice(0, e.start) + e.replacement + acc.slice(e.end), source);
74
+ }
75
+ function getPropertyName(prop) {
76
+ const name = prop.name;
77
+ if (ts.isIdentifier(name))
78
+ return name.text;
79
+ if (ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)) {
80
+ return name.text;
81
+ }
82
+ return undefined;
83
+ }
84
+ /**
85
+ * Exported for unit testing. Rewrites a single-key
86
+ * `preview: { setupMiddlewares: [...] }` literal to
87
+ * `server: { setupMiddlewares: [...] }`.
88
+ *
89
+ * Narrowed to the single-key shape only. For any other shape — preview
90
+ * with host/port/headers, multiple keys, or anything beyond the
91
+ * setupMiddlewares array — the file is left untouched. A blanket
92
+ * rename would move options that still belong under `preview` in v2,
93
+ * or duplicate a `server:` key the user already has elsewhere.
94
+ */
95
+ function renameSetupMiddlewares(source) {
96
+ if (!source.includes('setupMiddlewares'))
97
+ return source;
98
+ const sourceFile = (0, tsquery_1.ast)(source);
99
+ const edits = [];
100
+ const visit = (node) => {
101
+ if (ts.isPropertyAssignment(node) &&
102
+ getPropertyName(node) === 'preview' &&
103
+ ts.isObjectLiteralExpression(node.initializer)) {
104
+ const props = node.initializer.properties;
105
+ if (props.length === 1 &&
106
+ ts.isPropertyAssignment(props[0]) &&
107
+ getPropertyName(props[0]) === 'setupMiddlewares') {
108
+ // Replace only the property name `preview` → `server`. The
109
+ // initializer (the object literal with setupMiddlewares) stays
110
+ // untouched, preserving its formatting verbatim.
111
+ const nameNode = node.name;
112
+ edits.push({
113
+ start: nameNode.getStart(sourceFile),
114
+ end: nameNode.getEnd(),
115
+ replacement: 'server',
116
+ });
117
+ }
118
+ }
119
+ ts.forEachChild(node, visit);
120
+ };
121
+ visit(sourceFile);
122
+ return applyEdits(source, edits);
123
+ }
124
+ /**
125
+ * Exported for unit testing. Drops `removeMomentLocale: true|false`
126
+ * property assignments anywhere in the config — v2 removed the option,
127
+ * so the value is irrelevant.
128
+ */
129
+ function dropRemoveMomentLocale(source) {
130
+ if (!source.includes('removeMomentLocale'))
131
+ return source;
132
+ const sourceFile = (0, tsquery_1.ast)(source);
133
+ const edits = [];
134
+ const visit = (node) => {
135
+ if (ts.isPropertyAssignment(node) &&
136
+ getPropertyName(node) === 'removeMomentLocale' &&
137
+ (node.initializer.kind === ts.SyntaxKind.TrueKeyword ||
138
+ node.initializer.kind === ts.SyntaxKind.FalseKeyword)) {
139
+ let endOfRemoval = node.getEnd();
140
+ const trailingText = source.slice(endOfRemoval);
141
+ const trailingMatch = trailingText.match(/^\s*,?\s*\r?\n/);
142
+ if (trailingMatch) {
143
+ endOfRemoval += trailingMatch[0].length;
144
+ }
145
+ let startOfRemoval = node.getStart(sourceFile);
146
+ const lineStart = source.lastIndexOf('\n', startOfRemoval - 1) + 1;
147
+ if (source.slice(lineStart, startOfRemoval).trim() === '') {
148
+ startOfRemoval = lineStart;
149
+ }
150
+ edits.push({
151
+ start: startOfRemoval,
152
+ end: endOfRemoval,
153
+ replacement: '',
154
+ });
155
+ }
156
+ ts.forEachChild(node, visit);
157
+ };
158
+ visit(sourceFile);
159
+ return applyEdits(source, edits);
160
+ }
161
+ /**
162
+ * Exported for unit testing. Relocates `source.alias` and
163
+ * `source.aliasStrategy` into a sibling `resolve` block — v2 removed
164
+ * the deprecated `source`-level forms.
165
+ *
166
+ * Three shapes are handled per config object:
167
+ * - `source` has only the alias keys, no `resolve` → rename the
168
+ * `source` key to `resolve` (one-token edit).
169
+ * - `source` has alias keys plus others, with an existing `resolve`
170
+ * block → splice the alias properties into the `resolve` block.
171
+ * - same, but no `resolve` block → create one immediately before
172
+ * `source` carrying the alias properties.
173
+ *
174
+ * `formatFiles` tidies whitespace afterward, so the inserts only need
175
+ * to be syntactically valid, not pretty.
176
+ */
177
+ function moveSourceAliasToResolve(source) {
178
+ if (!/\balias(?:Strategy)?\b/.test(source))
179
+ return source;
180
+ const sourceFile = (0, tsquery_1.ast)(source);
181
+ const edits = [];
182
+ const ALIAS_KEYS = new Set(['alias', 'aliasStrategy']);
183
+ const visit = (node) => {
184
+ if (ts.isObjectLiteralExpression(node)) {
185
+ const sourceProp = node.properties.find((p) => ts.isPropertyAssignment(p) && getPropertyName(p) === 'source');
186
+ if (sourceProp && ts.isObjectLiteralExpression(sourceProp.initializer)) {
187
+ const sourceObj = sourceProp.initializer;
188
+ const aliasProps = sourceObj.properties.filter((p) => ts.isPropertyAssignment(p) &&
189
+ ALIAS_KEYS.has(getPropertyName(p) ?? ''));
190
+ if (aliasProps.length > 0) {
191
+ const resolveProp = node.properties.find((p) => ts.isPropertyAssignment(p) && getPropertyName(p) === 'resolve');
192
+ const aliasAreAllProps = aliasProps.length === sourceObj.properties.length;
193
+ if (!resolveProp && aliasAreAllProps) {
194
+ // `source` carries nothing but the alias keys — just rename
195
+ // the property to `resolve`.
196
+ edits.push({
197
+ start: sourceProp.name.getStart(sourceFile),
198
+ end: sourceProp.name.getEnd(),
199
+ replacement: 'resolve',
200
+ });
201
+ }
202
+ else {
203
+ const movedTexts = aliasProps.map((p) => p.getText(sourceFile));
204
+ // Remove each alias property from `source` (+ trailing
205
+ // comma / line terminator).
206
+ for (const p of aliasProps) {
207
+ let end = p.getEnd();
208
+ const trailing = source.slice(end).match(/^\s*,?\s*\r?\n?/);
209
+ if (trailing)
210
+ end += trailing[0].length;
211
+ let start = p.getStart(sourceFile);
212
+ const lineStart = source.lastIndexOf('\n', start - 1) + 1;
213
+ if (source.slice(lineStart, start).trim() === '') {
214
+ start = lineStart;
215
+ }
216
+ edits.push({ start, end, replacement: '' });
217
+ }
218
+ if (resolveProp &&
219
+ ts.isObjectLiteralExpression(resolveProp.initializer)) {
220
+ // Splice into the existing `resolve` block, just before
221
+ // its closing brace. Lead with a comma when the block
222
+ // already has properties so we don't fuse onto a
223
+ // comma-less last property.
224
+ const closeBrace = resolveProp.initializer.getEnd() - 1;
225
+ const leadingSep = resolveProp.initializer.properties.length > 0 ? ',\n' : '';
226
+ edits.push({
227
+ start: closeBrace,
228
+ end: closeBrace,
229
+ replacement: leadingSep + movedTexts.map((t) => `${t},\n`).join(''),
230
+ });
231
+ }
232
+ else {
233
+ // No `resolve` block — create one right before `source`.
234
+ const insertAt = sourceProp.getStart(sourceFile);
235
+ edits.push({
236
+ start: insertAt,
237
+ end: insertAt,
238
+ replacement: `resolve: {\n${movedTexts
239
+ .map((t) => `${t},\n`)
240
+ .join('')}},\n`,
241
+ });
242
+ }
243
+ }
244
+ }
245
+ }
246
+ }
247
+ ts.forEachChild(node, visit);
248
+ };
249
+ visit(sourceFile);
250
+ return applyEdits(source, edits);
251
+ }
252
+ /**
253
+ * Returns true when `obj` is a `server.proxy` entry — either an element
254
+ * of the array form (`proxy: [{ ... }]`) or a value of the object-map
255
+ * form (`proxy: { '/api': { ... } }`).
256
+ */
257
+ function isProxyEntryObject(obj) {
258
+ const parent = obj.parent;
259
+ // Array form: proxy: [ { ... } ]
260
+ if (parent && ts.isArrayLiteralExpression(parent)) {
261
+ const arrProp = parent.parent;
262
+ return (!!arrProp &&
263
+ ts.isPropertyAssignment(arrProp) &&
264
+ getPropertyName(arrProp) === 'proxy');
265
+ }
266
+ // Object-map form: proxy: { '/api': { ... } }
267
+ if (parent && ts.isPropertyAssignment(parent)) {
268
+ const mapObj = parent.parent;
269
+ if (mapObj && ts.isObjectLiteralExpression(mapObj)) {
270
+ const mapProp = mapObj.parent;
271
+ return (!!mapProp &&
272
+ ts.isPropertyAssignment(mapProp) &&
273
+ getPropertyName(mapProp) === 'proxy');
274
+ }
275
+ }
276
+ return false;
277
+ }
278
+ /**
279
+ * Exported for unit testing. Renames the `context` matcher option to
280
+ * `pathFilter` inside `server.proxy` entries — http-proxy-middleware@4
281
+ * (pulled in by @rsbuild/core@2) renamed it. Anchored to a proxy entry
282
+ * so an unrelated `context` key elsewhere is left alone.
283
+ */
284
+ function renameProxyContext(source) {
285
+ if (!source.includes('context'))
286
+ return source;
287
+ const sourceFile = (0, tsquery_1.ast)(source);
288
+ const edits = [];
289
+ const visit = (node) => {
290
+ if (ts.isPropertyAssignment(node) &&
291
+ getPropertyName(node) === 'context' &&
292
+ ts.isObjectLiteralExpression(node.parent) &&
293
+ isProxyEntryObject(node.parent)) {
294
+ edits.push({
295
+ start: node.name.getStart(sourceFile),
296
+ end: node.name.getEnd(),
297
+ replacement: 'pathFilter',
298
+ });
299
+ }
300
+ ts.forEachChild(node, visit);
301
+ };
302
+ visit(sourceFile);
303
+ return applyEdits(source, edits);
304
+ }
305
+ const PROXY_HANDLER_RENAMES = {
306
+ onOpen: 'open',
307
+ onClose: 'close',
308
+ onError: 'error',
309
+ onProxyReq: 'proxyReq',
310
+ onProxyRes: 'proxyRes',
311
+ };
312
+ /**
313
+ * Exported for unit testing. Consolidates the per-event proxy handlers
314
+ * (`onOpen`, `onClose`, `onError`, `onProxyReq`, `onProxyRes`) into a
315
+ * single `on: { open, close, error, proxyReq, proxyRes }` object —
316
+ * http-proxy-middleware@4's unified event API.
317
+ *
318
+ * Skips any proxy entry that already has an `on` property (already
319
+ * migrated). Anchored to proxy entries so unrelated `on*` keys are
320
+ * untouched.
321
+ */
322
+ function groupProxyEventHandlers(source) {
323
+ if (!/\bon(Open|Close|Error|ProxyReq|ProxyRes)\b/.test(source)) {
324
+ return source;
325
+ }
326
+ const sourceFile = (0, tsquery_1.ast)(source);
327
+ const edits = [];
328
+ const visit = (node) => {
329
+ if (ts.isObjectLiteralExpression(node) && isProxyEntryObject(node)) {
330
+ const alreadyHasOn = node.properties.some((p) => ts.isPropertyAssignment(p) && getPropertyName(p) === 'on');
331
+ if (!alreadyHasOn) {
332
+ const handlers = node.properties.filter((p) => ts.isPropertyAssignment(p) &&
333
+ (getPropertyName(p) ?? '') in PROXY_HANDLER_RENAMES);
334
+ if (handlers.length > 0) {
335
+ const entries = handlers.map((p) => {
336
+ const newName = PROXY_HANDLER_RENAMES[getPropertyName(p)];
337
+ return `${newName}: ${p.initializer.getText(sourceFile)}`;
338
+ });
339
+ // Replace the first handler's range with the `on` block...
340
+ const first = handlers[0];
341
+ let firstEnd = first.getEnd();
342
+ const firstTrailing = source.slice(firstEnd).match(/^\s*,?\s*\r?\n?/);
343
+ if (firstTrailing)
344
+ firstEnd += firstTrailing[0].length;
345
+ edits.push({
346
+ start: first.getStart(sourceFile),
347
+ end: firstEnd,
348
+ replacement: `on: {\n${entries.map((e) => `${e},\n`).join('')}},\n`,
349
+ });
350
+ // ...and delete the remaining handlers.
351
+ for (const p of handlers.slice(1)) {
352
+ let end = p.getEnd();
353
+ const trailing = source.slice(end).match(/^\s*,?\s*\r?\n?/);
354
+ if (trailing)
355
+ end += trailing[0].length;
356
+ let start = p.getStart(sourceFile);
357
+ const lineStart = source.lastIndexOf('\n', start - 1) + 1;
358
+ if (source.slice(lineStart, start).trim() === '') {
359
+ start = lineStart;
360
+ }
361
+ edits.push({ start, end, replacement: '' });
362
+ }
363
+ }
364
+ }
365
+ }
366
+ ts.forEachChild(node, visit);
367
+ };
368
+ visit(sourceFile);
369
+ return applyEdits(source, edits);
370
+ }
@@ -74,7 +74,9 @@ async function createRsbuildTargets(configFilePath, projectRoot, options, tsConf
74
74
  const absoluteConfigFilePath = (0, devkit_1.joinPathFragments)(context.workspaceRoot, configFilePath);
75
75
  // Required lazily: `@rsbuild/core` is an optional peer dependency, so it
76
76
  // may be absent when the plugin is loaded in a workspace that doesn't use
77
- // Rsbuild yet (e.g. before a generator installs it).
77
+ // Rsbuild yet (e.g. before a generator installs it). This path runs in
78
+ // the CLI/daemon (never Jest), so a plain require works on Node 22.12+
79
+ // via require(esm) for the pure-ESM @rsbuild/core@2.
78
80
  const { loadConfig } = require('@rsbuild/core');
79
81
  const rsbuildConfig = await loadConfig({
80
82
  path: absoluteConfigFilePath,
@@ -1,6 +1,6 @@
1
1
  export declare const nxVersion: any;
2
2
  export declare const minSupportedRsbuildVersion = "1.0.0";
3
- export declare const supportedRsbuildMajorVersions: readonly [1];
3
+ export declare const supportedRsbuildMajorVersions: readonly [2, 1];
4
4
  export type SupportedRsbuildMajorVersion = (typeof supportedRsbuildMajorVersions)[number];
5
5
  type RsbuildVersionMap = {
6
6
  rsbuildVersion: string;
@@ -4,18 +4,26 @@ exports.rsbuildPluginSassVersion = exports.rsbuildPluginVueVersion = exports.rsb
4
4
  const path_1 = require("path");
5
5
  exports.nxVersion = require((0, path_1.join)('@nx/rsbuild', 'package.json')).version;
6
6
  exports.minSupportedRsbuildVersion = '1.0.0';
7
- // Supported `@rsbuild/core` majors. Currently v1 only v2 ships as pure
8
- // ESM, which @nx/rsbuild (CommonJS) cannot consume without a deeper
9
- // refactor; tracked separately.
10
- exports.supportedRsbuildMajorVersions = [1];
7
+ // Supported `@rsbuild/core` majors. v2 is the latest; v1 stays in the
8
+ // window for backward compatibility per the multi-version policy.
9
+ exports.supportedRsbuildMajorVersions = [2, 1];
11
10
  exports.latestRsbuildVersions = {
12
- rsbuildVersion: '1.1.10',
13
- rsbuildPluginReactVersion: '1.1.0',
14
- rsbuildPluginVueVersion: '1.0.5',
15
- rsbuildPluginSassVersion: '1.1.2',
11
+ rsbuildVersion: '2.0.7',
12
+ rsbuildPluginReactVersion: '2.0.0',
13
+ // plugin-vue and plugin-sass still publish 1.x but declare compatibility
14
+ // with rsbuild ^1.0.0 || ^2.0.0-0, so the same range works on both
15
+ // majors.
16
+ rsbuildPluginVueVersion: '1.2.8',
17
+ rsbuildPluginSassVersion: '1.5.2',
16
18
  };
17
19
  exports.backwardCompatibleRsbuildVersions = {
18
- 1: exports.latestRsbuildVersions,
20
+ 2: exports.latestRsbuildVersions,
21
+ 1: {
22
+ rsbuildVersion: '1.1.10',
23
+ rsbuildPluginReactVersion: '1.1.0',
24
+ rsbuildPluginVueVersion: '1.0.5',
25
+ rsbuildPluginSassVersion: '1.1.2',
26
+ },
19
27
  };
20
28
  /**
21
29
  * Kept for backward compatibility with code paths that don't yet branch on
package/migrations.json CHANGED
@@ -5,6 +5,51 @@
5
5
  "description": "Rename imports of `createNodesV2` from `@nx/rsbuild` to the canonical `createNodes` export.",
6
6
  "implementation": "./dist/src/migrations/update-23-0-0/migrate-create-nodes-v2-to-create-nodes",
7
7
  "documentation": "./dist/src/migrations/update-23-0-0/migrate-create-nodes-v2-to-create-nodes.md"
8
+ },
9
+ "update-23-0-0-migrate-rsbuild-config-to-v2": {
10
+ "cli": "nx",
11
+ "version": "23.1.0-beta.0",
12
+ "description": "Rewrites rsbuild.config.{js,ts,mjs,cjs} files for @rsbuild/core@2: renames preview.setupMiddlewares to server.setupMiddlewares, drops the removed performance.removeMomentLocale option, moves source.alias/source.aliasStrategy to the resolve block, and updates server.proxy entries for http-proxy-middleware v4 (context to pathFilter, on* handlers to a unified on object).",
13
+ "factory": "./dist/src/migrations/update-23-0-0/migrate-rsbuild-config-to-v2",
14
+ "requires": {
15
+ "@rsbuild/core": ">=2.0.0"
16
+ }
17
+ },
18
+ "update-23-0-0-migrate-node-output-to-esm": {
19
+ "cli": "nx",
20
+ "version": "23.1.0-beta.0",
21
+ "description": "AI-assisted migration: make Node-target rsbuild builds runnable under @rsbuild/core@2, which emits ESM output by default.",
22
+ "prompt": "./dist/src/migrations/update-23-0-0/migrate-node-output-to-esm.md",
23
+ "requires": {
24
+ "@rsbuild/core": ">=2.0.0"
25
+ }
26
+ },
27
+ "update-23-0-0-migrate-performance-options": {
28
+ "cli": "nx",
29
+ "version": "23.1.0-beta.0",
30
+ "description": "AI-assisted migration: adopt replacements for the performance.bundleAnalyze and performance.profile options removed in @rsbuild/core@2, and migrate the deprecated performance.chunkSplit option to splitChunks.",
31
+ "prompt": "./dist/src/migrations/update-23-0-0/migrate-performance-options.md",
32
+ "requires": {
33
+ "@rsbuild/core": ">=2.0.0"
34
+ }
35
+ }
36
+ },
37
+ "packageJsonUpdates": {
38
+ "23.0.0-rsbuild-v2": {
39
+ "version": "23.1.0-beta.0",
40
+ "requires": {
41
+ "@rsbuild/core": ">=1.0.0 <2.0.0"
42
+ },
43
+ "packages": {
44
+ "@rsbuild/core": {
45
+ "version": "^2.0.7",
46
+ "alwaysAddToPackageJson": false
47
+ },
48
+ "@rsbuild/plugin-react": {
49
+ "version": "^2.0.0",
50
+ "alwaysAddToPackageJson": false
51
+ }
52
+ }
8
53
  }
9
54
  }
10
55
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nx/rsbuild",
3
3
  "description": "The Nx Plugin for Rsbuild contains an Nx plugin, executors and utilities that support building applications using Rsbuild.",
4
- "version": "23.0.0",
4
+ "version": "23.1.0-beta.0",
5
5
  "type": "commonjs",
6
6
  "files": [
7
7
  "dist",
@@ -57,14 +57,14 @@
57
57
  "minimatch": "10.2.5",
58
58
  "semver": "^7.6.3",
59
59
  "@phenomnomnominal/tsquery": "~6.2.0",
60
- "@nx/devkit": "23.0.0",
61
- "@nx/js": "23.0.0"
60
+ "@nx/devkit": "23.1.0-beta.0",
61
+ "@nx/js": "23.1.0-beta.0"
62
62
  },
63
63
  "devDependencies": {
64
- "nx": "23.0.0"
64
+ "nx": "23.1.0-beta.0"
65
65
  },
66
66
  "peerDependencies": {
67
- "@rsbuild/core": "^1.0.0"
67
+ "@rsbuild/core": "^1.0.0 || ^2.0.0"
68
68
  },
69
69
  "peerDependenciesMeta": {
70
70
  "@rsbuild/core": {