@grafana/create-plugin 7.18.0 → 7.19.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.19.0](https://github.com/grafana/plugin-tools/compare/@grafana/create-plugin@7.18.0...@grafana/create-plugin@7.19.0) (2026-11-02)
4
+
5
+
6
+ ### Features
7
+
8
+ * extract CSS into separate files in rspack builds ([#3040](https://github.com/grafana/plugin-tools/issues/3040)) ([2fd22de](https://github.com/grafana/plugin-tools/commit/2fd22de25721a0bfc718e266b6cc1bcddcd37457))
9
+
3
10
  ## [7.18.0](https://github.com/grafana/plugin-tools/compare/@grafana/create-plugin@7.17.0...@grafana/create-plugin@7.18.0) (2026-11-02)
4
11
 
5
12
 
@@ -24,8 +24,8 @@ What is left is the plugin's own build customisation, which needs judgement to p
24
24
  - No plugin code builds a runtime URL to a file the new base config no longer copies into `dist` (see step 6).
25
25
  3. Otherwise, work out the starting point:
26
26
  - **From webpack**: there is a root `webpack.config.*`, or `rspack.config.ts` contains the `[rspack]` error. Follow all the steps below.
27
- - **From experimental rspack**: there is a root `rspack.config.ts` without the `[rspack]` error, and there is no root `webpack.config.*`. The config is already rspack but may use rspack 1 APIs. Skip step 2 and upgrade that file in place using steps 3 to 7.
28
- - **No custom config**: there is neither a root `webpack.config.*` nor a root `rspack.config.ts`. Only step 6 applies.
27
+ - **From experimental rspack**: there is a root `rspack.config.ts` without the `[rspack]` error, and there is no root `webpack.config.*`. The config is already rspack but may use rspack 1 APIs. Skip step 2 and upgrade that file in place using steps 3 to 8.
28
+ - **No custom config**: there is neither a root `webpack.config.*` nor a root `rspack.config.ts`. Only steps 6 and 7 apply.
29
29
 
30
30
  With a custom config, the build is expected to fail at this point with the `[rspack]` error. Do not chase build errors one by one; the steps below drive the work.
31
31
 
@@ -50,8 +50,7 @@ With a custom config, the build is expected to fail at this point with the `[rsp
50
50
  | `fork-ts-checker-webpack-plugin` | `TsCheckerRspackPlugin` from `ts-checker-rspack-plugin` |
51
51
  | `eslint-webpack-plugin`, `eslint-rspack-plugin`, or `diagnostics-webpack-plugin` used directly | `DiagnosticsRspackPlugin` from `./.config/rspack/DiagnosticsRspackPlugin.ts`, with `checks: [{ use: 'eslint', ... }]`. It wraps `diagnostics-webpack-plugin`, which fails under rspack on its own. `failOnError` and `severity` become `reportAs` |
52
52
  | `webpack-livereload-plugin` | already in the base config for development builds |
53
- | `mini-css-extract-plugin` | `CssExtractRspackPlugin` from `@rspack/core` |
54
- | `css-minimizer-webpack-plugin` | `LightningCssMinimizerRspackPlugin` from `@rspack/core` |
53
+ | `mini-css-extract-plugin`, `css-minimizer-webpack-plugin` | not needed: the base config already emits CSS files and minifies them with `LightningCssMinimizerRspackPlugin` |
55
54
  | `terser-webpack-plugin` added by the plugin | `SwcJsMinimizerRspackPlugin` from `@rspack/core`, or keep terser |
56
55
  | `html-webpack-plugin` | `HtmlRspackPlugin` from `@rspack/core` |
57
56
  | `tsconfig-paths-webpack-plugin` | the `resolve.tsConfig` option |
@@ -63,6 +62,7 @@ With a custom config, the build is expected to fail at this point with the `[rsp
63
62
 
64
63
  4. Fix code that depends on how the base config is built. Each of these works in webpack and silently does nothing, or throws, in rspack:
65
64
  - **Looking up a rule by loader name**, such as `rule.use.loader === 'swc-loader'`. The base config uses `builtin:swc-loader`, so match that. Prefer merging a rule over mutating the base one.
65
+ - **CSS rules**: the base config builds CSS natively with `type: 'css/auto'`. It has no `style-loader`, `css-loader` or `mini-css-extract-plugin`, and it emits `module.css`. Remove `style-loader`, `css-loader` and `mini-css-extract-plugin` from ported rules. Keep loaders that transform the CSS, such as `postcss-loader` for Tailwind or `sass-loader`, as `use` on a rule with `type: 'css/auto'`. Move `css-loader` `modules` options to `module.parser['css/auto']` and `module.generator['css/auto']`. A `mergeWithRules` keyed on `use.loader: 'style-loader'` or `'css-loader'` matches nothing.
66
66
  - **Looking up a plugin with `instanceof` or `constructor.name`** against webpack plugin classes, such as `CopyWebpackPlugin` or `LiveReloadPlugin`. Use the rspack class names, or merge instead of mutating.
67
67
  - **`webpack-merge` rules keyed on old names**: `unique('plugins', ['CopyPlugin'], …)` or `mergeWithRules` matching `use.loader: 'swc-loader'`. Update the names.
68
68
  - **A `filter` function on a `CopyWebpackPlugin` pattern**. `CopyRspackPlugin` has no `filter`; use `globOptions: { ignore: [...] }`. The base copy patterns come from `.config/bundler/copyFiles.ts`.
@@ -85,30 +85,32 @@ With a custom config, the build is expected to fail at this point with the `[rsp
85
85
  - The base asset rules only cover `png`, `jpg`, `jpeg`, `gif`, `svg` and font files. For other types, such as `webp`, add an `asset/resource` rule in `rspack.config.ts`.
86
86
  - Copy the file instead of importing it when the path is built at runtime, for example `` `${base}/img/${name}.svg` ``, or when something outside the bundle refers to it, such as the README, documentation or another plugin. Add a `CopyRspackPlugin` to `rspack.config.ts` for just those files, with a comment saying why they are copied. Do not copy whole folders to be safe: every copied file ends up in the plugin archive.
87
87
 
88
- 7. If something has no rspack equivalent, for example a monkey-patch of webpack internals, a custom `RuntimeModule` plugin, or an SWC Wasm plugin whose ABI does not match rspack's built-in SWC:
88
+ 7. List the plugin's own global stylesheets: `.css`, `.scss` and `.sass` files outside `node_modules` whose names don't end in `.module.css`, `.module.scss` or `.module.sass`. The base config loads them as plain CSS, so their rules apply to every page in Grafana, not only the plugin. Do not convert them to CSS modules or change the code that imports them. Name each file in your report, so the user can decide whether to scope it.
89
+
90
+ 8. If something has no rspack equivalent, for example a monkey-patch of webpack internals, a custom `RuntimeModule` plugin, or an SWC Wasm plugin whose ABI does not match rspack's built-in SWC:
89
91
  - Port what you can.
90
92
  - Comment out the rest, keeping the original code in the comment, so it does not run and the build does not fail.
91
93
  - Put a `TODO(rspack):` comment above it that says what the code did, why it has no direct equivalent, and a suggested approach.
92
94
  - Never drop it silently.
93
95
 
94
- 8. Fix any script the codemod reported as not rewritten so it calls `rspack` with `./rspack.config.ts` or `./.config/rspack/rspack.config.ts`, keeping its other commands and flags. Remove the CLI flags rspack does not support: `--progress`, `--color`, `--bail`, `--output-pathinfo`.
96
+ 9. Fix any script the codemod reported as not rewritten so it calls `rspack` with `./rspack.config.ts` or `./.config/rspack/rspack.config.ts`, keeping its other commands and flags. Remove the CLI flags rspack does not support: `--progress`, `--color`, `--bail`, `--output-pathinfo`.
95
97
 
96
- 9. For each file the codemod kept in `.config/webpack/`, copy what is still needed to a folder outside `.config/` (for example `build/`), port it to rspack, and update the imports. Do not delete the originals; list them so the user can delete them.
98
+ 10. For each file the codemod kept in `.config/webpack/`, copy what is still needed to a folder outside `.config/` (for example `build/`), port it to rspack, and update the imports. Do not delete the originals; list them so the user can delete them.
97
99
 
98
- 10. Remove the webpack packages the codemod kept, once nothing imports them. Keep `webpack` itself if anything still imports it.
100
+ 11. Remove the webpack packages the codemod kept, once nothing imports them. Keep `webpack` itself if anything still imports it.
99
101
 
100
- 11. Delete the root `webpack.config.*` and its helper files once everything in them is ported or commented with `TODO(rspack):`.
102
+ 12. Delete the root `webpack.config.*` and its helper files once everything in them is ported or commented with `TODO(rspack):`.
101
103
 
102
104
  ## Verify
103
105
 
104
106
  1. Install dependencies, then run the `typecheck` and `build` scripts. Both must pass, with any unportable code commented out.
105
- 2. Check that `dist/` contains `module.js`, `plugin.json`, and every file that plugin code or `plugin.json` refers to by path at runtime. Imported images are hashed, so their names change.
107
+ 2. Check that `dist/` contains `module.js`, `plugin.json`, `module.css` if the entry code imports any CSS, and every file that plugin code or `plugin.json` refers to by path at runtime. Imported images are hashed, so their names change.
106
108
  3. Run the `dev` script briefly and check that it compiles and keeps watching.
107
109
  4. Fix only breakage caused by this migration.
108
110
 
109
111
  ## Report
110
112
 
111
- End with a list of every `TODO(rspack):` comment you added (file, line, one-line reason), every file the user should delete, any duplicate transpiler rule you removed or kept (step 5), every image reference you changed to an import or kept as a copy (step 6), and any `exportsPresence` errors you left for the user.
113
+ End with a list of every `TODO(rspack):` comment you added (file, line, one-line reason), every file the user should delete, any duplicate transpiler rule you removed or kept (step 5), every image reference you changed to an import or kept as a copy (step 6), every global stylesheet (step 7), and any `exportsPresence` errors you left for the user.
112
114
 
113
115
  ## Out of scope
114
116
 
@@ -15,7 +15,8 @@ const RSPACK_CONFIG_FILES = [
15
15
  ".config/rspack/BuildModeRspackPlugin.ts",
16
16
  ".config/rspack/LiveReloadRspackPlugin.ts",
17
17
  ".config/rspack/LicenseRspackPlugin.ts",
18
- ".config/rspack/DiagnosticsRspackPlugin.ts"
18
+ ".config/rspack/DiagnosticsRspackPlugin.ts",
19
+ ".config/rspack/PluginCssRspackPlugin.ts"
19
20
  ];
20
21
  const RETIRED_RSPACK_FILES = [".config/rspack/liveReloadPlugin.ts"];
21
22
  const DOC_FILES = [".config/AGENTS/instructions.md", ".config/README.md"];
@@ -58,6 +59,7 @@ function rspack(context) {
58
59
  replaceExperimentalRspackFlag(context, { enable: true });
59
60
  followUps.push(...addRootRspackConfigStub(context));
60
61
  followUps.push(...updatePackageJson(context));
62
+ followUps.push(...raiseGrafanaDependency(context));
61
63
  followUps.push(...deleteWebpackTemplateFiles(context));
62
64
  reportFollowUps(context, followUps);
63
65
  return context;
@@ -171,6 +173,28 @@ function raiseNodeEngine(context) {
171
173
  const engines = { ...packageJson.engines, node: ">=22.23" };
172
174
  context.updateFile("package.json", JSON.stringify({ ...packageJson, engines }, null, 2));
173
175
  }
176
+ const MIN_GRAFANA_VERSION = "12.2.5";
177
+ function raiseGrafanaDependency(context) {
178
+ const pluginJsonPath = "src/plugin.json";
179
+ if (!context.doesFileExist(pluginJsonPath)) {
180
+ return [];
181
+ }
182
+ const pluginJson = readJsonFile(context, pluginJsonPath);
183
+ const grafanaRange = pluginJson.dependencies?.grafanaDependency;
184
+ if (typeof grafanaRange !== "string" || !validRange(grafanaRange)) {
185
+ return [];
186
+ }
187
+ const lowestAllowedVersion = minVersion(grafanaRange);
188
+ if (!lowestAllowedVersion || !lt(lowestAllowedVersion, MIN_GRAFANA_VERSION)) {
189
+ return [];
190
+ }
191
+ const grafanaDependency = `>=${MIN_GRAFANA_VERSION}`;
192
+ const dependencies = { ...pluginJson.dependencies, grafanaDependency };
193
+ context.updateFile(pluginJsonPath, JSON.stringify({ ...pluginJson, dependencies }, null, 2));
194
+ return [
195
+ `Raised grafanaDependency in src/plugin.json from ${grafanaRange} to ${grafanaDependency}. Earlier versions of Grafana do not provide react/jsx-runtime, which the rspack config loads from Grafana.`
196
+ ];
197
+ }
174
198
  const LOCKED_RSPACK_PACKAGE_PATTERN = /(^|\/)node_modules\/@rspack\//;
175
199
  function dropLockedRspackOnePackages(context) {
176
200
  if (!context.doesFileExist("package-lock.json")) {
@@ -220,14 +244,17 @@ const RSPACK_DEV_DEPENDENCIES = [
220
244
  "rspack-merge",
221
245
  "rspack-plugin-virtual-module",
222
246
  "ts-checker-rspack-plugin",
223
- // style-loader and imports-loader still need webpack, and diagnostics-webpack-plugin peers on webpack ^5.106
247
+ // diagnostics-webpack-plugin peers on webpack ^5.106
224
248
  "webpack",
225
249
  "ws"
226
250
  ];
227
251
  const WEBPACK_ONLY_DEV_DEPENDENCIES = [
228
252
  "copy-webpack-plugin",
253
+ "css-loader",
229
254
  "eslint-webpack-plugin",
230
255
  "fork-ts-checker-webpack-plugin",
256
+ "imports-loader",
257
+ "style-loader",
231
258
  "swc-loader",
232
259
  "terser-webpack-plugin",
233
260
  "webpack-cli",
@@ -235,15 +262,7 @@ const WEBPACK_ONLY_DEV_DEPENDENCIES = [
235
262
  "webpack-subresource-integrity",
236
263
  "webpack-virtual-modules"
237
264
  ];
238
- const BASE_CONFIG_DEV_DEPENDENCIES = [
239
- "@swc/helpers",
240
- "css-loader",
241
- "imports-loader",
242
- "replace-in-file-webpack-plugin",
243
- "sass",
244
- "sass-loader",
245
- "style-loader"
246
- ];
265
+ const BASE_CONFIG_DEV_DEPENDENCIES = ["@swc/helpers", "replace-in-file-webpack-plugin", "sass", "sass-loader"];
247
266
  const PACKAGE_JSON_TEMPLATE_DATA = {
248
267
  ...RSPACK_TEMPLATE_DATA_OVERRIDES,
249
268
  pluginName: "plugin",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/create-plugin",
3
- "version": "7.18.0",
3
+ "version": "7.19.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "directory": "packages/create-plugin",
@@ -24,8 +24,8 @@ What is left is the plugin's own build customisation, which needs judgement to p
24
24
  - No plugin code builds a runtime URL to a file the new base config no longer copies into `dist` (see step 6).
25
25
  3. Otherwise, work out the starting point:
26
26
  - **From webpack**: there is a root `webpack.config.*`, or `rspack.config.ts` contains the `[rspack]` error. Follow all the steps below.
27
- - **From experimental rspack**: there is a root `rspack.config.ts` without the `[rspack]` error, and there is no root `webpack.config.*`. The config is already rspack but may use rspack 1 APIs. Skip step 2 and upgrade that file in place using steps 3 to 7.
28
- - **No custom config**: there is neither a root `webpack.config.*` nor a root `rspack.config.ts`. Only step 6 applies.
27
+ - **From experimental rspack**: there is a root `rspack.config.ts` without the `[rspack]` error, and there is no root `webpack.config.*`. The config is already rspack but may use rspack 1 APIs. Skip step 2 and upgrade that file in place using steps 3 to 8.
28
+ - **No custom config**: there is neither a root `webpack.config.*` nor a root `rspack.config.ts`. Only steps 6 and 7 apply.
29
29
 
30
30
  With a custom config, the build is expected to fail at this point with the `[rspack]` error. Do not chase build errors one by one; the steps below drive the work.
31
31
 
@@ -50,8 +50,7 @@ With a custom config, the build is expected to fail at this point with the `[rsp
50
50
  | `fork-ts-checker-webpack-plugin` | `TsCheckerRspackPlugin` from `ts-checker-rspack-plugin` |
51
51
  | `eslint-webpack-plugin`, `eslint-rspack-plugin`, or `diagnostics-webpack-plugin` used directly | `DiagnosticsRspackPlugin` from `./.config/rspack/DiagnosticsRspackPlugin.ts`, with `checks: [{ use: 'eslint', ... }]`. It wraps `diagnostics-webpack-plugin`, which fails under rspack on its own. `failOnError` and `severity` become `reportAs` |
52
52
  | `webpack-livereload-plugin` | already in the base config for development builds |
53
- | `mini-css-extract-plugin` | `CssExtractRspackPlugin` from `@rspack/core` |
54
- | `css-minimizer-webpack-plugin` | `LightningCssMinimizerRspackPlugin` from `@rspack/core` |
53
+ | `mini-css-extract-plugin`, `css-minimizer-webpack-plugin` | not needed: the base config already emits CSS files and minifies them with `LightningCssMinimizerRspackPlugin` |
55
54
  | `terser-webpack-plugin` added by the plugin | `SwcJsMinimizerRspackPlugin` from `@rspack/core`, or keep terser |
56
55
  | `html-webpack-plugin` | `HtmlRspackPlugin` from `@rspack/core` |
57
56
  | `tsconfig-paths-webpack-plugin` | the `resolve.tsConfig` option |
@@ -63,6 +62,7 @@ With a custom config, the build is expected to fail at this point with the `[rsp
63
62
 
64
63
  4. Fix code that depends on how the base config is built. Each of these works in webpack and silently does nothing, or throws, in rspack:
65
64
  - **Looking up a rule by loader name**, such as `rule.use.loader === 'swc-loader'`. The base config uses `builtin:swc-loader`, so match that. Prefer merging a rule over mutating the base one.
65
+ - **CSS rules**: the base config builds CSS natively with `type: 'css/auto'`. It has no `style-loader`, `css-loader` or `mini-css-extract-plugin`, and it emits `module.css`. Remove `style-loader`, `css-loader` and `mini-css-extract-plugin` from ported rules. Keep loaders that transform the CSS, such as `postcss-loader` for Tailwind or `sass-loader`, as `use` on a rule with `type: 'css/auto'`. Move `css-loader` `modules` options to `module.parser['css/auto']` and `module.generator['css/auto']`. A `mergeWithRules` keyed on `use.loader: 'style-loader'` or `'css-loader'` matches nothing.
66
66
  - **Looking up a plugin with `instanceof` or `constructor.name`** against webpack plugin classes, such as `CopyWebpackPlugin` or `LiveReloadPlugin`. Use the rspack class names, or merge instead of mutating.
67
67
  - **`webpack-merge` rules keyed on old names**: `unique('plugins', ['CopyPlugin'], …)` or `mergeWithRules` matching `use.loader: 'swc-loader'`. Update the names.
68
68
  - **A `filter` function on a `CopyWebpackPlugin` pattern**. `CopyRspackPlugin` has no `filter`; use `globOptions: { ignore: [...] }`. The base copy patterns come from `.config/bundler/copyFiles.ts`.
@@ -85,30 +85,32 @@ With a custom config, the build is expected to fail at this point with the `[rsp
85
85
  - The base asset rules only cover `png`, `jpg`, `jpeg`, `gif`, `svg` and font files. For other types, such as `webp`, add an `asset/resource` rule in `rspack.config.ts`.
86
86
  - Copy the file instead of importing it when the path is built at runtime, for example `` `${base}/img/${name}.svg` ``, or when something outside the bundle refers to it, such as the README, documentation or another plugin. Add a `CopyRspackPlugin` to `rspack.config.ts` for just those files, with a comment saying why they are copied. Do not copy whole folders to be safe: every copied file ends up in the plugin archive.
87
87
 
88
- 7. If something has no rspack equivalent, for example a monkey-patch of webpack internals, a custom `RuntimeModule` plugin, or an SWC Wasm plugin whose ABI does not match rspack's built-in SWC:
88
+ 7. List the plugin's own global stylesheets: `.css`, `.scss` and `.sass` files outside `node_modules` whose names don't end in `.module.css`, `.module.scss` or `.module.sass`. The base config loads them as plain CSS, so their rules apply to every page in Grafana, not only the plugin. Do not convert them to CSS modules or change the code that imports them. Name each file in your report, so the user can decide whether to scope it.
89
+
90
+ 8. If something has no rspack equivalent, for example a monkey-patch of webpack internals, a custom `RuntimeModule` plugin, or an SWC Wasm plugin whose ABI does not match rspack's built-in SWC:
89
91
  - Port what you can.
90
92
  - Comment out the rest, keeping the original code in the comment, so it does not run and the build does not fail.
91
93
  - Put a `TODO(rspack):` comment above it that says what the code did, why it has no direct equivalent, and a suggested approach.
92
94
  - Never drop it silently.
93
95
 
94
- 8. Fix any script the codemod reported as not rewritten so it calls `rspack` with `./rspack.config.ts` or `./.config/rspack/rspack.config.ts`, keeping its other commands and flags. Remove the CLI flags rspack does not support: `--progress`, `--color`, `--bail`, `--output-pathinfo`.
96
+ 9. Fix any script the codemod reported as not rewritten so it calls `rspack` with `./rspack.config.ts` or `./.config/rspack/rspack.config.ts`, keeping its other commands and flags. Remove the CLI flags rspack does not support: `--progress`, `--color`, `--bail`, `--output-pathinfo`.
95
97
 
96
- 9. For each file the codemod kept in `.config/webpack/`, copy what is still needed to a folder outside `.config/` (for example `build/`), port it to rspack, and update the imports. Do not delete the originals; list them so the user can delete them.
98
+ 10. For each file the codemod kept in `.config/webpack/`, copy what is still needed to a folder outside `.config/` (for example `build/`), port it to rspack, and update the imports. Do not delete the originals; list them so the user can delete them.
97
99
 
98
- 10. Remove the webpack packages the codemod kept, once nothing imports them. Keep `webpack` itself if anything still imports it.
100
+ 11. Remove the webpack packages the codemod kept, once nothing imports them. Keep `webpack` itself if anything still imports it.
99
101
 
100
- 11. Delete the root `webpack.config.*` and its helper files once everything in them is ported or commented with `TODO(rspack):`.
102
+ 12. Delete the root `webpack.config.*` and its helper files once everything in them is ported or commented with `TODO(rspack):`.
101
103
 
102
104
  ## Verify
103
105
 
104
106
  1. Install dependencies, then run the `typecheck` and `build` scripts. Both must pass, with any unportable code commented out.
105
- 2. Check that `dist/` contains `module.js`, `plugin.json`, and every file that plugin code or `plugin.json` refers to by path at runtime. Imported images are hashed, so their names change.
107
+ 2. Check that `dist/` contains `module.js`, `plugin.json`, `module.css` if the entry code imports any CSS, and every file that plugin code or `plugin.json` refers to by path at runtime. Imported images are hashed, so their names change.
106
108
  3. Run the `dev` script briefly and check that it compiles and keeps watching.
107
109
  4. Fix only breakage caused by this migration.
108
110
 
109
111
  ## Report
110
112
 
111
- End with a list of every `TODO(rspack):` comment you added (file, line, one-line reason), every file the user should delete, any duplicate transpiler rule you removed or kept (step 5), every image reference you changed to an import or kept as a copy (step 6), and any `exportsPresence` errors you left for the user.
113
+ End with a list of every `TODO(rspack):` comment you added (file, line, one-line reason), every file the user should delete, any duplicate transpiler rule you removed or kept (step 5), every image reference you changed to an import or kept as a copy (step 6), every global stylesheet (step 7), and any `exportsPresence` errors you left for the user.
112
114
 
113
115
  ## Out of scope
114
116
 
@@ -222,6 +222,7 @@ describe('rspack', () => {
222
222
  expect(result.doesFileExist('.config/rspack/liveReloadPlugin.ts')).toBe(false);
223
223
  expect(result.doesFileExist('.config/rspack/LicenseRspackPlugin.ts')).toBe(true);
224
224
  expect(result.doesFileExist('.config/rspack/DiagnosticsRspackPlugin.ts')).toBe(true);
225
+ expect(result.doesFileExist('.config/rspack/PluginCssRspackPlugin.ts')).toBe(true);
225
226
  });
226
227
  });
227
228
 
@@ -245,7 +246,7 @@ describe('rspack', () => {
245
246
  const result = addRspack(context);
246
247
  const externals = result.getFile('.config/bundler/externals.ts')!;
247
248
 
248
- expect(externals).toContain('RspackOptions');
249
+ expect(externals).toContain('ExternalItem[]');
249
250
  });
250
251
 
251
252
  it('should re-render all bundler files', () => {
@@ -253,7 +254,7 @@ describe('rspack', () => {
253
254
 
254
255
  const result = addRspack(context);
255
256
 
256
- expect(result.getFile('.config/bundler/externals.ts')).toContain('RspackOptions');
257
+ expect(result.getFile('.config/bundler/externals.ts')).toContain('ExternalItem[]');
257
258
  expect(result.getFile('.config/bundler/constants.ts')).toBe('// rendered template stub');
258
259
  expect(result.getFile('.config/bundler/copyFiles.ts')).toBe('// rendered template stub');
259
260
  expect(result.getFile('.config/bundler/utils.ts')).toBe('// rendered template stub');
@@ -314,7 +315,45 @@ describe('rspack', () => {
314
315
 
315
316
  // builtin:swc-loader runs with externalHelpers, so builds import @swc/helpers
316
317
  expect(pkg.devDependencies['@swc/helpers']).toBe(rspackDeps['@swc/helpers']);
317
- expect(pkg.devDependencies['imports-loader']).toBe(rspackDeps['imports-loader']);
318
+ expect(pkg.devDependencies['sass-loader']).toBe(rspackDeps['sass-loader']);
319
+ });
320
+
321
+ it('should remove the css loaders that native css replaces', () => {
322
+ const context = createBaseContext();
323
+ const pkg = JSON.parse(context.getFile('package.json')!);
324
+ Object.assign(pkg.devDependencies, {
325
+ 'css-loader': '^7.1.0',
326
+ 'imports-loader': '^5.0.0',
327
+ 'style-loader': '^4.0.0',
328
+ });
329
+ context.updateFile('package.json', JSON.stringify(pkg, null, 2));
330
+
331
+ const result = addRspack(context);
332
+ const updated = JSON.parse(result.getFile('package.json')!);
333
+
334
+ expect(updated.devDependencies['css-loader']).toBeUndefined();
335
+ expect(updated.devDependencies['imports-loader']).toBeUndefined();
336
+ expect(updated.devDependencies['style-loader']).toBeUndefined();
337
+ });
338
+
339
+ it('should keep css loaders a root build file still uses and report them', () => {
340
+ const context = createBaseContext();
341
+ const pkg = JSON.parse(context.getFile('package.json')!);
342
+ Object.assign(pkg.devDependencies, { 'css-loader': '^7.1.0', 'style-loader': '^4.0.0' });
343
+ context.updateFile('package.json', JSON.stringify(pkg, null, 2));
344
+ context.addFile(
345
+ 'webpack.config.ts',
346
+ "export default { module: { rules: [{ use: ['style-loader', 'css-loader'] }] } };"
347
+ );
348
+
349
+ const result = addRspack(context);
350
+ const updated = JSON.parse(result.getFile('package.json')!);
351
+
352
+ expect(updated.devDependencies['css-loader']).toBe('^7.1.0');
353
+ expect(updated.devDependencies['style-loader']).toBe('^4.0.0');
354
+ const report = result.getMessage()?.body?.join('\n');
355
+ expect(report).toContain('css-loader');
356
+ expect(report).toContain('style-loader');
318
357
  });
319
358
 
320
359
  it('should not change the versions of base config packages the plugin already has', () => {
@@ -583,6 +622,51 @@ describe('rspack', () => {
583
622
  });
584
623
  });
585
624
 
625
+ describe('grafanaDependency', () => {
626
+ function runWithGrafanaDependency(dependencies?: Record<string, string>) {
627
+ const context = createBaseContext();
628
+ if (dependencies) {
629
+ context.addFile('src/plugin.json', JSON.stringify({ id: 'myorg-mypanel-panel', dependencies }, null, 2));
630
+ }
631
+ const result = addRspack(context);
632
+ const pluginJson = result.getFile('src/plugin.json');
633
+ return { result, dependencies: pluginJson ? JSON.parse(pluginJson).dependencies : undefined };
634
+ }
635
+
636
+ it('should raise grafanaDependency when it allows versions without react/jsx-runtime', () => {
637
+ const { result, dependencies } = runWithGrafanaDependency({ grafanaDependency: '>=10.0.0', plugins: '[]' });
638
+
639
+ expect(dependencies).toEqual({ grafanaDependency: '>=12.2.5', plugins: '[]' });
640
+ expect(result.getMessage()?.body?.join('\n')).toContain('>=12.2.5');
641
+ });
642
+
643
+ it('should raise grafanaDependency ranges that include backported versions', () => {
644
+ expect(runWithGrafanaDependency({ grafanaDependency: '>=11.6.11 <12 || >=12.2.5' }).dependencies).toEqual({
645
+ grafanaDependency: '>=12.2.5',
646
+ });
647
+ });
648
+
649
+ it('should keep grafanaDependency when it already requires a newer Grafana', () => {
650
+ expect(runWithGrafanaDependency({ grafanaDependency: '>=13.2.0' }).dependencies).toEqual({
651
+ grafanaDependency: '>=13.2.0',
652
+ });
653
+ expect(runWithGrafanaDependency({ grafanaDependency: '>=12.2.5' }).dependencies).toEqual({
654
+ grafanaDependency: '>=12.2.5',
655
+ });
656
+ });
657
+
658
+ it('should not fail when the plugin has no src/plugin.json', () => {
659
+ expect(runWithGrafanaDependency().dependencies).toBeUndefined();
660
+ });
661
+
662
+ it('should be idempotent', async () => {
663
+ const context = createBaseContext();
664
+ context.addFile('src/plugin.json', JSON.stringify({ dependencies: { grafanaDependency: '>=10.0.0' } }, null, 2));
665
+
666
+ await expect(addRspack).toBeIdempotent(context);
667
+ });
668
+ });
669
+
586
670
  describe('npm lockfile', () => {
587
671
  function createLockfile(rspackCoreVersion: string) {
588
672
  return JSON.stringify(
@@ -24,6 +24,7 @@ const RSPACK_CONFIG_FILES = [
24
24
  '.config/rspack/LiveReloadRspackPlugin.ts',
25
25
  '.config/rspack/LicenseRspackPlugin.ts',
26
26
  '.config/rspack/DiagnosticsRspackPlugin.ts',
27
+ '.config/rspack/PluginCssRspackPlugin.ts',
27
28
  ];
28
29
 
29
30
  // files the rspack template used to ship under another name. removed so they do not linger
@@ -80,6 +81,7 @@ export default function rspack(context: Context): Context {
80
81
 
81
82
  followUps.push(...addRootRspackConfigStub(context));
82
83
  followUps.push(...updatePackageJson(context));
84
+ followUps.push(...raiseGrafanaDependency(context));
83
85
  followUps.push(...deleteWebpackTemplateFiles(context));
84
86
  reportFollowUps(context, followUps);
85
87
 
@@ -232,6 +234,34 @@ function raiseNodeEngine(context: Context): void {
232
234
  context.updateFile('package.json', JSON.stringify({ ...packageJson, engines }, null, 2));
233
235
  }
234
236
 
237
+ // the base config externalizes react/jsx-runtime, which Grafana provides from 12.2.5
238
+ const MIN_GRAFANA_VERSION = '12.2.5';
239
+
240
+ function raiseGrafanaDependency(context: Context): string[] {
241
+ const pluginJsonPath = 'src/plugin.json';
242
+ if (!context.doesFileExist(pluginJsonPath)) {
243
+ return [];
244
+ }
245
+
246
+ const pluginJson = readJsonFile<{ dependencies?: Record<string, unknown> }>(context, pluginJsonPath);
247
+ const grafanaRange = pluginJson.dependencies?.grafanaDependency;
248
+ if (typeof grafanaRange !== 'string' || !validRange(grafanaRange)) {
249
+ return [];
250
+ }
251
+
252
+ const lowestAllowedVersion = minVersion(grafanaRange);
253
+ if (!lowestAllowedVersion || !lt(lowestAllowedVersion, MIN_GRAFANA_VERSION)) {
254
+ return [];
255
+ }
256
+
257
+ const grafanaDependency = `>=${MIN_GRAFANA_VERSION}`;
258
+ const dependencies = { ...pluginJson.dependencies, grafanaDependency };
259
+ context.updateFile(pluginJsonPath, JSON.stringify({ ...pluginJson, dependencies }, null, 2));
260
+ return [
261
+ `Raised grafanaDependency in src/plugin.json from ${grafanaRange} to ${grafanaDependency}. Earlier versions of Grafana do not provide react/jsx-runtime, which the rspack config loads from Grafana.`,
262
+ ];
263
+ }
264
+
235
265
  // npm refuses to move @rspack/cli and @rspack/core from 1 to 2 in one install while the lockfile
236
266
  // still pins the old versions (ERESOLVE). dropping the locked @rspack packages lets npm resolve
237
267
  // them again from package.json. pnpm and yarn upgrade them without help
@@ -310,7 +340,7 @@ export const RSPACK_DEV_DEPENDENCIES = [
310
340
  'rspack-merge',
311
341
  'rspack-plugin-virtual-module',
312
342
  'ts-checker-rspack-plugin',
313
- // style-loader and imports-loader still need webpack, and diagnostics-webpack-plugin peers on webpack ^5.106
343
+ // diagnostics-webpack-plugin peers on webpack ^5.106
314
344
  'webpack',
315
345
  'ws',
316
346
  ];
@@ -318,8 +348,11 @@ export const RSPACK_DEV_DEPENDENCIES = [
318
348
  // packages only the webpack base config uses. removed unless a root build file still imports them
319
349
  export const WEBPACK_ONLY_DEV_DEPENDENCIES = [
320
350
  'copy-webpack-plugin',
351
+ 'css-loader',
321
352
  'eslint-webpack-plugin',
322
353
  'fork-ts-checker-webpack-plugin',
354
+ 'imports-loader',
355
+ 'style-loader',
323
356
  'swc-loader',
324
357
  'terser-webpack-plugin',
325
358
  'webpack-cli',
@@ -331,15 +364,7 @@ export const WEBPACK_ONLY_DEV_DEPENDENCIES = [
331
364
  // packages the rendered .config/rspack and .config/bundler files load at build time that every
332
365
  // scaffold gets, so older plugins may have removed some of them. added only when missing.
333
366
  // @swc/helpers is needed because builtin:swc-loader runs with externalHelpers, which webpack did not
334
- export const BASE_CONFIG_DEV_DEPENDENCIES = [
335
- '@swc/helpers',
336
- 'css-loader',
337
- 'imports-loader',
338
- 'replace-in-file-webpack-plugin',
339
- 'sass',
340
- 'sass-loader',
341
- 'style-loader',
342
- ];
367
+ export const BASE_CONFIG_DEV_DEPENDENCIES = ['@swc/helpers', 'replace-in-file-webpack-plugin', 'sass', 'sass-loader'];
343
368
 
344
369
  // the lists above say which packages the addition manages. their versions come from the package.json
345
370
  // template, so they always match the .config files rendered from that same template. the template is
@@ -1,7 +1,7 @@
1
1
  {{#unless useRspack}}import type { Configuration, ExternalItemFunctionData } from 'webpack';{{/unless}}{{#if useRspack}}
2
- import type { RspackOptions, ExternalItemFunctionData } from '@rspack/core';{{/if}}
2
+ import type { ExternalItem, ExternalItemFunctionData } from '@rspack/core';{{/if}}
3
3
 
4
- {{#unless useRspack}} type ExternalsType = Configuration['externals'];{{/unless}}{{#if useRspack}}type ExternalsType = RspackOptions['externals'];{{/if}}
4
+ {{#unless useRspack}} type ExternalsType = Configuration['externals'];{{/unless}}{{#if useRspack}}type ExternalsType = ExternalItem[];{{/if}}
5
5
 
6
6
  export const externals: ExternalsType = [
7
7
  // Required for dynamic publicPath resolution
@@ -0,0 +1,103 @@
1
+ import { posix } from 'node:path';
2
+ import { Compilation, type Compiler } from '@rspack/core';
3
+
4
+ const PLUGIN_NAME = 'PluginCssRspackPlugin';
5
+ const URL_PATTERN = /url\(\s*(["']?)([^"')]+)\1\s*\)/g;
6
+ const CHARSET_PATTERN = /^@charset[^;]+;/;
7
+ const MODULE_CSS_DEPENDENCY = '"./module.css"';
8
+ // the AMD wrapper rspack writes before minification: define([deps], function(params){
9
+ const AMD_WRAPPER_PATTERN = /^define\(\[([^\]]*)\], function\(([^)]*)\)\{/;
10
+
11
+ interface PluginCssRspackPluginOptions {
12
+ pluginId: string;
13
+ publicPath: string;
14
+ }
15
+
16
+ // Prepares the CSS Grafana loads as module.css and the CSS of lazy chunks.
17
+ export class PluginCssRspackPlugin {
18
+ options: PluginCssRspackPluginOptions;
19
+
20
+ constructor(options: PluginCssRspackPluginOptions) {
21
+ this.options = options;
22
+ }
23
+
24
+ apply(compiler: Compiler) {
25
+ const { ConcatSource, ReplaceSource } = compiler.rspack.sources;
26
+ const { pluginId, publicPath } = this.options;
27
+ const layer = `plugin-${pluginId}`;
28
+
29
+ compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
30
+ compilation.hooks.processAssets.tap(
31
+ {
32
+ name: PLUGIN_NAME,
33
+ // before the CSS minimizer (PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE) so it minifies the result
34
+ stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,
35
+ },
36
+ (assets) => {
37
+ // a 404 on ./module.css fails the whole plugin load, so entries without CSS drop the dependency
38
+ for (const [entryName, entrypoint] of compilation.entrypoints) {
39
+ const files = entrypoint.getFiles().map((file) => file.split('?')[0]);
40
+ const jsFile = `${entryName}.js`;
41
+ if (files.includes(`${entryName}.css`) || !assets[jsFile]) {
42
+ continue;
43
+ }
44
+
45
+ const source = assets[jsFile];
46
+ const js = source.source().toString();
47
+ if (!js.includes(MODULE_CSS_DEPENDENCY)) {
48
+ continue;
49
+ }
50
+
51
+ const wrapper = js.match(AMD_WRAPPER_PATTERN);
52
+ const dependencies = wrapper?.[1].split(',') ?? [];
53
+ const params = wrapper?.[2].split(',').map((param) => param.trim()) ?? [];
54
+ const index = dependencies.indexOf(MODULE_CSS_DEPENDENCY);
55
+ if (!wrapper || index === -1 || dependencies.length !== params.length) {
56
+ throw new Error(`${PLUGIN_NAME}: could not remove ./module.css from the define() wrapper in ${jsFile}.`);
57
+ }
58
+
59
+ // the bundle still reads the external's parameter, so it becomes an undefined local
60
+ const [param] = params.splice(index, 1);
61
+ dependencies.splice(index, 1);
62
+ const updated = new ReplaceSource(source);
63
+ updated.replace(
64
+ 0,
65
+ wrapper[0].length - 1,
66
+ `define([${dependencies.join(',')}], function(${params.join(', ')}){var ${param};`
67
+ );
68
+ compilation.updateAsset(jsFile, updated);
69
+ }
70
+
71
+ for (const assetName of Object.keys(assets)) {
72
+ const fileName = assetName.split('?')[0];
73
+ if (!fileName.endsWith('.css')) {
74
+ continue;
75
+ }
76
+
77
+ const source = assets[assetName];
78
+ const css = source.source().toString();
79
+ const updated = new ReplaceSource(source);
80
+
81
+ // SystemJS and the browser resolve url() against the CSS file, not the page
82
+ for (const match of css.matchAll(URL_PATTERN)) {
83
+ const url = match[2];
84
+ if (!url.startsWith(publicPath)) {
85
+ continue;
86
+ }
87
+ const relativeUrl = posix.relative(posix.dirname(fileName), url.slice(publicPath.length));
88
+ const start = match.index + match[0].indexOf(url);
89
+ updated.replace(start, start + url.length - 1, relativeUrl);
90
+ }
91
+
92
+ // the layer keeps Grafana's unlayered styles winning at equal specificity. @charset must stay first
93
+ const charset = css.match(CHARSET_PATTERN)?.[0];
94
+ if (charset) {
95
+ updated.replace(0, charset.length - 1, '');
96
+ }
97
+ compilation.updateAsset(assetName, new ConcatSource(charset ?? '', `@layer ${layer}{`, updated, '}'));
98
+ }
99
+ }
100
+ );
101
+ });
102
+ }
103
+ }
@@ -9,6 +9,7 @@ import {
9
9
  BannerPlugin,
10
10
  Compilation,
11
11
  CopyRspackPlugin,
12
+ LightningCssMinimizerRspackPlugin,
12
13
  SubresourceIntegrityPlugin,
13
14
  SwcJsMinimizerRspackPlugin,
14
15
  type Configuration,
@@ -22,6 +23,7 @@ import RspackLiveReloadPlugin from './LiveReloadRspackPlugin.ts';
22
23
  import { BuildModeRspackPlugin } from './BuildModeRspackPlugin.ts';
23
24
  import { LicenseRspackPlugin } from './LicenseRspackPlugin.ts';
24
25
  import DiagnosticsRspackPlugin from './DiagnosticsRspackPlugin.ts';
26
+ import { PluginCssRspackPlugin } from './PluginCssRspackPlugin.ts';
25
27
  import { DIST_DIR, SOURCE_DIR } from '../bundler/constants.ts';
26
28
  import { getCPConfigVersion, getEntries, getPackageJson, getPluginJson, isWSL } from '../bundler/utils.ts';
27
29
  import { externals } from '../bundler/externals.ts';
@@ -52,9 +54,15 @@ const config = async (env: Env): Promise<Configuration> => {
52
54
 
53
55
  devtool: env.production ? 'source-map' : 'eval-source-map',
54
56
 
55
- entry: await getEntries(),
57
+ // grafana-public-path sets the public path first. grafana-plugin-css adds ./module.css to the define() dependencies
58
+ entry: Object.fromEntries(
59
+ Object.entries(await getEntries()).map(([name, entryPath]) => [
60
+ name,
61
+ ['grafana-public-path', 'grafana-plugin-css', entryPath],
62
+ ])
63
+ ),
56
64
 
57
- externals,
65
+ externals: [...externals, { 'grafana-plugin-css': './module.css' }],
58
66
 
59
67
  // Support WebAssembly according to latest spec - makes WebAssembly module async
60
68
  experiments: {
@@ -64,19 +72,15 @@ const config = async (env: Env): Promise<Configuration> => {
64
72
  mode: env.production ? 'production' : 'development',
65
73
 
66
74
  module: {
75
+ parser: {
76
+ // keeps default imports of CSS modules working: `import styles from './x.module.css'`
77
+ 'css/auto': { namedExports: false },
78
+ },
79
+ generator: {
80
+ // class names of CSS modules start with the plugin id
81
+ 'css/auto': { localIdentName: '[uniqueName]-[local]-[hash:6]' },
82
+ },
67
83
  rules: [
68
- // This must come first in the rules array otherwise it breaks sourcemaps.
69
- {
70
- test: /src\/(?:.*\/)?module\.tsx?$/,
71
- use: [
72
- {
73
- loader: 'imports-loader',
74
- options: {
75
- imports: `side-effects grafana-public-path`,
76
- },
77
- },
78
- ],
79
- },
80
84
  {
81
85
  exclude: /(node_modules)/,
82
86
  test: /\.[tj]sx?$/,
@@ -103,11 +107,12 @@ const config = async (env: Env): Promise<Configuration> => {
103
107
  },
104
108
  {
105
109
  test: /\.css$/,
106
- use: ['style-loader', 'css-loader'],
110
+ type: 'css/auto',
107
111
  },
108
112
  {
109
113
  test: /\.s[ac]ss$/,
110
- use: ['style-loader', 'css-loader', 'sass-loader'],
114
+ use: ['sass-loader'],
115
+ type: 'css/auto',
111
116
  },
112
117
  {
113
118
  test: /\.(png|jpe?g|gif|svg)$/,
@@ -128,6 +133,18 @@ const config = async (env: Env): Promise<Configuration> => {
128
133
 
129
134
  optimization: {
130
135
  minimize: Boolean(env.production),
136
+ splitChunks: {
137
+ cacheGroups: {
138
+ // CSS used by more than one lazy chunk goes into one shared file instead of a copy per chunk
139
+ sharedCss: {
140
+ type: /^css/,
141
+ chunks: 'async',
142
+ minChunks: 2,
143
+ minSize: 0,
144
+ priority: 10,
145
+ },
146
+ },
147
+ },
131
148
  minimizer: [
132
149
  new SwcJsMinimizerRspackPlugin({
133
150
  extractComments: { banner: false },
@@ -136,6 +153,7 @@ const config = async (env: Env): Promise<Configuration> => {
136
153
  compress: { pure_funcs: ['console.log', 'console.info'] },
137
154
  },
138
155
  }),
156
+ new LightningCssMinimizerRspackPlugin(),
139
157
  ],
140
158
  },
141
159
 
@@ -145,6 +163,8 @@ const config = async (env: Env): Promise<Configuration> => {
145
163
  },
146
164
  filename: '[name].js',
147
165
  chunkFilename: env.production ? '[name].js?_cache=[contenthash]' : '[name].js',
166
+ cssFilename: '[name].css',
167
+ cssChunkFilename: env.production ? '[name].css?_cache=[contenthash]' : '[name].css',
148
168
  library: {
149
169
  type: 'amd',
150
170
  },
@@ -157,6 +177,7 @@ const config = async (env: Env): Promise<Configuration> => {
157
177
  plugins: [
158
178
  new BuildModeRspackPlugin(),
159
179
  new LicenseRspackPlugin(),
180
+ new PluginCssRspackPlugin({ pluginId, publicPath: `public/plugins/${pluginId}/` }),
160
181
  virtualPublicPath,
161
182
  // Insert create plugin version information into the bundle
162
183
  new BannerPlugin({
@@ -166,6 +187,7 @@ const config = async (env: Env): Promise<Configuration> => {
166
187
  ].join('\n'),
167
188
  raw: true,
168
189
  entryOnly: true,
190
+ test: /\.js$/,
169
191
  // after minification to prevent banner removal and before source maps and hashes are generated so they include it.
170
192
  stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE + 1,
171
193
  }),
@@ -36,8 +36,8 @@
36
36
  "@types/ws": "^8.18.1",{{/if}}
37
37
  "@typescript-eslint/eslint-plugin": "^8.38.0",
38
38
  "@typescript-eslint/parser": "^8.38.0",{{#unless useRspack}}
39
- "copy-webpack-plugin": "^14.0.0",{{/unless}}
40
- "css-loader": "^7.1.0",
39
+ "copy-webpack-plugin": "^14.0.0",
40
+ "css-loader": "^7.1.0",{{/unless}}
41
41
  "diagnostics-webpack-plugin": "^1.0.1",
42
42
  "eslint": "^9.32.0",
43
43
  "eslint-config-prettier": "^10.1.0",
@@ -45,8 +45,8 @@
45
45
  "eslint-plugin-react": "^7.37.0",
46
46
  "eslint-plugin-react-hooks": "^7.1.0",{{#unless useRspack}}
47
47
  "fork-ts-checker-webpack-plugin": "^9.1.0",{{/unless}}
48
- "identity-obj-proxy": "^3.0.0",
49
- "imports-loader": "^5.0.0",
48
+ "identity-obj-proxy": "^3.0.0",{{#unless useRspack}}
49
+ "imports-loader": "^5.0.0",{{/unless}}
50
50
  "intersection-observer": "^0.12.2",
51
51
  "jest": "^30.5.0",
52
52
  "jest-canvas-mock": "^2.5.2",
@@ -57,8 +57,8 @@
57
57
  "rspack-plugin-virtual-module": "^1.0.0",{{/if}}
58
58
  "sass": "^1.89.0",
59
59
  "sass-loader": "^16.0.0",
60
- "semver": "^7.7.0",
61
- "style-loader": "^4.0.0",{{#unless useRspack}}
60
+ "semver": "^7.7.0",{{#unless useRspack}}
61
+ "style-loader": "^4.0.0",
62
62
  "swc-loader": "^0.2.0",
63
63
  "terser-webpack-plugin": "^5.3.0",{{/unless}}
64
64
  "ts-node": "^10.9.0",{{#if useRspack}}