@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,121 @@
1
+ import type { Context } from '../../context.js';
2
+ import * as recast from 'recast';
3
+ import {
4
+ callsFunction,
5
+ createImport,
6
+ findImports,
7
+ findFunctionDeclaration,
8
+ findNamedImport,
9
+ insertImports,
10
+ parseAsTypescript,
11
+ printAST,
12
+ } from '../../utils.ast.js';
13
+ import { migrationsDebug, removeDependenciesFromPackageJson } from '../../utils.js';
14
+
15
+ const OLD_PACKAGE_NAME = 'glob';
16
+ const NEW_PACKAGE_NAME = 'node:fs/promises';
17
+
18
+ const UTILS_PATHS = ['.config/webpack/utils.ts', '.config/bundler/utils.ts'];
19
+
20
+ const GET_ENTRIES_SOURCE = `export async function getEntries() {
21
+ const plugins: string[][] = [];
22
+ for await (const pluginJson of glob('**/src/**/plugin.json')) {
23
+ const folder = path.dirname(pluginJson);
24
+ const modules: string[] = [];
25
+ for await (const module of glob(\`\${folder}/module.{ts,tsx,js,jsx}\`)) {
26
+ modules.push(path.resolve(module));
27
+ }
28
+ plugins.push(modules);
29
+ }
30
+
31
+ return plugins.reduce<Record<string, string>>((result, modules) => {
32
+ return modules.reduce((innerResult, module) => {
33
+ const pluginPath = path.dirname(module);
34
+ const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\\/?/i, '');
35
+ const entryName = pluginName === '' ? 'module' : \`\${pluginName}/module\`;
36
+
37
+ innerResult[entryName] = module;
38
+ return innerResult;
39
+ }, result);
40
+ }, {});
41
+ }`;
42
+
43
+ export default function migrate(context: Context) {
44
+ const updatedPaths = UTILS_PATHS.filter((filePath) => replaceGlobWithFsGlob(context, filePath));
45
+ if (updatedPaths.length === 0) {
46
+ return context;
47
+ }
48
+
49
+ const isStillUsed = UTILS_PATHS.some((filePath) => {
50
+ const source = context.getFile(filePath);
51
+ if (!source) {
52
+ return false;
53
+ }
54
+ const parsed = parseAsTypescript(source);
55
+ return !parsed.success || findImports(parsed.ast, OLD_PACKAGE_NAME).length > 0;
56
+ });
57
+
58
+ if (context.doesFileExist('package.json') && !isStillUsed) {
59
+ removeDependenciesFromPackageJson(context, [OLD_PACKAGE_NAME], [OLD_PACKAGE_NAME]);
60
+ }
61
+
62
+ return context;
63
+ }
64
+
65
+ function replaceGlobWithFsGlob(context: Context, utilsPath: string): boolean {
66
+ if (!context.doesFileExist(utilsPath)) {
67
+ return false;
68
+ }
69
+
70
+ const source = context.getFile(utilsPath);
71
+ if (!source || !source.includes(OLD_PACKAGE_NAME)) {
72
+ return false;
73
+ }
74
+
75
+ const parsed = parseAsTypescript(source);
76
+ if (!parsed.success) {
77
+ migrationsDebug(`Failed to parse ${utilsPath}. Error: ${parsed.error.message}`);
78
+ return false;
79
+ }
80
+
81
+ const globImport = findNamedImport(parsed.ast, OLD_PACKAGE_NAME, 'glob');
82
+ if (!globImport) {
83
+ return false;
84
+ }
85
+
86
+ const getEntries = findFunctionDeclaration(parsed.ast, 'getEntries');
87
+ if (!getEntries) {
88
+ return false;
89
+ }
90
+
91
+ // The new body calls glob directly, so an aliased import (and its calls) is left alone.
92
+ if (!callsFunction(getEntries.body, 'glob')) {
93
+ return false;
94
+ }
95
+
96
+ const { declaration, specifier } = globImport;
97
+ declaration.specifiers = (declaration.specifiers ?? []).filter((candidate) => candidate !== specifier);
98
+ if (declaration.specifiers.length === 0) {
99
+ parsed.ast.program.body.splice(parsed.ast.program.body.indexOf(declaration), 1);
100
+ }
101
+ insertImports(parsed.ast, [createImport({ named: [{ name: 'glob' }] }, NEW_PACKAGE_NAME)]);
102
+
103
+ getEntries.body = getFunctionBody(GET_ENTRIES_SOURCE);
104
+
105
+ context.updateFile(utilsPath, printAST(parsed.ast));
106
+ return true;
107
+ }
108
+
109
+ function getFunctionBody(source: string): recast.types.namedTypes.BlockStatement {
110
+ const parsed = parseAsTypescript(source);
111
+ if (!parsed.success) {
112
+ throw parsed.error;
113
+ }
114
+
115
+ const [statement] = parsed.ast.program.body;
116
+ if (statement?.type !== 'ExportNamedDeclaration' || statement.declaration?.type !== 'FunctionDeclaration') {
117
+ throw new Error('Failed to parse target function body');
118
+ }
119
+
120
+ return statement.declaration.body;
121
+ }
@@ -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
+ }
@@ -7,6 +7,10 @@ import {
7
7
  findObjectProperty,
8
8
  createImport,
9
9
  insertImports,
10
+ findImports,
11
+ findNamedImport,
12
+ findFunctionDeclaration,
13
+ callsFunction,
10
14
  } from './utils.ast.js';
11
15
 
12
16
  // Helper to create a simple test AST from source code
@@ -31,6 +35,58 @@ function findFirstNode<T extends recast.types.namedTypes.Node>(ast: recast.types
31
35
  }
32
36
 
33
37
  describe('utils.ast', () => {
38
+ describe('Declaration Utilities', () => {
39
+ describe('findFunctionDeclaration', () => {
40
+ it('should find a function declaration by name', () => {
41
+ const ast = createTestAST(`function foo() {}\nfunction bar() {}`);
42
+
43
+ const declaration = findFunctionDeclaration(ast, 'bar');
44
+
45
+ expect(declaration?.id?.name).toBe('bar');
46
+ });
47
+
48
+ it('should find an exported function declaration', () => {
49
+ const ast = createTestAST(`export async function getEntries() {}`);
50
+
51
+ const declaration = findFunctionDeclaration(ast, 'getEntries');
52
+
53
+ expect(declaration?.async).toBe(true);
54
+ });
55
+
56
+ it('should return null when the function is not declared', () => {
57
+ const ast = createTestAST(`const getEntries = () => {};`);
58
+
59
+ expect(findFunctionDeclaration(ast, 'getEntries')).toBeNull();
60
+ });
61
+ });
62
+
63
+ describe('callsFunction', () => {
64
+ it('should return true when the function is called', () => {
65
+ const ast = createTestAST(`async function run() { const files = await glob('**/*.ts'); }`);
66
+
67
+ expect(callsFunction(ast, 'glob')).toBe(true);
68
+ });
69
+
70
+ it('should return true for nested calls', () => {
71
+ const ast = createTestAST(`items.map((item) => wrap(glob(item)));`);
72
+
73
+ expect(callsFunction(ast, 'glob')).toBe(true);
74
+ });
75
+
76
+ it('should return false when the function is only referenced', () => {
77
+ const ast = createTestAST(`const fn = glob;`);
78
+
79
+ expect(callsFunction(ast, 'glob')).toBe(false);
80
+ });
81
+
82
+ it('should return false for a method with the same name', () => {
83
+ const ast = createTestAST(`fs.glob('**/*.ts');`);
84
+
85
+ expect(callsFunction(ast, 'glob')).toBe(false);
86
+ });
87
+ });
88
+ });
89
+
34
90
  describe('Property Utilities', () => {
35
91
  describe('isProperty', () => {
36
92
  it('should return true for Property nodes', () => {
@@ -191,5 +247,100 @@ describe('utils.ast', () => {
191
247
  expect(output).toBe(`import React from 'react';\nimport { map } from 'lodash';\nconst x = 1;`);
192
248
  });
193
249
  });
250
+
251
+ describe('findImports', () => {
252
+ it('should return an empty array when the package is not imported', () => {
253
+ const ast = createTestAST(`import React from 'react';\nconst x = 1;`);
254
+
255
+ expect(findImports(ast, 'glob')).toEqual([]);
256
+ });
257
+
258
+ it('should return the matching import declaration', () => {
259
+ const ast = createTestAST(`import React from 'react';\nimport { glob } from 'glob';`);
260
+
261
+ const imports = findImports(ast, 'glob');
262
+
263
+ expect(imports).toHaveLength(1);
264
+ expect(imports[0].source.value).toBe('glob');
265
+ });
266
+
267
+ it('should return every declaration importing the package', () => {
268
+ const ast = createTestAST(`import { glob } from 'glob';\nimport type { GlobOptions } from 'glob';`);
269
+
270
+ expect(findImports(ast, 'glob')).toHaveLength(2);
271
+ });
272
+
273
+ it('should not match packages with a similar name', () => {
274
+ const ast = createTestAST(`import globParent from 'glob-parent';\nimport { glob } from 'node:fs/promises';`);
275
+
276
+ expect(findImports(ast, 'glob')).toEqual([]);
277
+ });
278
+ });
279
+
280
+ describe('findNamedImport', () => {
281
+ it('should find a named import', () => {
282
+ const ast = createTestAST(`import { glob } from 'glob';`);
283
+
284
+ const match = findNamedImport(ast, 'glob', 'glob');
285
+
286
+ expect(match).toBeDefined();
287
+ expect(match?.declaration.source.value).toBe('glob');
288
+ expect(match?.specifier.imported.name).toBe('glob');
289
+ expect(match?.specifier.local?.name).toBe('glob');
290
+ });
291
+
292
+ it('should find an aliased named import by its imported name', () => {
293
+ const ast = createTestAST(`import { glob as g } from 'glob';`);
294
+
295
+ const match = findNamedImport(ast, 'glob', 'glob');
296
+
297
+ expect(match).toBeDefined();
298
+ expect(match?.specifier.local?.name).toBe('g');
299
+ });
300
+
301
+ it('should find the specifier among other specifiers', () => {
302
+ const ast = createTestAST(`import { globSync, glob } from 'glob';`);
303
+
304
+ const match = findNamedImport(ast, 'glob', 'glob');
305
+
306
+ expect(match).toBeDefined();
307
+ expect(match?.declaration.specifiers).toHaveLength(2);
308
+ expect(match?.specifier.imported.name).toBe('glob');
309
+ });
310
+
311
+ it('should find the specifier in a later declaration of the same package', () => {
312
+ const ast = createTestAST(`import type { GlobOptions } from 'glob';\nimport { glob } from 'glob';`);
313
+ const [, secondDeclaration] = findImports(ast, 'glob');
314
+
315
+ const match = findNamedImport(ast, 'glob', 'glob');
316
+
317
+ expect(match).toBeDefined();
318
+ expect(match?.declaration).toBe(secondDeclaration);
319
+ });
320
+
321
+ it('should return undefined when only other named imports exist', () => {
322
+ const ast = createTestAST(`import { globSync } from 'glob';`);
323
+
324
+ expect(findNamedImport(ast, 'glob', 'glob')).toBeUndefined();
325
+ });
326
+
327
+ it('should return undefined for a default import', () => {
328
+ const ast = createTestAST(`import glob from 'glob';`);
329
+
330
+ expect(findNamedImport(ast, 'glob', 'glob')).toBeUndefined();
331
+ });
332
+
333
+ it('should return undefined for a namespace import', () => {
334
+ const ast = createTestAST(`import * as glob from 'glob';`);
335
+
336
+ expect(findNamedImport(ast, 'glob', 'glob')).toBeUndefined();
337
+ });
338
+
339
+ it('should return undefined when the package is not imported', () => {
340
+ const ast = createTestAST(`import { glob } from 'node:fs/promises';`);
341
+
342
+ expect(findNamedImport(ast, 'glob', 'glob')).toBeUndefined();
343
+ });
344
+ });
194
345
  });
195
346
  });
@@ -45,6 +45,42 @@ export function findVariableDeclaration(
45
45
  return found;
46
46
  }
47
47
 
48
+ export function findFunctionDeclaration(
49
+ ast: recast.types.ASTNode,
50
+ functionName: string
51
+ ): recast.types.namedTypes.FunctionDeclaration | null {
52
+ let found = null;
53
+
54
+ recast.types.visit(ast, {
55
+ visitFunctionDeclaration(path) {
56
+ if (path.node.id?.name === functionName) {
57
+ found = path.node;
58
+ return false; // Stop traversal
59
+ }
60
+ return this.traverse(path);
61
+ },
62
+ });
63
+
64
+ return found;
65
+ }
66
+
67
+ export function callsFunction(ast: recast.types.ASTNode, functionName: string): boolean {
68
+ let isCalled = false;
69
+
70
+ recast.types.visit(ast, {
71
+ visitCallExpression(path) {
72
+ const { callee } = path.node;
73
+ if (callee.type === 'Identifier' && callee.name === functionName) {
74
+ isCalled = true;
75
+ return false; // Stop traversal
76
+ }
77
+ return this.traverse(path);
78
+ },
79
+ });
80
+
81
+ return isCalled;
82
+ }
83
+
48
84
  export function isProperty(
49
85
  node: recast.types.ASTNode
50
86
  ): node is recast.types.namedTypes.Property | recast.types.namedTypes.ObjectProperty {
@@ -108,3 +144,27 @@ export function insertImports(
108
144
  ast.program.body.unshift(...imports);
109
145
  }
110
146
  }
147
+
148
+ export function findImports(
149
+ ast: recast.types.namedTypes.File,
150
+ packageName: string
151
+ ): recast.types.namedTypes.ImportDeclaration[] {
152
+ return ast.program.body.filter(
153
+ (node): node is recast.types.namedTypes.ImportDeclaration =>
154
+ node.type === 'ImportDeclaration' && node.source.value === packageName
155
+ );
156
+ }
157
+
158
+ export function findNamedImport(ast: recast.types.namedTypes.File, packageName: string, importedName: string) {
159
+ for (const declaration of findImports(ast, packageName)) {
160
+ const specifier = declaration.specifiers?.find(
161
+ (candidate): candidate is recast.types.namedTypes.ImportSpecifier =>
162
+ candidate.type === 'ImportSpecifier' && candidate.imported.name === importedName
163
+ );
164
+ if (specifier) {
165
+ return { declaration, specifier };
166
+ }
167
+ }
168
+
169
+ return undefined;
170
+ }
@@ -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',
@@ -140,16 +140,8 @@ function getActionsForTemplateFolder({
140
140
  files = files.filter((file) => path.basename(file) !== 'npmrc');
141
141
  }
142
142
 
143
- // filter out frontend bundler based on user choice
144
- files = files.filter((file) => {
145
- if (file.includes('webpack') && templateData.useExperimentalRspack) {
146
- return false;
147
- }
148
- if (file.includes('rspack') && !templateData.useExperimentalRspack) {
149
- return false;
150
- }
151
- return true;
152
- });
143
+ // new plugins always use webpack. the rspack templates are only rendered by `add rspack`
144
+ files = files.filter((file) => !file.includes('rspack'));
153
145
 
154
146
  function getFileExportPath(f: string) {
155
147
  return path.relative(folderPath, path.dirname(f));
package/src/constants.ts CHANGED
@@ -56,7 +56,6 @@ export const EXTRA_TEMPLATE_VARIABLES = {
56
56
 
57
57
  export const DEFAULT_FEATURE_FLAGS = {
58
58
  bundleGrafanaUI: false,
59
- useExperimentalRspack: false,
60
59
  useExperimentalUpdates: true,
61
60
  };
62
61
 
package/src/types.ts CHANGED
@@ -23,7 +23,7 @@ export type TemplateData = {
23
23
  version: string;
24
24
  bundleGrafanaUI: boolean;
25
25
  scenesVersion: string;
26
- useExperimentalRspack: boolean;
26
+ useRspack: boolean;
27
27
  pluginExecutable?: string;
28
28
  frontendBundler: 'webpack' | 'rspack';
29
29
  };
@@ -44,6 +44,16 @@ describe('getConfig', () => {
44
44
  });
45
45
  });
46
46
 
47
+ it('should not let cli args switch new plugins to rspack', async () => {
48
+ mocks.argv = {
49
+ 'feature-flags': 'useRspack',
50
+ };
51
+ const config = getConfig(tmpDir);
52
+
53
+ expect(config.features).toEqual(DEFAULT_FEATURE_FLAGS);
54
+ expect(config.features).not.toHaveProperty('useRspack');
55
+ });
56
+
47
57
  it('should override default feature flags via cli args', async () => {
48
58
  mocks.argv = {
49
59
  'feature-flags': 'bundleGrafanaUI',
@@ -11,7 +11,8 @@ import { EOL } from 'node:os';
11
11
 
12
12
  export type FeatureFlags = {
13
13
  bundleGrafanaUI?: boolean;
14
- useExperimentalRspack?: boolean;
14
+ // set by `add rspack` in .config/.cprc.json. not a default flag, so --feature-flags cannot set it
15
+ useRspack?: boolean;
15
16
  useExperimentalUpdates?: boolean;
16
17
  };
17
18
 
@@ -123,8 +123,8 @@ export function buildTemplateData({
123
123
  bundleGrafanaUI: features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI,
124
124
  scenesVersion: '^8.17.0',
125
125
  pluginExecutable: pluginJson.executable,
126
- useExperimentalRspack: Boolean(features.useExperimentalRspack),
127
- frontendBundler: features.useExperimentalRspack ? 'rspack' : 'webpack',
126
+ useRspack: Boolean(features.useRspack),
127
+ frontendBundler: features.useRspack ? 'rspack' : 'webpack',
128
128
  };
129
129
  }
130
130
 
@@ -140,7 +140,7 @@ export function getTemplateData(cliArgs?: GenerateCliArgs): TemplateData {
140
140
  const { features } = getConfig();
141
141
  const currentVersion = CURRENT_APP_VERSION;
142
142
  const bundleGrafanaUI = features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI;
143
- const frontendBundler = features.useExperimentalRspack ? 'rspack' : 'webpack';
143
+ const frontendBundler = features.useRspack ? 'rspack' : 'webpack';
144
144
 
145
145
  let templateData: TemplateData;
146
146
 
@@ -164,7 +164,7 @@ export function getTemplateData(cliArgs?: GenerateCliArgs): TemplateData {
164
164
  version: currentVersion,
165
165
  bundleGrafanaUI,
166
166
  scenesVersion: '^8.17.0',
167
- useExperimentalRspack: Boolean(features.useExperimentalRspack),
167
+ useRspack: Boolean(features.useRspack),
168
168
  frontendBundler,
169
169
  };
170
170
  // Updating or migrating a plugin
@@ -28,7 +28,7 @@ Your training data about the Grafana API is out of date. Use the official docume
28
28
  - **Do not change plugin ID or plugin type** in `plugin.json`.
29
29
  - Any modifications to `plugin.json` require a **restart of the Grafana server**. Remind the user of this.
30
30
  - Use `secureJsonData` for credentials and secrets; use `jsonData` only for non-sensitive configuration.
31
- - **You must use webpack** with the configuration provided in `.config/` for frontend builds.
31
+ - **You must use {{frontendBundler}}** with the configuration provided in `.config/` for frontend builds.
32
32
  - **You must use mage** with the build targets provided by the Grafana plugin Go SDK for backend builds.
33
- - To extend webpack, prettier, eslint or other tools, use the existing configuration as a base. Follow the guide: https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations.md
33
+ - To extend {{frontendBundler}}, prettier, eslint or other tools, use the existing configuration as a base. Follow the guide: https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations.md
34
34
  - Use **`@grafana/plugin-e2e`** for end-to-end testing. Read @./.config/AGENTS/e2e-testing.md before writing or modifying e2e tests.