@grafana/create-plugin 7.16.0 → 7.18.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/codemods/additions/additions.js +7 -0
  3. package/dist/codemods/additions/prompts/rspack.md +118 -0
  4. package/dist/codemods/additions/scripts/docs.js +2 -2
  5. package/dist/codemods/additions/scripts/rspack.js +322 -0
  6. package/dist/codemods/agentic/prompts.js +1 -1
  7. package/dist/codemods/migrations/migrations.js +13 -1
  8. package/dist/codemods/migrations/scripts/019-remove-glob-dependency.js +93 -0
  9. package/dist/codemods/migrations/scripts/020-rename-rspack-feature-flag.js +8 -0
  10. package/dist/codemods/utils.ast.js +44 -1
  11. package/dist/codemods/utils.rspack.js +38 -0
  12. package/dist/commands/generate.command.js +1 -9
  13. package/dist/constants.js +0 -1
  14. package/dist/utils/utils.templates.js +4 -4
  15. package/package.json +1 -1
  16. package/src/codemods/additions/additions.test.ts +9 -0
  17. package/src/codemods/additions/additions.ts +7 -0
  18. package/src/codemods/additions/prompts/rspack.md +118 -0
  19. package/src/codemods/additions/scripts/docs.test.ts +4 -4
  20. package/src/codemods/additions/scripts/docs.ts +2 -2
  21. package/src/codemods/additions/scripts/rspack.test.ts +768 -0
  22. package/src/codemods/additions/scripts/rspack.ts +428 -0
  23. package/src/codemods/agentic/prompts.test.ts +11 -2
  24. package/src/codemods/agentic/prompts.ts +1 -1
  25. package/src/codemods/migrations/migrations.ts +14 -1
  26. package/src/codemods/migrations/scripts/019-remove-glob-dependency.test.ts +146 -0
  27. package/src/codemods/migrations/scripts/019-remove-glob-dependency.ts +121 -0
  28. package/src/codemods/migrations/scripts/020-rename-rspack-feature-flag.test.ts +94 -0
  29. package/src/codemods/migrations/scripts/020-rename-rspack-feature-flag.ts +8 -0
  30. package/src/codemods/utils.ast.test.ts +151 -0
  31. package/src/codemods/utils.ast.ts +60 -0
  32. package/src/codemods/utils.rspack.ts +58 -0
  33. package/src/codemods/utils.test.ts +5 -2
  34. package/src/commands/generate.command.ts +2 -10
  35. package/src/constants.ts +0 -1
  36. package/src/types.ts +1 -1
  37. package/src/utils/tests/utils.config.test.ts +10 -0
  38. package/src/utils/utils.config.ts +2 -1
  39. package/src/utils/utils.templates.ts +4 -4
  40. package/templates/common/.config/AGENTS/instructions.md +2 -2
  41. package/templates/common/.config/README.md +56 -0
  42. package/templates/common/.config/bundler/externals.ts +2 -2
  43. package/templates/common/.config/bundler/utils.ts +10 -9
  44. package/templates/common/.config/rspack/DiagnosticsRspackPlugin.ts +29 -0
  45. package/templates/common/.config/rspack/LicenseRspackPlugin.ts +43 -0
  46. package/templates/common/.config/rspack/LiveReloadRspackPlugin.ts +95 -0
  47. package/templates/common/.config/rspack/rspack.config.ts +41 -32
  48. package/templates/common/.cprc.json +1 -2
  49. package/templates/common/_package.json +19 -20
  50. package/templates/common/.config/rspack/liveReloadPlugin.ts +0 -133
@@ -0,0 +1,768 @@
1
+ import { Context } from '../../context.js';
2
+ import addRspack, {
3
+ BASE_CONFIG_DEV_DEPENDENCIES,
4
+ getTemplateDevDependencyVersions,
5
+ RSPACK_DEV_DEPENDENCIES,
6
+ WEBPACK_ONLY_DEV_DEPENDENCIES,
7
+ } from './rspack.js';
8
+
9
+ vi.mock(import('../../../utils/utils.plugin.js'), async (importOriginal) => {
10
+ const originalModule = await importOriginal();
11
+ return {
12
+ ...originalModule,
13
+ getPluginJson: () => ({
14
+ id: 'my-plugin-id',
15
+ name: 'My Plugin',
16
+ type: 'panel',
17
+ info: { author: { name: 'my-author' } },
18
+ }),
19
+ };
20
+ });
21
+
22
+ vi.mock(import('../../../utils/utils.config.js'), async (importOriginal) => {
23
+ const originalModule = await importOriginal();
24
+ return {
25
+ ...originalModule,
26
+ getConfig: () => ({ version: '5.0.0', features: {} }),
27
+ };
28
+ });
29
+
30
+ vi.mock(import('../../utils.js'), async (importOriginal) => {
31
+ const originalModule = await importOriginal();
32
+
33
+ // Only render externals.ts from the real template since we assert on its content.
34
+ // All other templates just need a non-empty stub.
35
+ return {
36
+ ...originalModule,
37
+ renderTemplate: (path: string, includeWarning?: boolean, data?: Record<string, unknown>) => {
38
+ if (path.includes('.config/bundler/externals.ts')) {
39
+ return originalModule.renderTemplate(path, includeWarning, data);
40
+ }
41
+ return '// rendered template stub';
42
+ },
43
+ };
44
+ });
45
+
46
+ const RSPACK_TEMPLATE_DATA = { useRspack: true, frontendBundler: 'rspack' };
47
+ const WEBPACK_TEMPLATE_DATA = { useRspack: false, frontendBundler: 'webpack' };
48
+
49
+ async function getTemplateDevDependencies(templateData: Record<string, unknown>): Promise<Record<string, string>> {
50
+ const { renderTemplate } = await vi.importActual<typeof import('../../utils.js')>('../../utils.js');
51
+ const { getTemplateData } = await vi.importActual<typeof import('../../../utils/utils.templates.js')>(
52
+ '../../../utils/utils.templates.js'
53
+ );
54
+ const packageJsonTemplatePath = new URL('../../../../templates/common/_package.json', import.meta.url).pathname;
55
+ const { devDependencies } = JSON.parse(
56
+ renderTemplate(packageJsonTemplatePath, false, { ...getTemplateData(), ...templateData })
57
+ );
58
+ return devDependencies;
59
+ }
60
+
61
+ function createBaseContext(): Context {
62
+ const context = new Context('/virtual');
63
+
64
+ context.addFile('.config/webpack/webpack.config.ts', '');
65
+ context.addFile('.config/webpack/BuildModeWebpackPlugin.ts', '');
66
+ context.addFile('.config/bundler/externals.ts', '');
67
+ context.addFile('.config/bundler/constants.ts', '');
68
+ context.addFile('.config/bundler/copyFiles.ts', '');
69
+ context.addFile('.config/bundler/utils.ts', '');
70
+
71
+ context.addFile(
72
+ 'package.json',
73
+ JSON.stringify(
74
+ {
75
+ scripts: {
76
+ build: 'webpack -c ./.config/webpack/webpack.config.ts --env production',
77
+ dev: 'webpack -w -c ./.config/webpack/webpack.config.ts --env development',
78
+ },
79
+ devDependencies: {
80
+ 'copy-webpack-plugin': '^12.0.0',
81
+ 'fork-ts-checker-webpack-plugin': '^9.0.0',
82
+ 'swc-loader': '^0.2.0',
83
+ webpack: '^5.94.0',
84
+ 'webpack-cli': '^5.1.4',
85
+ 'webpack-livereload-plugin': '^3.0.2',
86
+ 'webpack-subresource-integrity': '^5.1.0',
87
+ 'webpack-virtual-modules': '^0.6.2',
88
+ },
89
+ },
90
+ null,
91
+ 2
92
+ )
93
+ );
94
+ context.addFile('.config/.cprc.json', JSON.stringify({ version: '5.0.0', features: {} }, null, 2));
95
+ return context;
96
+ }
97
+
98
+ describe('rspack', () => {
99
+ // guards the explicit package lists against the template: fails when the rspack template gains,
100
+ // loses or renames a package the addition should manage. version bumps do not fail it
101
+ describe('package lists', () => {
102
+ it('should find a template version for every package the addition installs', async () => {
103
+ const rspackDeps = await getTemplateDevDependencies(RSPACK_TEMPLATE_DATA);
104
+
105
+ for (const name of [...RSPACK_DEV_DEPENDENCIES, ...BASE_CONFIG_DEV_DEPENDENCIES]) {
106
+ expect(rspackDeps[name], name).toBeDefined();
107
+ }
108
+ });
109
+
110
+ it('should list every package only the rspack template has', async () => {
111
+ const rspackDeps = await getTemplateDevDependencies(RSPACK_TEMPLATE_DATA);
112
+ const webpackDeps = await getTemplateDevDependencies(WEBPACK_TEMPLATE_DATA);
113
+ const rspackOnly = Object.keys(rspackDeps).filter((name) => !(name in webpackDeps));
114
+
115
+ expect(RSPACK_DEV_DEPENDENCIES).toEqual(expect.arrayContaining(rspackOnly));
116
+ });
117
+
118
+ it('should list every package only the webpack template has', async () => {
119
+ const rspackDeps = await getTemplateDevDependencies(RSPACK_TEMPLATE_DATA);
120
+ const webpackDeps = await getTemplateDevDependencies(WEBPACK_TEMPLATE_DATA);
121
+ const webpackOnly = Object.keys(webpackDeps).filter((name) => !(name in rspackDeps));
122
+
123
+ expect(WEBPACK_ONLY_DEV_DEPENDENCIES).toEqual(expect.arrayContaining(webpackOnly));
124
+ });
125
+
126
+ it('should read the same versions as a render with the plugin template data', async () => {
127
+ expect(getTemplateDevDependencyVersions()).toEqual(await getTemplateDevDependencies(RSPACK_TEMPLATE_DATA));
128
+ });
129
+ });
130
+
131
+ describe('start states', () => {
132
+ it('should make no changes and explain why when there is no create-plugin bundler setup', () => {
133
+ const context = new Context('/virtual');
134
+
135
+ const result = addRspack(context);
136
+
137
+ expect(result.hasChanges()).toBe(false);
138
+ expect(result.getMessage()).toEqual(
139
+ expect.objectContaining({ level: 'warning', title: expect.stringContaining('bundler configuration') })
140
+ );
141
+ });
142
+
143
+ it('should re-render a stale .config/rspack left behind on a webpack plugin', () => {
144
+ const context = createBaseContext();
145
+ context.addFile('.config/rspack/rspack.config.ts', 'stale rspack config');
146
+
147
+ const result = addRspack(context);
148
+
149
+ expect(result.getFile('.config/rspack/rspack.config.ts')).toBe('// rendered template stub');
150
+ expect(result.doesFileExist('.config/webpack/webpack.config.ts')).toBe(false);
151
+ });
152
+
153
+ it('should replace an experimental rspack setup with the current rspack templates', () => {
154
+ const context = new Context('/virtual');
155
+ context.addFile('.config/rspack/rspack.config.ts', 'experimental rspack config');
156
+ context.addFile('.config/rspack/BuildModeRspackPlugin.ts', 'experimental build mode plugin');
157
+ context.addFile('.config/rspack/liveReloadPlugin.ts', 'experimental live reload plugin');
158
+ context.addFile(
159
+ '.config/.cprc.json',
160
+ JSON.stringify({ version: '7.9.0', features: { useExperimentalRspack: true } }, null, 2)
161
+ );
162
+
163
+ const result = addRspack(context);
164
+
165
+ expect(result.getFile('.config/rspack/rspack.config.ts')).toBe('// rendered template stub');
166
+ expect(result.getFile('.config/rspack/BuildModeRspackPlugin.ts')).toBe('// rendered template stub');
167
+ expect(result.getFile('.config/rspack/LiveReloadRspackPlugin.ts')).toBe('// rendered template stub');
168
+ // renamed in the template, so the experimental setup's copy is removed
169
+ expect(result.doesFileExist('.config/rspack/liveReloadPlugin.ts')).toBe(false);
170
+ });
171
+ });
172
+
173
+ describe('.cprc.json', () => {
174
+ it('should set the useRspack feature flag in .config/.cprc.json', () => {
175
+ const context = createBaseContext();
176
+
177
+ const result = addRspack(context);
178
+ const cprc = JSON.parse(result.getFile('.config/.cprc.json')!);
179
+
180
+ expect(cprc.features.useRspack).toBe(true);
181
+ });
182
+
183
+ it('should remove the experimental rspack flag from both config files', () => {
184
+ const context = createBaseContext();
185
+ context.updateFile(
186
+ '.config/.cprc.json',
187
+ JSON.stringify({ version: '7.9.0', features: { useExperimentalRspack: true } }, null, 2)
188
+ );
189
+ // every generated plugin's own .cprc.json carried the flag as false
190
+ context.addFile(
191
+ '.cprc.json',
192
+ JSON.stringify({ features: { bundleGrafanaUI: false, useExperimentalRspack: false } }, null, 2)
193
+ );
194
+
195
+ const result = addRspack(context);
196
+ const rootCprc = JSON.parse(result.getFile('.config/.cprc.json')!);
197
+ const userCprc = JSON.parse(result.getFile('.cprc.json')!);
198
+
199
+ expect(rootCprc.features).toEqual({ useRspack: true });
200
+ expect(userCprc.features).toEqual({ bundleGrafanaUI: false });
201
+ });
202
+
203
+ it('should preserve existing .cprc.json properties', () => {
204
+ const context = createBaseContext();
205
+
206
+ const result = addRspack(context);
207
+ const cprc = JSON.parse(result.getFile('.config/.cprc.json')!);
208
+
209
+ expect(cprc.version).toBe('5.0.0');
210
+ });
211
+ });
212
+
213
+ describe('rspack config files', () => {
214
+ it('should add rspack config files', () => {
215
+ const context = createBaseContext();
216
+
217
+ const result = addRspack(context);
218
+
219
+ expect(result.doesFileExist('.config/rspack/rspack.config.ts')).toBe(true);
220
+ expect(result.doesFileExist('.config/rspack/BuildModeRspackPlugin.ts')).toBe(true);
221
+ expect(result.doesFileExist('.config/rspack/LiveReloadRspackPlugin.ts')).toBe(true);
222
+ expect(result.doesFileExist('.config/rspack/liveReloadPlugin.ts')).toBe(false);
223
+ expect(result.doesFileExist('.config/rspack/LicenseRspackPlugin.ts')).toBe(true);
224
+ expect(result.doesFileExist('.config/rspack/DiagnosticsRspackPlugin.ts')).toBe(true);
225
+ });
226
+ });
227
+
228
+ describe('docs for agents and developers', () => {
229
+ it('should re-render the agent instructions and .config README for rspack', () => {
230
+ const context = createBaseContext();
231
+ context.addFile('.config/AGENTS/instructions.md', '**You must use webpack**');
232
+ context.addFile('.config/README.md', '### Extending the Webpack config');
233
+
234
+ const result = addRspack(context);
235
+
236
+ expect(result.getFile('.config/AGENTS/instructions.md')).toBe('// rendered template stub');
237
+ expect(result.getFile('.config/README.md')).toBe('// rendered template stub');
238
+ });
239
+ });
240
+
241
+ describe('bundler files', () => {
242
+ it('should update externals.ts with rspack imports', () => {
243
+ const context = createBaseContext();
244
+
245
+ const result = addRspack(context);
246
+ const externals = result.getFile('.config/bundler/externals.ts')!;
247
+
248
+ expect(externals).toContain('RspackOptions');
249
+ });
250
+
251
+ it('should re-render all bundler files', () => {
252
+ const context = createBaseContext();
253
+
254
+ const result = addRspack(context);
255
+
256
+ expect(result.getFile('.config/bundler/externals.ts')).toContain('RspackOptions');
257
+ expect(result.getFile('.config/bundler/constants.ts')).toBe('// rendered template stub');
258
+ expect(result.getFile('.config/bundler/copyFiles.ts')).toBe('// rendered template stub');
259
+ expect(result.getFile('.config/bundler/utils.ts')).toBe('// rendered template stub');
260
+ });
261
+
262
+ it('should add bundler files that do not already exist', () => {
263
+ const context = new Context('/virtual');
264
+ context.addFile('.config/webpack/webpack.config.ts', 'webpack config');
265
+ context.addFile(
266
+ 'package.json',
267
+ JSON.stringify({ scripts: { build: 'webpack', dev: 'webpack -w' }, devDependencies: {} }, null, 2)
268
+ );
269
+
270
+ const result = addRspack(context);
271
+
272
+ expect(result.doesFileExist('.config/bundler/externals.ts')).toBe(true);
273
+ expect(result.doesFileExist('.config/bundler/constants.ts')).toBe(true);
274
+ expect(result.doesFileExist('.config/bundler/copyFiles.ts')).toBe(true);
275
+ expect(result.doesFileExist('.config/bundler/utils.ts')).toBe(true);
276
+ });
277
+ });
278
+
279
+ describe('package.json', () => {
280
+ it('should add the rspack-only devDependencies from the template at the template versions', async () => {
281
+ const rspackDeps = await getTemplateDevDependencies(RSPACK_TEMPLATE_DATA);
282
+ const webpackDeps = await getTemplateDevDependencies(WEBPACK_TEMPLATE_DATA);
283
+ const rspackOnlyDeps = Object.keys(rspackDeps).filter((name) => !(name in webpackDeps));
284
+ const context = createBaseContext();
285
+
286
+ const result = addRspack(context);
287
+ const pkg = JSON.parse(result.getFile('package.json')!);
288
+
289
+ expect(rspackOnlyDeps).toEqual(expect.arrayContaining(['@rspack/core', '@rspack/cli']));
290
+ for (const name of rspackOnlyDeps) {
291
+ expect(pkg.devDependencies[name], name).toBe(rspackDeps[name]);
292
+ }
293
+ });
294
+
295
+ it('should lint with diagnostics-webpack-plugin and raise webpack to the version it needs', async () => {
296
+ const rspackDeps = await getTemplateDevDependencies(RSPACK_TEMPLATE_DATA);
297
+ const context = createBaseContext();
298
+
299
+ const result = addRspack(context);
300
+ const pkg = JSON.parse(result.getFile('package.json')!);
301
+
302
+ expect(pkg.devDependencies['diagnostics-webpack-plugin']).toBe(rspackDeps['diagnostics-webpack-plugin']);
303
+ expect(pkg.devDependencies['eslint-rspack-plugin']).toBeUndefined();
304
+ // the base fixture pins webpack ^5.94.0, below diagnostics-webpack-plugin's peer floor
305
+ expect(pkg.devDependencies.webpack).toBe(rspackDeps.webpack);
306
+ });
307
+
308
+ it('should add packages the rspack base config needs when the plugin has dropped them', async () => {
309
+ const rspackDeps = await getTemplateDevDependencies(RSPACK_TEMPLATE_DATA);
310
+ const context = createBaseContext();
311
+
312
+ const result = addRspack(context);
313
+ const pkg = JSON.parse(result.getFile('package.json')!);
314
+
315
+ // builtin:swc-loader runs with externalHelpers, so builds import @swc/helpers
316
+ expect(pkg.devDependencies['@swc/helpers']).toBe(rspackDeps['@swc/helpers']);
317
+ expect(pkg.devDependencies['imports-loader']).toBe(rspackDeps['imports-loader']);
318
+ });
319
+
320
+ it('should not change the versions of base config packages the plugin already has', () => {
321
+ const context = createBaseContext();
322
+ const pkg = JSON.parse(context.getFile('package.json')!);
323
+ pkg.devDependencies['sass-loader'] = '^13.0.0';
324
+ pkg.dependencies = { '@swc/helpers': '0.4.14' };
325
+ context.updateFile('package.json', JSON.stringify(pkg, null, 2));
326
+
327
+ const result = addRspack(context);
328
+ const updated = JSON.parse(result.getFile('package.json')!);
329
+
330
+ expect(updated.devDependencies['sass-loader']).toBe('^13.0.0');
331
+ expect(updated.dependencies['@swc/helpers']).toBe('0.4.14');
332
+ expect(updated.devDependencies['@swc/helpers']).toBeUndefined();
333
+ });
334
+
335
+ it('should upgrade rspack devDependencies that are below the template version', async () => {
336
+ const rspackDeps = await getTemplateDevDependencies(RSPACK_TEMPLATE_DATA);
337
+ const context = createBaseContext();
338
+ const pkg = JSON.parse(context.getFile('package.json')!);
339
+ pkg.devDependencies['@rspack/core'] = '^1.6.0';
340
+ pkg.devDependencies['@rspack/cli'] = '^1.6.0';
341
+ pkg.devDependencies['ts-checker-rspack-plugin'] = '^1.2.0';
342
+ context.updateFile('package.json', JSON.stringify(pkg, null, 2));
343
+
344
+ const result = addRspack(context);
345
+ const updated = JSON.parse(result.getFile('package.json')!);
346
+
347
+ expect(updated.devDependencies['@rspack/core']).toBe(rspackDeps['@rspack/core']);
348
+ expect(updated.devDependencies['@rspack/cli']).toBe(rspackDeps['@rspack/cli']);
349
+ expect(updated.devDependencies['ts-checker-rspack-plugin']).toBe(rspackDeps['ts-checker-rspack-plugin']);
350
+ });
351
+
352
+ it('should remove webpack-only devDependencies', () => {
353
+ const context = createBaseContext();
354
+ const pkg = JSON.parse(context.getFile('package.json')!);
355
+ pkg.devDependencies['eslint-webpack-plugin'] = '^5.0.0';
356
+ pkg.devDependencies['terser-webpack-plugin'] = '^5.3.0';
357
+ context.updateFile('package.json', JSON.stringify(pkg, null, 2));
358
+
359
+ const result = addRspack(context);
360
+ const updated = JSON.parse(result.getFile('package.json')!);
361
+
362
+ expect(updated.devDependencies['copy-webpack-plugin']).toBeUndefined();
363
+ expect(updated.devDependencies['eslint-webpack-plugin']).toBeUndefined();
364
+ expect(updated.devDependencies['fork-ts-checker-webpack-plugin']).toBeUndefined();
365
+ expect(updated.devDependencies['swc-loader']).toBeUndefined();
366
+ expect(updated.devDependencies['terser-webpack-plugin']).toBeUndefined();
367
+ expect(updated.devDependencies['webpack-cli']).toBeUndefined();
368
+ expect(updated.devDependencies['webpack-livereload-plugin']).toBeUndefined();
369
+ expect(updated.devDependencies['webpack-subresource-integrity']).toBeUndefined();
370
+ expect(updated.devDependencies['webpack-virtual-modules']).toBeUndefined();
371
+ });
372
+
373
+ it('should keep webpack-only devDependencies that root build files still use and report them', () => {
374
+ const context = createBaseContext();
375
+ context.addFile(
376
+ 'webpack.config.ts',
377
+ "import CopyWebpackPlugin from 'copy-webpack-plugin';\nimport { getAlias } from './webpack.config.utils';"
378
+ );
379
+ context.addFile('webpack.config.utils.ts', "export const rule = { use: { loader: 'swc-loader' } };");
380
+
381
+ const result = addRspack(context);
382
+ const pkg = JSON.parse(result.getFile('package.json')!);
383
+
384
+ expect(pkg.devDependencies['copy-webpack-plugin']).toBe('^12.0.0');
385
+ expect(pkg.devDependencies['swc-loader']).toBe('^0.2.0');
386
+ expect(pkg.devDependencies['webpack-livereload-plugin']).toBeUndefined();
387
+ const report = result.getMessage()?.body?.join('\n');
388
+ expect(report).toContain('copy-webpack-plugin');
389
+ expect(report).toContain('swc-loader');
390
+ });
391
+
392
+ it('should not treat source files as build files when deciding which dependencies to keep', () => {
393
+ const context = createBaseContext();
394
+ context.addFile('src/module.ts', "import 'copy-webpack-plugin';");
395
+
396
+ const result = addRspack(context);
397
+ const pkg = JSON.parse(result.getFile('package.json')!);
398
+
399
+ expect(pkg.devDependencies['copy-webpack-plugin']).toBeUndefined();
400
+ });
401
+
402
+ it('should keep webpack at a version already above the template floor', () => {
403
+ const context = createBaseContext();
404
+ const basePkg = JSON.parse(context.getFile('package.json')!);
405
+ basePkg.devDependencies.webpack = '^5.200.0';
406
+ context.updateFile('package.json', JSON.stringify(basePkg, null, 2));
407
+
408
+ const result = addRspack(context);
409
+ const pkg = JSON.parse(result.getFile('package.json')!);
410
+
411
+ expect(pkg.devDependencies.webpack).toBe('^5.200.0');
412
+ });
413
+
414
+ it('should update build and dev scripts to use rspack', () => {
415
+ const context = createBaseContext();
416
+
417
+ const result = addRspack(context);
418
+ const pkg = JSON.parse(result.getFile('package.json')!);
419
+
420
+ expect(pkg.scripts.build).toBe('rspack -c ./.config/rspack/rspack.config.ts --env production');
421
+ expect(pkg.scripts.dev).toBe('rspack -w -c ./.config/rspack/rspack.config.ts --env development');
422
+ });
423
+ });
424
+
425
+ describe('scripts', () => {
426
+ function runWithScripts(scripts: Record<string, string>, rootConfig = true) {
427
+ const context = createBaseContext();
428
+ const pkg = JSON.parse(context.getFile('package.json')!);
429
+ pkg.scripts = scripts;
430
+ context.updateFile('package.json', JSON.stringify(pkg, null, 2));
431
+ if (rootConfig) {
432
+ context.addFile('webpack.config.ts', 'custom webpack config');
433
+ }
434
+ const result = addRspack(context);
435
+ return {
436
+ scripts: JSON.parse(result.getFile('package.json')!).scripts,
437
+ report: result.getMessage()?.body?.join('\n') ?? '',
438
+ };
439
+ }
440
+
441
+ it('should point scripts that use the root webpack config at the root rspack config', () => {
442
+ const { scripts } = runWithScripts({
443
+ build: 'webpack -c webpack.config.ts --env production',
444
+ dev: 'webpack -w --config=./webpack.config.ts --env development',
445
+ });
446
+
447
+ expect(scripts.build).toBe('rspack -c ./rspack.config.ts --env production');
448
+ expect(scripts.dev).toBe('rspack -w --config=./rspack.config.ts --env development');
449
+ });
450
+
451
+ it('should keep chained commands, env assignments and extra flags', () => {
452
+ const { scripts } = runWithScripts({
453
+ build:
454
+ 'pnpm run build:deps && NODE_ENV=production npx webpack -c ./webpack.config.ts --env production --stats-error-details',
455
+ });
456
+
457
+ expect(scripts.build).toBe(
458
+ 'pnpm run build:deps && NODE_ENV=production npx rspack -c ./rspack.config.ts --env production --stats-error-details'
459
+ );
460
+ });
461
+
462
+ it('should rewrite sub scripts and leave script runners alone', () => {
463
+ const { scripts } = runWithScripts({
464
+ build: 'run-s "build:*"',
465
+ 'build:compile': 'webpack -c ./webpack.config.ts --env production',
466
+ });
467
+
468
+ expect(scripts.build).toBe('run-s "build:*"');
469
+ expect(scripts['build:compile']).toBe('rspack -c ./rspack.config.ts --env production');
470
+ });
471
+
472
+ it('should rewrite webpack calls that rely on the default config lookup', () => {
473
+ const { scripts } = runWithScripts({ build: 'webpack --env production' });
474
+
475
+ expect(scripts.build).toBe('rspack --env production');
476
+ });
477
+
478
+ it('should leave scripts it cannot rewrite unchanged and report them', () => {
479
+ const unparseable = 'tsx node_modules/webpack-cli/bin/cli.js -c ./webpack.config.ts --env production';
480
+ const { scripts, report } = runWithScripts({ build: unparseable });
481
+
482
+ expect(scripts.build).toBe(unparseable);
483
+ expect(report).toContain('build');
484
+ expect(report).toContain(unparseable);
485
+ });
486
+
487
+ it('should not touch or report scripts that only mention webpack tooling', () => {
488
+ const { scripts, report } = runWithScripts({ analyze: 'webpack-bundle-analyzer dist/stats.json' });
489
+
490
+ expect(scripts.analyze).toBe('webpack-bundle-analyzer dist/stats.json');
491
+ expect(report).not.toContain('webpack-bundle-analyzer');
492
+ });
493
+
494
+ it('should leave scripts that already call rspack unchanged', () => {
495
+ const { scripts } = runWithScripts(
496
+ {
497
+ build: 'rspack -c ./rspack.config.ts --env production',
498
+ dev: 'rspack -w -c ./rspack.config.ts --env development',
499
+ },
500
+ false
501
+ );
502
+
503
+ expect(scripts.build).toBe('rspack -c ./rspack.config.ts --env production');
504
+ expect(scripts.dev).toBe('rspack -w -c ./rspack.config.ts --env development');
505
+ });
506
+ });
507
+
508
+ describe('experimental rspack start', () => {
509
+ function createExperimentalRspackContext(devDependencies: Record<string, string>) {
510
+ const context = new Context('/virtual');
511
+ context.addFile('.config/rspack/rspack.config.ts', 'experimental rspack config');
512
+ context.addFile(
513
+ '.config/.cprc.json',
514
+ JSON.stringify({ version: '7.9.0', features: { useExperimentalRspack: true } }, null, 2)
515
+ );
516
+ context.addFile('rspack.config.ts', 'my rspack config');
517
+ context.addFile(
518
+ 'package.json',
519
+ JSON.stringify(
520
+ {
521
+ scripts: { build: 'rspack -c ./rspack.config.ts --env production' },
522
+ devDependencies: { 'eslint-webpack-plugin': '^5.0.0', webpack: '^5.94.0', ...devDependencies },
523
+ },
524
+ null,
525
+ 2
526
+ )
527
+ );
528
+ return context;
529
+ }
530
+
531
+ it('should hand the root rspack config to the user untouched and not add a stub', () => {
532
+ const context = createExperimentalRspackContext({ '@rspack/core': '^1.6.0', '@rspack/cli': '^1.6.0' });
533
+
534
+ const result = addRspack(context);
535
+
536
+ expect(result.getFile('rspack.config.ts')).toBe('my rspack config');
537
+ });
538
+
539
+ it('should swap eslint-webpack-plugin for diagnostics-webpack-plugin', () => {
540
+ const context = createExperimentalRspackContext({ '@rspack/core': '^1.6.0', '@rspack/cli': '^1.6.0' });
541
+
542
+ const result = addRspack(context);
543
+ const pkg = JSON.parse(result.getFile('package.json')!);
544
+
545
+ expect(pkg.devDependencies['eslint-webpack-plugin']).toBeUndefined();
546
+ expect(pkg.devDependencies['diagnostics-webpack-plugin']).toBeDefined();
547
+ });
548
+
549
+ it('should keep rspack versions the plugin already pinned at or above the template', () => {
550
+ const context = createExperimentalRspackContext({ '@rspack/core': '2.2.6', '@rspack/cli': '2.2.6' });
551
+
552
+ const result = addRspack(context);
553
+ const pkg = JSON.parse(result.getFile('package.json')!);
554
+
555
+ expect(pkg.devDependencies['@rspack/core']).toBe('2.2.6');
556
+ expect(pkg.devDependencies['@rspack/cli']).toBe('2.2.6');
557
+ });
558
+ });
559
+
560
+ describe('engines', () => {
561
+ function runWithEngines(engines?: Record<string, string>) {
562
+ const context = createBaseContext();
563
+ const pkg = JSON.parse(context.getFile('package.json')!);
564
+ if (engines) {
565
+ pkg.engines = engines;
566
+ }
567
+ context.updateFile('package.json', JSON.stringify(pkg, null, 2));
568
+ return JSON.parse(addRspack(context).getFile('package.json')!).engines;
569
+ }
570
+
571
+ it('should raise engines.node when it allows versions rspack 2 cannot run on', () => {
572
+ expect(runWithEngines({ node: '>=22' })).toEqual({ node: '>=22.23' });
573
+ expect(runWithEngines({ node: '>=20', npm: '>=10' })).toEqual({ node: '>=22.23', npm: '>=10' });
574
+ });
575
+
576
+ it('should keep engines.node when it already requires a newer Node', () => {
577
+ expect(runWithEngines({ node: '>=24' })).toEqual({ node: '>=24' });
578
+ expect(runWithEngines({ node: '>=22.23.3' })).toEqual({ node: '>=22.23.3' });
579
+ });
580
+
581
+ it('should not add engines when the plugin has none', () => {
582
+ expect(runWithEngines()).toBeUndefined();
583
+ });
584
+ });
585
+
586
+ describe('npm lockfile', () => {
587
+ function createLockfile(rspackCoreVersion: string) {
588
+ return JSON.stringify(
589
+ {
590
+ name: 'my-plugin',
591
+ lockfileVersion: 3,
592
+ packages: {
593
+ '': { name: 'my-plugin' },
594
+ 'node_modules/@rspack/core': { version: rspackCoreVersion },
595
+ 'node_modules/@rspack/cli': { version: rspackCoreVersion },
596
+ 'node_modules/@rspack/cli/node_modules/@rspack/dev-server': { version: '1.1.5' },
597
+ 'node_modules/react': { version: '18.3.1' },
598
+ },
599
+ },
600
+ null,
601
+ 2
602
+ );
603
+ }
604
+
605
+ it('should drop locked rspack 1 packages so npm can resolve rspack 2', () => {
606
+ const context = createBaseContext();
607
+ context.addFile('package-lock.json', createLockfile('1.7.12'));
608
+
609
+ const result = addRspack(context);
610
+ const packages = Object.keys(JSON.parse(result.getFile('package-lock.json')!).packages);
611
+
612
+ expect(packages).toEqual(['', 'node_modules/react']);
613
+ });
614
+
615
+ it('should leave the lockfile alone when rspack 2 is already locked', () => {
616
+ const context = createBaseContext();
617
+ const lockfile = createLockfile('2.2.8');
618
+ context.addFile('package-lock.json', lockfile);
619
+
620
+ const result = addRspack(context);
621
+
622
+ expect(result.getFile('package-lock.json')).toBe(lockfile);
623
+ });
624
+
625
+ it('should not touch other package managers lockfiles', () => {
626
+ const context = createBaseContext();
627
+ context.addFile('pnpm-lock.yaml', "lockfileVersion: '9.0'\n");
628
+ context.addFile('yarn.lock', '# yarn lockfile v1\n');
629
+
630
+ const result = addRspack(context);
631
+
632
+ expect(result.getFile('pnpm-lock.yaml')).toBe("lockfileVersion: '9.0'\n");
633
+ expect(result.getFile('yarn.lock')).toBe('# yarn lockfile v1\n');
634
+ });
635
+ });
636
+
637
+ describe('idempotency', () => {
638
+ const run = async (context: Context) => addRspack(context);
639
+
640
+ it('should be idempotent for a webpack plugin', async () => {
641
+ await expect(run).toBeIdempotent(createBaseContext());
642
+ });
643
+
644
+ it('should be idempotent for a webpack plugin with a root config, helpers and a lockfile', async () => {
645
+ const context = createBaseContext();
646
+ const pkg = JSON.parse(context.getFile('package.json')!);
647
+ pkg.scripts = { build: 'pnpm run prebuild && webpack -c ./webpack.config.ts --env production' };
648
+ pkg.engines = { node: '>=22' };
649
+ context.updateFile('package.json', JSON.stringify(pkg, null, 2));
650
+ context.addFile('webpack.config.ts', "import CopyWebpackPlugin from 'copy-webpack-plugin';");
651
+ context.addFile('.config/webpack/CorsWorkerPlugin.ts', 'export class CorsWorkerPlugin {}');
652
+ context.addFile(
653
+ 'package-lock.json',
654
+ JSON.stringify({ packages: { 'node_modules/@rspack/core': { version: '1.7.12' } } }, null, 2)
655
+ );
656
+
657
+ await expect(run).toBeIdempotent(context);
658
+ });
659
+
660
+ it('should be idempotent for an experimental rspack plugin', async () => {
661
+ const context = new Context('/virtual');
662
+ context.addFile('.config/rspack/rspack.config.ts', 'experimental rspack config');
663
+ context.addFile('rspack.config.ts', 'my rspack config');
664
+ context.addFile(
665
+ 'package.json',
666
+ JSON.stringify({ scripts: { build: 'rspack -c ./rspack.config.ts' }, devDependencies: {} }, null, 2)
667
+ );
668
+
669
+ await expect(run).toBeIdempotent(context);
670
+ });
671
+ });
672
+
673
+ describe('webpack cleanup', () => {
674
+ it('should delete the create-plugin webpack files from .config/webpack/', () => {
675
+ const context = createBaseContext();
676
+ context.addFile('.config/webpack/utils.ts', '');
677
+ context.addFile('.config/webpack/constants.ts', '');
678
+ context.addFile('.config/webpack/tsconfig.webpack.json', '');
679
+
680
+ const result = addRspack(context);
681
+
682
+ expect(result.doesFileExist('.config/webpack/webpack.config.ts')).toBe(false);
683
+ expect(result.doesFileExist('.config/webpack/BuildModeWebpackPlugin.ts')).toBe(false);
684
+ expect(result.doesFileExist('.config/webpack/utils.ts')).toBe(false);
685
+ expect(result.doesFileExist('.config/webpack/constants.ts')).toBe(false);
686
+ expect(result.doesFileExist('.config/webpack/tsconfig.webpack.json')).toBe(false);
687
+ });
688
+
689
+ it('should keep files the plugin added to .config/webpack/ and report them', () => {
690
+ const context = createBaseContext();
691
+ context.addFile('.config/webpack/CorsWorkerPlugin.ts', 'export class CorsWorkerPlugin {}');
692
+
693
+ const result = addRspack(context);
694
+
695
+ expect(result.getFile('.config/webpack/CorsWorkerPlugin.ts')).toBe('export class CorsWorkerPlugin {}');
696
+ expect(result.getMessage()?.body?.join('\n')).toContain('.config/webpack/CorsWorkerPlugin.ts');
697
+ });
698
+ });
699
+
700
+ describe('root config stub', () => {
701
+ it.each(['webpack.config.ts', 'webpack.config.js', 'webpack.config.cjs', 'webpack.config.mjs'])(
702
+ 'should add a root rspack.config.ts stub when %s exists',
703
+ (rootConfig) => {
704
+ const context = createBaseContext();
705
+ context.addFile(rootConfig, 'custom webpack config');
706
+
707
+ const result = addRspack(context);
708
+
709
+ expect(result.doesFileExist('rspack.config.ts')).toBe(true);
710
+ }
711
+ );
712
+
713
+ it('should make the stub fail the build until the config is ported', () => {
714
+ const context = createBaseContext();
715
+ context.addFile('webpack.config.ts', 'custom webpack config');
716
+
717
+ const result = addRspack(context);
718
+ const stub = result.getFile('rspack.config.ts')!;
719
+
720
+ expect(stub).toContain('throw new Error(');
721
+ expect(stub).toContain('[rspack]');
722
+ expect(stub).toContain('add rspack --agent');
723
+ expect(stub).toContain('TODO(rspack)');
724
+ });
725
+
726
+ it('should write the stub as an ESM safe rspack-merge config', () => {
727
+ const context = createBaseContext();
728
+ context.addFile('webpack.config.ts', 'custom webpack config');
729
+
730
+ const result = addRspack(context);
731
+ const stub = result.getFile('rspack.config.ts')!;
732
+
733
+ expect(stub).toContain("import { merge } from 'rspack-merge';");
734
+ expect(stub).toContain("import grafanaConfig, { type Env } from './.config/rspack/rspack.config.ts';");
735
+ expect(stub).not.toContain('__dirname');
736
+ expect(stub).not.toContain('webpack-merge');
737
+ });
738
+
739
+ it('should leave the root webpack config untouched', () => {
740
+ const context = createBaseContext();
741
+ const originalContent = 'import grafanaConfig from "./.config/webpack/webpack.config";';
742
+ context.addFile('webpack.config.ts', originalContent);
743
+
744
+ const result = addRspack(context);
745
+
746
+ expect(result.getFile('webpack.config.ts')).toBe(originalContent);
747
+ });
748
+
749
+ it('should not overwrite an existing root rspack config and should report it', () => {
750
+ const context = createBaseContext();
751
+ context.addFile('webpack.config.ts', 'custom webpack config');
752
+ context.addFile('rspack.config.ts', 'my rspack config');
753
+
754
+ const result = addRspack(context);
755
+
756
+ expect(result.getFile('rspack.config.ts')).toBe('my rspack config');
757
+ expect(result.getMessage()?.body?.join('\n')).toContain('rspack.config.ts');
758
+ });
759
+
760
+ it('should not add a stub when there is no root webpack config', () => {
761
+ const context = createBaseContext();
762
+
763
+ const result = addRspack(context);
764
+
765
+ expect(result.doesFileExist('rspack.config.ts')).toBe(false);
766
+ });
767
+ });
768
+ });