@grafana/create-plugin 7.17.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/codemods/additions/additions.js +7 -0
  3. package/dist/codemods/additions/prompts/rspack.md +120 -0
  4. package/dist/codemods/additions/scripts/docs.js +2 -2
  5. package/dist/codemods/additions/scripts/rspack.js +341 -0
  6. package/dist/codemods/agentic/prompts.js +1 -1
  7. package/dist/codemods/migrations/migrations.js +6 -0
  8. package/dist/codemods/migrations/scripts/020-rename-rspack-feature-flag.js +8 -0
  9. package/dist/codemods/utils.rspack.js +38 -0
  10. package/dist/commands/generate.command.js +1 -9
  11. package/dist/constants.js +0 -1
  12. package/dist/utils/utils.templates.js +4 -4
  13. package/package.json +1 -1
  14. package/src/codemods/additions/additions.test.ts +9 -0
  15. package/src/codemods/additions/additions.ts +7 -0
  16. package/src/codemods/additions/prompts/rspack.md +120 -0
  17. package/src/codemods/additions/scripts/docs.test.ts +4 -4
  18. package/src/codemods/additions/scripts/docs.ts +2 -2
  19. package/src/codemods/additions/scripts/rspack.test.ts +852 -0
  20. package/src/codemods/additions/scripts/rspack.ts +453 -0
  21. package/src/codemods/agentic/prompts.test.ts +11 -2
  22. package/src/codemods/agentic/prompts.ts +1 -1
  23. package/src/codemods/migrations/migrations.ts +7 -0
  24. package/src/codemods/migrations/scripts/020-rename-rspack-feature-flag.test.ts +94 -0
  25. package/src/codemods/migrations/scripts/020-rename-rspack-feature-flag.ts +8 -0
  26. package/src/codemods/utils.rspack.ts +58 -0
  27. package/src/codemods/utils.test.ts +5 -2
  28. package/src/commands/generate.command.ts +2 -10
  29. package/src/constants.ts +0 -1
  30. package/src/types.ts +1 -1
  31. package/src/utils/tests/utils.config.test.ts +10 -0
  32. package/src/utils/utils.config.ts +2 -1
  33. package/src/utils/utils.templates.ts +4 -4
  34. package/templates/common/.config/AGENTS/instructions.md +2 -2
  35. package/templates/common/.config/README.md +56 -0
  36. package/templates/common/.config/bundler/externals.ts +3 -3
  37. package/templates/common/.config/rspack/DiagnosticsRspackPlugin.ts +29 -0
  38. package/templates/common/.config/rspack/LicenseRspackPlugin.ts +43 -0
  39. package/templates/common/.config/rspack/LiveReloadRspackPlugin.ts +95 -0
  40. package/templates/common/.config/rspack/PluginCssRspackPlugin.ts +103 -0
  41. package/templates/common/.config/rspack/rspack.config.ts +79 -48
  42. package/templates/common/.cprc.json +1 -2
  43. package/templates/common/_package.json +22 -22
  44. package/templates/common/.config/rspack/liveReloadPlugin.ts +0 -133
@@ -0,0 +1,453 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { lt, major, minVersion, validRange } from 'semver';
4
+ import type { Context } from '../../context.js';
5
+ import { replaceExperimentalRspackFlag } from '../../utils.rspack.js';
6
+ import { renderHandlebarsTemplate } from '../../../utils/utils.handlebars.js';
7
+ import { getTemplateData } from '../../../utils/utils.templates.js';
8
+ import {
9
+ additionsDebug,
10
+ addDependenciesToPackageJson,
11
+ readJsonFile,
12
+ removeDependenciesFromPackageJson,
13
+ renderTemplate,
14
+ } from '../../utils.js';
15
+
16
+ const RSPACK_TEMPLATE_DATA_OVERRIDES = {
17
+ useRspack: true,
18
+ frontendBundler: 'rspack',
19
+ };
20
+
21
+ const RSPACK_CONFIG_FILES = [
22
+ '.config/rspack/rspack.config.ts',
23
+ '.config/rspack/BuildModeRspackPlugin.ts',
24
+ '.config/rspack/LiveReloadRspackPlugin.ts',
25
+ '.config/rspack/LicenseRspackPlugin.ts',
26
+ '.config/rspack/DiagnosticsRspackPlugin.ts',
27
+ '.config/rspack/PluginCssRspackPlugin.ts',
28
+ ];
29
+
30
+ // files the rspack template used to ship under another name. removed so they do not linger
31
+ const RETIRED_RSPACK_FILES = ['.config/rspack/liveReloadPlugin.ts'];
32
+
33
+ // markdown in .config that describes the bundler, for agents and for developers
34
+ const DOC_FILES = ['.config/AGENTS/instructions.md', '.config/README.md'];
35
+
36
+ const BUNDLER_FILES = [
37
+ '.config/bundler/constants.ts',
38
+ '.config/bundler/copyFiles.ts',
39
+ '.config/bundler/externals.ts',
40
+ '.config/bundler/utils.ts',
41
+ ];
42
+
43
+ const WEBPACK_CONFIG_PATH = '.config/webpack/webpack.config.ts';
44
+ const RSPACK_CONFIG_PATH = '.config/rspack/rspack.config.ts';
45
+
46
+ // every file create-plugin has scaffolded into .config/webpack over time. anything else in there
47
+ // was added by the plugin author, so it is kept and reported instead of deleted
48
+ const WEBPACK_TEMPLATE_FILES = [
49
+ '.config/webpack/BuildModeWebpackPlugin.ts',
50
+ '.config/webpack/PluginSchemaWebpackPlugin.ts',
51
+ '.config/webpack/constants.ts',
52
+ '.config/webpack/generateCode.ts',
53
+ '.config/webpack/publicPath.ts',
54
+ '.config/webpack/tsconfig.webpack.json',
55
+ '.config/webpack/utils.ts',
56
+ '.config/webpack/watchPluginJson.ts',
57
+ '.config/webpack/webpack.config.ts',
58
+ '.config/webpack/webpack.parts.ts',
59
+ ];
60
+
61
+ export default function rspack(context: Context): Context {
62
+ const hasWebpackSetup = context.doesFileExist(WEBPACK_CONFIG_PATH);
63
+ const hasRspackSetup = context.doesFileExist(RSPACK_CONFIG_PATH);
64
+
65
+ if (!hasWebpackSetup && !hasRspackSetup) {
66
+ context.setMessage({
67
+ level: 'warning',
68
+ title: 'No create-plugin bundler configuration found, so nothing was changed.',
69
+ body: [`The rspack addition expects ${WEBPACK_CONFIG_PATH} or ${RSPACK_CONFIG_PATH} to exist.`],
70
+ });
71
+ return context;
72
+ }
73
+
74
+ const followUps: string[] = [];
75
+
76
+ deleteRetiredRspackFiles(context);
77
+ renderTemplateFiles(context, RSPACK_CONFIG_FILES);
78
+ renderTemplateFiles(context, BUNDLER_FILES);
79
+ renderTemplateFiles(context, DOC_FILES, false);
80
+ replaceExperimentalRspackFlag(context, { enable: true });
81
+
82
+ followUps.push(...addRootRspackConfigStub(context));
83
+ followUps.push(...updatePackageJson(context));
84
+ followUps.push(...raiseGrafanaDependency(context));
85
+ followUps.push(...deleteWebpackTemplateFiles(context));
86
+ reportFollowUps(context, followUps);
87
+
88
+ return context;
89
+ }
90
+
91
+ function deleteRetiredRspackFiles(context: Context): void {
92
+ for (const filePath of RETIRED_RSPACK_FILES) {
93
+ if (context.doesFileExist(filePath)) {
94
+ context.deleteFile(filePath);
95
+ }
96
+ }
97
+ }
98
+
99
+ function reportFollowUps(context: Context, followUps: string[]): void {
100
+ if (followUps.length === 0) {
101
+ return;
102
+ }
103
+
104
+ context.setMessage({
105
+ level: 'warning',
106
+ title: 'The rspack addition left some things for you to review.',
107
+ body: followUps,
108
+ });
109
+ }
110
+
111
+ const resolveTemplatePath = (relativePath: string) =>
112
+ fileURLToPath(new URL(`../../../../templates/common/${relativePath}`, import.meta.url));
113
+
114
+ // updateFile is a no-op for identical content, so re-rendering on every run keeps the addition idempotent
115
+ function renderTemplateFiles(context: Context, filePaths: string[], includeWarning = true): void {
116
+ const templateData = { ...getTemplateData(), ...RSPACK_TEMPLATE_DATA_OVERRIDES };
117
+ for (const filePath of filePaths) {
118
+ const rendered = renderTemplate(resolveTemplatePath(filePath), includeWarning, templateData);
119
+ if (context.doesFileExist(filePath)) {
120
+ context.updateFile(filePath, rendered);
121
+ } else {
122
+ context.addFile(filePath, rendered);
123
+ }
124
+ }
125
+ }
126
+
127
+ const ROOT_CONFIG_EXTENSIONS = ['ts', 'js', 'cjs', 'mjs', 'cts', 'mts'];
128
+
129
+ // a root webpack config means the plugin extends the build. the stub makes the build fail loudly
130
+ // until that config is ported, so a plugin cannot ship without the build features it relies on
131
+ function addRootRspackConfigStub(context: Context): string[] {
132
+ const rootWebpackConfig = ROOT_CONFIG_EXTENSIONS.map((extension) => `webpack.config.${extension}`).find((filePath) =>
133
+ context.doesFileExist(filePath)
134
+ );
135
+ if (!rootWebpackConfig) {
136
+ return [];
137
+ }
138
+
139
+ const existingRootRspackConfig = ROOT_CONFIG_EXTENSIONS.map((extension) => `rspack.config.${extension}`).find(
140
+ (filePath) => context.doesFileExist(filePath)
141
+ );
142
+ if (existingRootRspackConfig) {
143
+ return [
144
+ `${existingRootRspackConfig} already exists, so it was left as is. Check that it includes everything from ${rootWebpackConfig}.`,
145
+ ];
146
+ }
147
+
148
+ additionsDebug(`Custom ${rootWebpackConfig} detected. Adding a rspack.config.ts stub that fails until it is ported.`);
149
+ context.addFile('rspack.config.ts', getRootRspackConfigStub(rootWebpackConfig));
150
+
151
+ return [
152
+ `rspack.config.ts was added and fails the build until the customisations in ${rootWebpackConfig} are ported to it.`,
153
+ ];
154
+ }
155
+
156
+ interface PackageJson {
157
+ dependencies?: Record<string, string>;
158
+ devDependencies?: Record<string, string>;
159
+ scripts?: Record<string, string>;
160
+ engines?: Record<string, string>;
161
+ [key: string]: unknown;
162
+ }
163
+
164
+ function updatePackageJson(context: Context): string[] {
165
+ if (!context.doesFileExist('package.json')) {
166
+ additionsDebug('No package.json found. Skipping dependency and script updates.');
167
+ return [];
168
+ }
169
+
170
+ const followUps: string[] = [];
171
+ const templateVersions = getTemplateDevDependencyVersions();
172
+ const userBuildFileSources = readUserBuildFileSources(context);
173
+ const stillUsedDevDependencies = WEBPACK_ONLY_DEV_DEPENDENCIES.filter((name) =>
174
+ userBuildFileSources.some((source) => referencesPackage(source, name))
175
+ );
176
+ const unusedDevDependencies = WEBPACK_ONLY_DEV_DEPENDENCIES.filter(
177
+ (name) => !stillUsedDevDependencies.includes(name)
178
+ );
179
+
180
+ addDependenciesToPackageJson(
181
+ context,
182
+ {},
183
+ {
184
+ ...pickVersions(getMissingPackages(context, BASE_CONFIG_DEV_DEPENDENCIES), templateVersions),
185
+ ...pickVersions(RSPACK_DEV_DEPENDENCIES, templateVersions),
186
+ }
187
+ );
188
+ removeDependenciesFromPackageJson(context, [], unusedDevDependencies);
189
+
190
+ if (stillUsedDevDependencies.length > 0) {
191
+ followUps.push(
192
+ 'These webpack packages were kept because your root build config still uses them. Remove them once the config is ported to rspack:',
193
+ ...stillUsedDevDependencies.map((name) => ` ${name}`)
194
+ );
195
+ }
196
+
197
+ const packageJson = readJsonFile<PackageJson>(context, 'package.json');
198
+ const { scripts, unparsedScripts } = rewriteWebpackScripts(packageJson.scripts ?? {});
199
+
200
+ if (packageJson.scripts) {
201
+ context.updateFile('package.json', JSON.stringify({ ...packageJson, scripts }, null, 2));
202
+ }
203
+
204
+ raiseNodeEngine(context);
205
+ dropLockedRspackOnePackages(context);
206
+
207
+ if (unparsedScripts.length > 0) {
208
+ followUps.push(
209
+ 'These scripts still call webpack in a way the addition could not rewrite. Change them to call rspack with ./rspack.config.ts or ./.config/rspack/rspack.config.ts:',
210
+ ...unparsedScripts.map(([name, command]) => ` ${name}: ${command}`)
211
+ );
212
+ }
213
+
214
+ return followUps;
215
+ }
216
+
217
+ // rspack 2 needs Node 22.12. 22.23 also has type stripping on by default, so the rspack CLI can
218
+ // always load TypeScript configs natively. matches the engines range create-plugin scaffolds
219
+ const MIN_NODE_VERSION = '22.23.0';
220
+
221
+ function raiseNodeEngine(context: Context): void {
222
+ const packageJson = readJsonFile<PackageJson>(context, 'package.json');
223
+ const nodeRange = packageJson.engines?.node;
224
+ if (!nodeRange || !validRange(nodeRange)) {
225
+ return;
226
+ }
227
+
228
+ const lowestAllowedVersion = minVersion(nodeRange);
229
+ if (!lowestAllowedVersion || !lt(lowestAllowedVersion, MIN_NODE_VERSION)) {
230
+ return;
231
+ }
232
+
233
+ const engines = { ...packageJson.engines, node: '>=22.23' };
234
+ context.updateFile('package.json', JSON.stringify({ ...packageJson, engines }, null, 2));
235
+ }
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
+
265
+ // npm refuses to move @rspack/cli and @rspack/core from 1 to 2 in one install while the lockfile
266
+ // still pins the old versions (ERESOLVE). dropping the locked @rspack packages lets npm resolve
267
+ // them again from package.json. pnpm and yarn upgrade them without help
268
+ const LOCKED_RSPACK_PACKAGE_PATTERN = /(^|\/)node_modules\/@rspack\//;
269
+
270
+ function dropLockedRspackOnePackages(context: Context): void {
271
+ if (!context.doesFileExist('package-lock.json')) {
272
+ return;
273
+ }
274
+
275
+ const lockfile = readJsonFile<{ packages?: Record<string, { version?: string }> }>(context, 'package-lock.json');
276
+ const lockedCoreVersion = lockfile.packages?.['node_modules/@rspack/core']?.version;
277
+ if (!lockfile.packages || !lockedCoreVersion || major(lockedCoreVersion) >= 2) {
278
+ return;
279
+ }
280
+
281
+ const packages = Object.fromEntries(
282
+ Object.entries(lockfile.packages).filter(([path]) => !LOCKED_RSPACK_PACKAGE_PATTERN.test(path))
283
+ );
284
+ context.updateFile('package-lock.json', JSON.stringify({ ...lockfile, packages }, null, 2) + '\n');
285
+ }
286
+
287
+ // a webpack or webpack-cli command at the start of a script, after a shell separator, after env
288
+ // assignments or after npx. the lookbehind keeps the separator and prefixes out of the match
289
+ const WEBPACK_COMMAND_PATTERN = /(?<=(?:^|&&|\|\||;|\|)\s*(?:\w+=\S*\s+)*(?:npx\s+)?)webpack(?:-cli)?(?=\s|$)/g;
290
+ const ROOT_WEBPACK_CONFIG_ARG_PATTERN =
291
+ /((?:^|\s)(?:-c|--config)(?:\s+|=))(?:\.\/)?webpack\.config\.[cm]?[jt]s(?=\s|$)/g;
292
+ const TEMPLATE_WEBPACK_CONFIG_ARG_PATTERN =
293
+ /((?:^|\s)(?:-c|--config)(?:\s+|=))(?:\.\/)?\.config\/webpack\/webpack\.config\.ts(?=\s|$)/g;
294
+ // anything that still runs webpack after the rewrite, e.g. node_modules/webpack-cli/bin/cli.js
295
+ const REMAINING_WEBPACK_PATTERN = /(?:^|[\s/])webpack(?:-cli)?(?:[\s/]|$)|\.config\/webpack\//;
296
+
297
+ function rewriteWebpackScripts(scripts: Record<string, string>) {
298
+ const unparsedScripts: Array<[string, string]> = [];
299
+ const rewrittenScripts = Object.fromEntries(
300
+ Object.entries(scripts).map(([name, command]) => {
301
+ const rewritten = command
302
+ .replace(WEBPACK_COMMAND_PATTERN, 'rspack')
303
+ .replace(ROOT_WEBPACK_CONFIG_ARG_PATTERN, '$1./rspack.config.ts')
304
+ .replace(TEMPLATE_WEBPACK_CONFIG_ARG_PATTERN, '$1./.config/rspack/rspack.config.ts');
305
+
306
+ if (REMAINING_WEBPACK_PATTERN.test(rewritten)) {
307
+ unparsedScripts.push([name, command]);
308
+ return [name, command];
309
+ }
310
+
311
+ return [name, rewritten];
312
+ })
313
+ );
314
+
315
+ return { scripts: rewrittenScripts, unparsedScripts };
316
+ }
317
+
318
+ // root level webpack.* and rspack.* files, e.g. webpack.config.ts and helpers like webpack.config.utils.ts
319
+ const USER_BUILD_FILE_PATTERN = /^(webpack|rspack)\.[\w.-]*\.[cm]?[jt]s$/;
320
+
321
+ function readUserBuildFileSources(context: Context): string[] {
322
+ return context
323
+ .readDir('.')
324
+ .filter((filePath) => USER_BUILD_FILE_PATTERN.test(filePath))
325
+ .map((filePath) => context.getFile(filePath) ?? '');
326
+ }
327
+
328
+ // matches the package as a module specifier or loader string: 'name', "name", `name` or 'name/subpath'
329
+ function referencesPackage(source: string, packageName: string): boolean {
330
+ const escapedName = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
331
+ return new RegExp(`['"\`]${escapedName}(/[^'"\`]*)?['"\`]`).test(source);
332
+ }
333
+
334
+ // packages the rspack base config and its helpers need that webpack plugins do not
335
+ export const RSPACK_DEV_DEPENDENCIES = [
336
+ '@rspack/cli',
337
+ '@rspack/core',
338
+ '@types/ws',
339
+ 'diagnostics-webpack-plugin',
340
+ 'rspack-merge',
341
+ 'rspack-plugin-virtual-module',
342
+ 'ts-checker-rspack-plugin',
343
+ // diagnostics-webpack-plugin peers on webpack ^5.106
344
+ 'webpack',
345
+ 'ws',
346
+ ];
347
+
348
+ // packages only the webpack base config uses. removed unless a root build file still imports them
349
+ export const WEBPACK_ONLY_DEV_DEPENDENCIES = [
350
+ 'copy-webpack-plugin',
351
+ 'css-loader',
352
+ 'eslint-webpack-plugin',
353
+ 'fork-ts-checker-webpack-plugin',
354
+ 'imports-loader',
355
+ 'style-loader',
356
+ 'swc-loader',
357
+ 'terser-webpack-plugin',
358
+ 'webpack-cli',
359
+ 'webpack-livereload-plugin',
360
+ 'webpack-subresource-integrity',
361
+ 'webpack-virtual-modules',
362
+ ];
363
+
364
+ // packages the rendered .config/rspack and .config/bundler files load at build time that every
365
+ // scaffold gets, so older plugins may have removed some of them. added only when missing.
366
+ // @swc/helpers is needed because builtin:swc-loader runs with externalHelpers, which webpack did not
367
+ export const BASE_CONFIG_DEV_DEPENDENCIES = ['@swc/helpers', 'replace-in-file-webpack-plugin', 'sass', 'sass-loader'];
368
+
369
+ // the lists above say which packages the addition manages. their versions come from the package.json
370
+ // template, so they always match the .config files rendered from that same template. the template is
371
+ // rendered with fixed data, since only devDependencies are read and they do not depend on the plugin
372
+ const PACKAGE_JSON_TEMPLATE_DATA = {
373
+ ...RSPACK_TEMPLATE_DATA_OVERRIDES,
374
+ pluginName: 'plugin',
375
+ orgName: 'org',
376
+ pluginType: 'panel',
377
+ packageManagerName: 'npm',
378
+ packageManagerVersion: '10.0.0',
379
+ scenesVersion: '*',
380
+ isAppType: false,
381
+ isNPM: true,
382
+ };
383
+
384
+ export function getTemplateDevDependencyVersions(): Record<string, string> {
385
+ const template = readFileSync(resolveTemplatePath('_package.json'), 'utf-8');
386
+ const rendered: { devDependencies?: Record<string, string> } = JSON.parse(
387
+ renderHandlebarsTemplate(template, PACKAGE_JSON_TEMPLATE_DATA)
388
+ );
389
+ return rendered.devDependencies ?? {};
390
+ }
391
+
392
+ function pickVersions(names: string[], templateVersions: Record<string, string>): Record<string, string> {
393
+ return Object.fromEntries(
394
+ names.map((name) => {
395
+ const version = templateVersions[name];
396
+ if (!version) {
397
+ throw new Error(`The package.json template has no version for ${name}, which the rspack addition installs.`);
398
+ }
399
+ return [name, version];
400
+ })
401
+ );
402
+ }
403
+
404
+ // a plugin that already has one of these keeps its version
405
+ function getMissingPackages(context: Context, names: string[]): string[] {
406
+ const packageJson = readJsonFile<PackageJson>(context, 'package.json');
407
+ const installed = { ...packageJson.dependencies, ...packageJson.devDependencies };
408
+ return names.filter((name) => !(name in installed));
409
+ }
410
+
411
+ function deleteWebpackTemplateFiles(context: Context): string[] {
412
+ for (const filePath of WEBPACK_TEMPLATE_FILES) {
413
+ if (context.doesFileExist(filePath)) {
414
+ context.deleteFile(filePath);
415
+ }
416
+ }
417
+
418
+ const leftoverFiles = context.readDir('.config/webpack');
419
+ if (leftoverFiles.length === 0) {
420
+ return [];
421
+ }
422
+
423
+ return [
424
+ 'These files in .config/webpack were not created by create-plugin, so they were kept. Move anything you still need out of .config and port it to rspack:',
425
+ ...leftoverFiles.map((filePath) => ` ${filePath}`),
426
+ ];
427
+ }
428
+
429
+ function getRootRspackConfigStub(rootWebpackConfig: string): string {
430
+ return `import type { Configuration } from '@rspack/core';
431
+ import { merge } from 'rspack-merge';
432
+
433
+ import grafanaConfig, { type Env } from './.config/rspack/rspack.config.ts';
434
+
435
+ // TODO(rspack): port the customisations in ./${rootWebpackConfig} to this file, then delete it.
436
+ // Run \`npx @grafana/create-plugin@latest add rspack --agent\` to have an AI agent do the port,
437
+ // or follow https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations
438
+ // This file is loaded as native ESM, so CommonJS globals are not available. Use import.meta.dirname,
439
+ // createRequire(import.meta.url) and .ts extensions on relative imports.
440
+ throw new Error(
441
+ '[rspack] ${rootWebpackConfig} has not been ported to rspack.config.ts yet. ' +
442
+ 'Run \`npx @grafana/create-plugin@latest add rspack --agent\` or port it by hand, then remove this error.'
443
+ );
444
+
445
+ const config = async (env: Env): Promise<Configuration> => {
446
+ const baseConfig = await grafanaConfig(env);
447
+
448
+ return merge(baseConfig, {});
449
+ };
450
+
451
+ export default config;
452
+ `;
453
+ }
@@ -89,8 +89,17 @@ describe('buildSystemPrompt', () => {
89
89
  expect(systemPrompt).toContain('Write it with your file-writing tool, not a shell command.');
90
90
  });
91
91
 
92
- it('should tell the agent to fix failing checks rather than suppress them', () => {
93
- expect(systemPrompt).toContain('fix the cause. Never make it pass by suppressing it');
92
+ it('should tell the agent to fix failing checks rather than hide them', () => {
93
+ expect(systemPrompt).toContain('fix the cause. Never hide a failure');
94
+ });
95
+
96
+ it('should defer to the addition instructions for setting aside what cannot be applied', () => {
97
+ // additions keep the build passing by commenting out unportable code under a TODO marker,
98
+ // so the contract must not forbid that or tell the agent to leave the check failing instead
99
+ expect(systemPrompt).toContain(
100
+ 'If the addition instructions say how to set aside something that cannot be applied, such as commenting it out under a `TODO(<addition-name>):` marker, follow them.'
101
+ );
102
+ expect(systemPrompt).not.toMatch(/or changed scripts\./);
94
103
  });
95
104
 
96
105
  it('should render the package manager commands', () => {
@@ -76,7 +76,7 @@ Before changing any file, print a short plan of what you intend to do so the use
76
76
 
77
77
  <scope_rules>
78
78
  Make only the changes the addition instructions require. Do not run package installs, builds, or tests unless the instructions ask for them. Run the repository formatter on files you touched if one is configured. Do not reformat files you did not change.
79
- When a check fails (typecheck, lint, build or tests), fix the cause. Never make it pass by suppressing it: no disable comments, skipped or deleted tests, loosened compiler or lint settings, or changed scripts. If you cannot fix the cause, leave it failing and say so in your summary.
79
+ When a check fails (typecheck, lint, build or tests), fix the cause. Never hide a failure: no disable comments, skipped or deleted tests, loosened compiler or lint settings, or scripts changed to skip the check. If the addition instructions say how to set aside something that cannot be applied, such as commenting it out under a \`TODO(<addition-name>):\` marker, follow them. Otherwise leave the check failing and say so in your summary.
80
80
  </scope_rules>
81
81
 
82
82
  <handoff_contract>
@@ -133,6 +133,13 @@ export default [
133
133
  description: 'Replace the glob package with the Node.js built-in fs.glob in bundler utils.',
134
134
  scriptPath: import.meta.resolve('./scripts/019-remove-glob-dependency.js'),
135
135
  },
136
+ {
137
+ name: '020-rename-rspack-feature-flag',
138
+ version: '7.18.0',
139
+ description:
140
+ 'Rename the useExperimentalRspack feature flag to useRspack in .config/.cprc.json, and remove it from the plugin .cprc.json.',
141
+ scriptPath: import.meta.resolve('./scripts/020-rename-rspack-feature-flag.js'),
142
+ },
136
143
  // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
137
144
  // for those written before the switch to updates as migrations.
138
145
  //
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import migrate from './020-rename-rspack-feature-flag.js';
3
+ import { Context } from '../../context.js';
4
+
5
+ const ROOT_CPRC_PATH = '.config/.cprc.json';
6
+ const USER_CPRC_PATH = '.cprc.json';
7
+
8
+ function setupContext(files: { root?: object; user?: object }) {
9
+ const context = new Context('/virtual');
10
+ if (files.root) {
11
+ context.addFile(ROOT_CPRC_PATH, JSON.stringify(files.root, null, 2));
12
+ }
13
+ if (files.user) {
14
+ context.addFile(USER_CPRC_PATH, JSON.stringify(files.user, null, 2));
15
+ }
16
+ return context;
17
+ }
18
+
19
+ function readFeatures(context: Context, path: string) {
20
+ return JSON.parse(context.getFile(path) || '{}').features;
21
+ }
22
+
23
+ describe('020-rename-rspack-feature-flag', () => {
24
+ it('should rename the flag in .config/.cprc.json', () => {
25
+ const result = migrate(setupContext({ root: { version: '7.9.0', features: { useExperimentalRspack: true } } }));
26
+
27
+ expect(readFeatures(result, ROOT_CPRC_PATH)).toEqual({ useRspack: true });
28
+ });
29
+
30
+ it('should move the flag from the plugin .cprc.json to .config/.cprc.json', () => {
31
+ const result = migrate(
32
+ setupContext({
33
+ root: { version: '7.9.0', features: {} },
34
+ user: { features: { bundleGrafanaUI: false, useExperimentalRspack: true } },
35
+ })
36
+ );
37
+
38
+ expect(readFeatures(result, ROOT_CPRC_PATH)).toEqual({ useRspack: true });
39
+ expect(readFeatures(result, USER_CPRC_PATH)).toEqual({ bundleGrafanaUI: false });
40
+ });
41
+
42
+ it('should remove a disabled flag without turning rspack on', () => {
43
+ const result = migrate(
44
+ setupContext({
45
+ root: { version: '7.9.0', features: {} },
46
+ user: { features: { bundleGrafanaUI: false, useExperimentalRspack: false } },
47
+ })
48
+ );
49
+
50
+ expect(readFeatures(result, ROOT_CPRC_PATH)).toEqual({});
51
+ expect(readFeatures(result, USER_CPRC_PATH)).toEqual({ bundleGrafanaUI: false });
52
+ });
53
+
54
+ it('should keep the version and other feature flags', () => {
55
+ const result = migrate(
56
+ setupContext({ root: { version: '7.9.0', features: { bundleGrafanaUI: true, useExperimentalRspack: true } } })
57
+ );
58
+
59
+ expect(JSON.parse(result.getFile(ROOT_CPRC_PATH) || '{}')).toEqual({
60
+ version: '7.9.0',
61
+ features: { bundleGrafanaUI: true, useRspack: true },
62
+ });
63
+ });
64
+
65
+ it('should not rewrite config files that never had the flag', () => {
66
+ const context = setupContext({
67
+ root: { version: '7.9.0', features: {} },
68
+ user: { features: { bundleGrafanaUI: false } },
69
+ });
70
+ // formatting a rewrite would change, so an untouched file keeps it
71
+ context.updateFile(ROOT_CPRC_PATH, '{"version":"7.9.0","features":{}}');
72
+ context.updateFile(USER_CPRC_PATH, '{"features":{"bundleGrafanaUI":false}}');
73
+
74
+ const result = migrate(context);
75
+
76
+ expect(result.getFile(ROOT_CPRC_PATH)).toBe('{"version":"7.9.0","features":{}}');
77
+ expect(result.getFile(USER_CPRC_PATH)).toBe('{"features":{"bundleGrafanaUI":false}}');
78
+ });
79
+
80
+ it('should not fail when neither config file exists', () => {
81
+ const result = migrate(new Context('/virtual'));
82
+
83
+ expect(result.hasChanges()).toBe(false);
84
+ });
85
+
86
+ it('should be idempotent', async () => {
87
+ await expect(migrate).toBeIdempotent(
88
+ setupContext({
89
+ root: { version: '7.9.0', features: { useExperimentalRspack: true } },
90
+ user: { features: { bundleGrafanaUI: false, useExperimentalRspack: false } },
91
+ })
92
+ );
93
+ });
94
+ });
@@ -0,0 +1,8 @@
1
+ import type { Context } from '../../context.js';
2
+ import { replaceExperimentalRspackFlag } from '../../utils.rspack.js';
3
+
4
+ // only renames the flag. plugins on the experimental rspack setup move to rspack 2 by running `add rspack`
5
+ export default function migrate(context: Context): Context {
6
+ replaceExperimentalRspackFlag(context, { enable: false });
7
+ return context;
8
+ }
@@ -0,0 +1,58 @@
1
+ import type { Context } from './context.js';
2
+ import { readJsonFile } from './utils.js';
3
+
4
+ const ROOT_CPRC_PATH = '.config/.cprc.json';
5
+ const USER_CPRC_PATH = '.cprc.json';
6
+ const EXPERIMENTAL_RSPACK_FLAG = 'useExperimentalRspack';
7
+
8
+ interface CprcConfig {
9
+ features?: Record<string, unknown>;
10
+ }
11
+
12
+ /**
13
+ * Replaces the `useExperimentalRspack` feature flag with `useRspack` in `.config/.cprc.json`, and removes the old flag
14
+ * from both config files. Every generated plugin's own `.cprc.json` carried the old flag as `false`, and that file
15
+ * overrides `.config/.cprc.json`. `useRspack` is set when the old flag was on in either file, or when `enable` is true.
16
+ * Files that need no change are left untouched.
17
+ */
18
+ export function replaceExperimentalRspackFlag(context: Context, { enable }: { enable: boolean }): void {
19
+ const wasRspack = [ROOT_CPRC_PATH, USER_CPRC_PATH].some(
20
+ (path) => readCprc(context, path)?.features?.[EXPERIMENTAL_RSPACK_FLAG] === true
21
+ );
22
+
23
+ updateFeatures(context, USER_CPRC_PATH, (features) => withoutExperimentalFlag(features));
24
+ updateFeatures(context, ROOT_CPRC_PATH, (features) => {
25
+ const cleaned = withoutExperimentalFlag(features);
26
+ return enable || wasRspack ? { ...cleaned, useRspack: true } : cleaned;
27
+ });
28
+ }
29
+
30
+ function readCprc(context: Context, path: string): CprcConfig | undefined {
31
+ if (!context.doesFileExist(path)) {
32
+ return undefined;
33
+ }
34
+ return readJsonFile<CprcConfig>(context, path);
35
+ }
36
+
37
+ function updateFeatures(
38
+ context: Context,
39
+ path: string,
40
+ update: (features: Record<string, unknown>) => Record<string, unknown>
41
+ ): void {
42
+ const config = readCprc(context, path);
43
+ if (!config) {
44
+ return;
45
+ }
46
+
47
+ const features = config.features ?? {};
48
+ const updatedFeatures = update(features);
49
+ if (JSON.stringify(updatedFeatures) === JSON.stringify(features)) {
50
+ return;
51
+ }
52
+
53
+ context.updateFile(path, JSON.stringify({ ...config, features: updatedFeatures }, null, 2));
54
+ }
55
+
56
+ function withoutExperimentalFlag(features: Record<string, unknown>): Record<string, unknown> {
57
+ return Object.fromEntries(Object.entries(features).filter(([name]) => name !== EXPERIMENTAL_RSPACK_FLAG));
58
+ }
@@ -236,8 +236,11 @@ describe('utils', () => {
236
236
 
237
237
  it('reads feature flags from the .cprc.json files', async () => {
238
238
  await mkdir(join(tmpDir, '.config'));
239
- await writeFile(join(tmpDir, '.config/.cprc.json'), JSON.stringify({ features: { bundleGrafanaUI: true } }));
240
- await writeFile(join(tmpDir, '.cprc.json'), JSON.stringify({ features: { useExperimentalRspack: true } }));
239
+ await writeFile(
240
+ join(tmpDir, '.config/.cprc.json'),
241
+ JSON.stringify({ features: { bundleGrafanaUI: true, useRspack: true } })
242
+ );
243
+ await writeFile(join(tmpDir, '.cprc.json'), JSON.stringify({ features: { bundleGrafanaUI: true } }));
241
244
  expect(getTemplateDataFromContext(pluginContext())).toMatchObject({
242
245
  bundleGrafanaUI: true,
243
246
  frontendBundler: 'rspack',