@grafana/create-plugin 6.8.0-canary.2356.20816081919.0 → 6.8.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 (34) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/dist/codemods/additions/additions.js +3 -3
  3. package/dist/codemods/utils.js +2 -2
  4. package/package.json +3 -3
  5. package/src/codemods/additions/additions.ts +3 -3
  6. package/templates/backend/go.mod +30 -29
  7. package/templates/backend/go.sum +62 -62
  8. package/templates/backend-app/go.mod +30 -29
  9. package/templates/backend-app/go.sum +62 -62
  10. package/templates/common/.config/bundler/copyFiles.ts +23 -0
  11. package/templates/common/.config/docker-compose-base.yaml +1 -1
  12. package/templates/common/.config/rspack/BuildModeRspackPlugin.ts +4 -4
  13. package/templates/common/.config/rspack/{liveReloadPlugin.js → liveReloadPlugin.ts} +34 -11
  14. package/templates/common/.config/rspack/rspack.config.ts +8 -22
  15. package/templates/common/.config/webpack/BuildModeWebpackPlugin.ts +1 -1
  16. package/templates/common/.config/webpack/webpack.config.ts +4 -17
  17. package/templates/common/_package.json +46 -44
  18. package/templates/github/workflows/bundle-stats.yml +1 -1
  19. package/templates/github/workflows/ci.yml +17 -10
  20. package/templates/github/workflows/is-compatible.yml +2 -2
  21. package/templates/github/workflows/release.yml +1 -1
  22. package/dist/codemods/additions/scripts/bundle-grafana-ui/index.js +0 -174
  23. package/dist/codemods/utils.bundler-config.js +0 -19
  24. package/dist/codemods/utils.externals.js +0 -116
  25. package/src/codemods/additions/scripts/bundle-grafana-ui/README.md +0 -68
  26. package/src/codemods/additions/scripts/bundle-grafana-ui/index.test.ts +0 -511
  27. package/src/codemods/additions/scripts/bundle-grafana-ui/index.ts +0 -259
  28. package/src/codemods/utils.bundler-config.ts +0 -203
  29. package/src/codemods/utils.externals.test.ts +0 -87
  30. package/src/codemods/utils.externals.ts +0 -181
  31. package/templates/common/.config/rspack/utils.ts +0 -63
  32. package/templates/common/.config/webpack/constants.ts +0 -2
  33. /package/templates/common/.config/{rspack → bundler}/constants.ts +0 -0
  34. /package/templates/common/.config/{webpack → bundler}/utils.ts +0 -0
@@ -1,259 +0,0 @@
1
- import * as v from 'valibot';
2
- import * as recast from 'recast';
3
- import { coerce, gte } from 'semver';
4
-
5
- import type { Context } from '../../../context.js';
6
- import { additionsDebug } from '../../../utils.js';
7
- import { getBundlerConfig } from '../../../utils.bundler-config.js';
8
- import { updateExternalsArray, type ExternalsArrayModifier } from '../../../utils.externals.js';
9
-
10
- const { builders } = recast.types;
11
-
12
- const PLUGIN_JSON_PATH = 'src/plugin.json';
13
- const MIN_GRAFANA_VERSION = '10.2.0';
14
-
15
- export const schema = v.object({});
16
- type BundleGrafanaUIOptions = v.InferOutput<typeof schema>;
17
-
18
- export default function bundleGrafanaUI(context: Context, _options: BundleGrafanaUIOptions): Context {
19
- additionsDebug('Running bundle-grafana-ui addition...');
20
-
21
- // Ensure minimum Grafana version requirement
22
- ensureMinGrafanaVersion(context);
23
-
24
- // Update externals array using the shared utility
25
- updateExternalsArray(context, createBundleGrafanaUIModifier());
26
-
27
- // Update bundler resolve configuration to handle ESM imports
28
- updateResolveExtensions(context);
29
-
30
- // Update module rules directly with simple string manipulation
31
- updateModuleRules(context);
32
-
33
- return context;
34
- }
35
-
36
- /**
37
- * Checks if an AST node is a regex matching a Grafana package pattern
38
- * @param element - The AST node to check
39
- * @param pattern - The regex pattern to match (e.g., "^@grafana\\/ui" or "^@grafana\\/data")
40
- */
41
- function isGrafanaRegex(element: recast.types.namedTypes.ASTNode, pattern: string): boolean {
42
- // Handle RegExpLiteral (TypeScript parser)
43
- if (element.type === 'RegExpLiteral') {
44
- const regexNode = element as recast.types.namedTypes.RegExpLiteral;
45
- return regexNode.pattern === pattern && regexNode.flags === 'i';
46
- }
47
- // Handle Literal with regex property (other parsers)
48
- if (element.type === 'Literal' && 'regex' in element && element.regex) {
49
- const regex = element.regex as { pattern: string; flags: string };
50
- return regex.pattern === pattern && regex.flags === 'i';
51
- }
52
- return false;
53
- }
54
-
55
- /**
56
- * Removes /^@grafana\/ui/i regex from externals array and adds 'react-inlinesvg'
57
- * @returns true if changes were made, false otherwise
58
- */
59
- function removeGrafanaUiAndAddReactInlineSvg(externalsArray: recast.types.namedTypes.ArrayExpression): boolean {
60
- let hasChanges = false;
61
- let hasGrafanaUiExternal = false;
62
- let hasReactInlineSvg = false;
63
-
64
- // Check current state
65
- for (const element of externalsArray.elements) {
66
- if (!element) {
67
- continue;
68
- }
69
-
70
- // Check for /^@grafana\/ui/i regex
71
- if (isGrafanaRegex(element, '^@grafana\\/ui')) {
72
- hasGrafanaUiExternal = true;
73
- }
74
-
75
- // Check for 'react-inlinesvg' string
76
- if (
77
- (element.type === 'Literal' || element.type === 'StringLiteral') &&
78
- 'value' in element &&
79
- typeof element.value === 'string' &&
80
- element.value === 'react-inlinesvg'
81
- ) {
82
- hasReactInlineSvg = true;
83
- }
84
- }
85
-
86
- // Remove /^@grafana\/ui/i if present
87
- if (hasGrafanaUiExternal) {
88
- externalsArray.elements = externalsArray.elements.filter((element) => {
89
- if (!element) {
90
- return true;
91
- }
92
- return !isGrafanaRegex(element, '^@grafana\\/ui');
93
- });
94
- hasChanges = true;
95
- additionsDebug('Removed /^@grafana\\/ui/i from externals array');
96
- }
97
-
98
- // Add 'react-inlinesvg' if not present
99
- if (!hasReactInlineSvg) {
100
- // Find the index of /^@grafana\/data/i to insert after it
101
- let insertIndex = -1;
102
- for (let i = 0; i < externalsArray.elements.length; i++) {
103
- const element = externalsArray.elements[i];
104
- if (element && isGrafanaRegex(element, '^@grafana\\/data')) {
105
- insertIndex = i + 1;
106
- break;
107
- }
108
- }
109
-
110
- if (insertIndex >= 0) {
111
- externalsArray.elements.splice(insertIndex, 0, builders.literal('react-inlinesvg'));
112
- } else {
113
- // Fallback: append to end
114
- externalsArray.elements.push(builders.literal('react-inlinesvg'));
115
- }
116
- hasChanges = true;
117
- additionsDebug("Added 'react-inlinesvg' to externals array");
118
- }
119
-
120
- return hasChanges;
121
- }
122
-
123
- /**
124
- * Creates a modifier function for updateExternalsArray that removes @grafana/ui
125
- * and adds react-inlinesvg
126
- */
127
- function createBundleGrafanaUIModifier(): ExternalsArrayModifier {
128
- return (externalsArray: recast.types.namedTypes.ArrayExpression) => {
129
- return removeGrafanaUiAndAddReactInlineSvg(externalsArray);
130
- };
131
- }
132
-
133
- /**
134
- * Updates resolve extensions to add .mjs using string manipulation
135
- */
136
- function updateResolveExtensions(context: Context): void {
137
- const config = getBundlerConfig(context);
138
- if (!config) {
139
- return;
140
- }
141
-
142
- const { path: configPath, content } = config;
143
-
144
- // Check if .mjs already exists
145
- if (content.includes("'.mjs'") || content.includes('".mjs"')) {
146
- return;
147
- }
148
-
149
- // Add .mjs to extensions array
150
- const updated = content.replace(/(extensions:\s*\[)([^\]]+)(\])/, (match, prefix, extensions, suffix) => {
151
- if (extensions.includes('.mjs')) {
152
- return match;
153
- }
154
- return `${prefix}${extensions}, '.mjs'${suffix}`;
155
- });
156
-
157
- if (updated !== content) {
158
- context.updateFile(configPath, updated);
159
- additionsDebug("Added '.mjs' to resolve.extensions");
160
- }
161
- }
162
-
163
- /**
164
- * Updates module rules to add .mjs rule using string manipulation
165
- */
166
- function updateModuleRules(context: Context): void {
167
- const config = getBundlerConfig(context);
168
- if (!config) {
169
- return;
170
- }
171
-
172
- const { path: configPath, content } = config;
173
-
174
- // Check if rule already exists
175
- if (content.includes('test: /\\.mjs$') || content.includes('test: /\\\\.mjs$')) {
176
- return;
177
- }
178
-
179
- const mjsRule = `{
180
- test: /\\.mjs$/,
181
- include: /node_modules/,
182
- resolve: {
183
- fullySpecified: false,
184
- },
185
- type: 'javascript/auto',
186
- },`;
187
-
188
- // Simple approach: find rules array and insert after first rule
189
- let updated = content;
190
-
191
- // Case 1: Empty array - insert at start
192
- if (content.match(/rules:\s*\[\s*\]/)) {
193
- updated = content.replace(/(rules:\s*\[\s*)(\])/, `$1${mjsRule}\n $2`);
194
- }
195
- // Case 2: Find first rule and insert after it
196
- else {
197
- // Match: rules: [ { ... }, and insert mjs rule after the first rule
198
- // The regex finds the first complete rule object (balanced braces)
199
- updated = content.replace(/(rules:\s*\[\s*)(\{[\s\S]*?\}),(\s*)/, (match, prefix, firstRule, suffix) => {
200
- // Check if we already inserted (avoid double insertion)
201
- if (match.includes('test: /\\.mjs$')) {
202
- return match;
203
- }
204
- // Insert mjs rule after first rule
205
- return `${prefix}${firstRule},\n ${mjsRule}${suffix}`;
206
- });
207
- }
208
-
209
- if (updated !== content) {
210
- context.updateFile(configPath, updated);
211
- additionsDebug('Added module rule for .mjs files in node_modules with resolve.fullySpecified: false');
212
- }
213
- }
214
-
215
- /**
216
- * Ensures plugin.json has grafanaDependency >= 10.2.0
217
- * Bundling @grafana/ui is only supported from Grafana 10.2.0 onwards
218
- */
219
- function ensureMinGrafanaVersion(context: Context): void {
220
- if (!context.doesFileExist(PLUGIN_JSON_PATH)) {
221
- additionsDebug(`${PLUGIN_JSON_PATH} not found, skipping version check`);
222
- return;
223
- }
224
-
225
- const pluginJsonRaw = context.getFile(PLUGIN_JSON_PATH);
226
- if (!pluginJsonRaw) {
227
- return;
228
- }
229
-
230
- try {
231
- const pluginJson = JSON.parse(pluginJsonRaw);
232
-
233
- if (!pluginJson.dependencies) {
234
- pluginJson.dependencies = {};
235
- }
236
-
237
- const currentGrafanaDep = pluginJson.dependencies.grafanaDependency || '>=9.0.0';
238
- const currentVersion = coerce(currentGrafanaDep.replace(/^[><=]+/, ''));
239
- const minVersion = coerce(MIN_GRAFANA_VERSION);
240
-
241
- if (!currentVersion || !minVersion || !gte(currentVersion, minVersion)) {
242
- const oldVersion = pluginJson.dependencies.grafanaDependency || 'not set';
243
- pluginJson.dependencies.grafanaDependency = `>=${MIN_GRAFANA_VERSION}`;
244
- context.updateFile(PLUGIN_JSON_PATH, JSON.stringify(pluginJson, null, 2));
245
- additionsDebug(
246
- `Updated grafanaDependency from "${oldVersion}" to ">=${MIN_GRAFANA_VERSION}" - bundling @grafana/ui requires Grafana ${MIN_GRAFANA_VERSION} or higher`
247
- );
248
- console.log(
249
- `\n⚠️ Updated grafanaDependency to ">=${MIN_GRAFANA_VERSION}" because bundling @grafana/ui is only supported from Grafana ${MIN_GRAFANA_VERSION} onwards.\n`
250
- );
251
- } else {
252
- additionsDebug(
253
- `grafanaDependency "${currentGrafanaDep}" already meets minimum requirement of ${MIN_GRAFANA_VERSION}`
254
- );
255
- }
256
- } catch (error) {
257
- additionsDebug(`Error updating ${PLUGIN_JSON_PATH}:`, error);
258
- }
259
- }
@@ -1,203 +0,0 @@
1
- import * as recast from 'recast';
2
- import * as typeScriptParser from 'recast/parsers/typescript.js';
3
-
4
- import type { Context } from './context.js';
5
- import { additionsDebug } from './utils.js';
6
-
7
- const WEBPACK_CONFIG_PATH = '.config/webpack/webpack.config.ts';
8
- const RSPACK_CONFIG_PATH = '.config/rspack/rspack.config.ts';
9
-
10
- /**
11
- * Gets the bundler config file path and content, preferring rspack over webpack
12
- * @returns Object with path and content, or null if no config file exists
13
- */
14
- export function getBundlerConfig(context: Context): { path: string; content: string } | null {
15
- const configPath = context.doesFileExist(RSPACK_CONFIG_PATH)
16
- ? RSPACK_CONFIG_PATH
17
- : context.doesFileExist(WEBPACK_CONFIG_PATH)
18
- ? WEBPACK_CONFIG_PATH
19
- : null;
20
-
21
- if (!configPath) {
22
- return null;
23
- }
24
-
25
- const content = context.getFile(configPath);
26
- if (!content) {
27
- return null;
28
- }
29
-
30
- return { path: configPath, content };
31
- }
32
-
33
- /**
34
- * Type for a function that modifies a resolve object expression
35
- * @param resolveObject - The AST node representing the resolve configuration
36
- * @returns true if changes were made, false otherwise
37
- */
38
- export type ResolveModifier = (resolveObject: recast.types.namedTypes.ObjectExpression) => boolean;
39
-
40
- /**
41
- * Type for a function that modifies a module rules array
42
- * @param moduleObject - The AST node representing the module configuration
43
- * @returns true if changes were made, false otherwise
44
- */
45
- export type ModuleRulesModifier = (moduleObject: recast.types.namedTypes.ObjectExpression) => boolean;
46
-
47
- /**
48
- * Updates the bundler's resolve and module configuration.
49
- *
50
- * This utility handles both webpack and rspack configurations, preferring rspack when both exist.
51
- *
52
- * @param context - The codemod context
53
- * @param resolveModifier - Optional function to modify the resolve configuration
54
- * @param moduleRulesModifier - Optional function to modify the module rules configuration
55
- */
56
- export function updateBundlerConfig(
57
- context: Context,
58
- resolveModifier?: ResolveModifier,
59
- moduleRulesModifier?: ModuleRulesModifier
60
- ): void {
61
- if (!resolveModifier && !moduleRulesModifier) {
62
- return;
63
- }
64
-
65
- // Try rspack config first (newer structure)
66
- if (context.doesFileExist(RSPACK_CONFIG_PATH)) {
67
- additionsDebug(`Found ${RSPACK_CONFIG_PATH}, updating bundler configuration...`);
68
- const rspackContent = context.getFile(RSPACK_CONFIG_PATH);
69
- if (rspackContent) {
70
- try {
71
- const ast = recast.parse(rspackContent, {
72
- parser: typeScriptParser,
73
- });
74
-
75
- let hasChanges = false;
76
-
77
- recast.visit(ast, {
78
- visitObjectExpression(path) {
79
- const { node } = path;
80
- const properties = node.properties;
81
-
82
- if (properties) {
83
- for (const prop of properties) {
84
- if (prop && (prop.type === 'Property' || prop.type === 'ObjectProperty')) {
85
- const key = 'key' in prop ? prop.key : null;
86
- const value = 'value' in prop ? prop.value : null;
87
-
88
- // Find the resolve property
89
- if (
90
- resolveModifier &&
91
- key &&
92
- key.type === 'Identifier' &&
93
- key.name === 'resolve' &&
94
- value &&
95
- value.type === 'ObjectExpression'
96
- ) {
97
- hasChanges = resolveModifier(value) || hasChanges;
98
- }
99
-
100
- // Find the module property
101
- if (
102
- moduleRulesModifier &&
103
- key &&
104
- key.type === 'Identifier' &&
105
- key.name === 'module' &&
106
- value &&
107
- value.type === 'ObjectExpression'
108
- ) {
109
- hasChanges = moduleRulesModifier(value) || hasChanges;
110
- }
111
- }
112
- }
113
- }
114
-
115
- return this.traverse(path);
116
- },
117
- });
118
-
119
- if (hasChanges) {
120
- const output = recast.print(ast, {
121
- tabWidth: 2,
122
- trailingComma: true,
123
- lineTerminator: '\n',
124
- });
125
- context.updateFile(RSPACK_CONFIG_PATH, output.code);
126
- additionsDebug(`Updated ${RSPACK_CONFIG_PATH}`);
127
- }
128
- } catch (error) {
129
- additionsDebug(`Error updating ${RSPACK_CONFIG_PATH}:`, error);
130
- }
131
- }
132
- return;
133
- }
134
-
135
- // Fall back to webpack config (legacy structure)
136
- if (context.doesFileExist(WEBPACK_CONFIG_PATH)) {
137
- additionsDebug(`Found ${WEBPACK_CONFIG_PATH}, updating bundler configuration...`);
138
- const webpackContent = context.getFile(WEBPACK_CONFIG_PATH);
139
- if (webpackContent) {
140
- try {
141
- const ast = recast.parse(webpackContent, {
142
- parser: typeScriptParser,
143
- });
144
-
145
- let hasChanges = false;
146
-
147
- recast.visit(ast, {
148
- visitObjectExpression(path) {
149
- const { node } = path;
150
- const properties = node.properties;
151
-
152
- if (properties) {
153
- for (const prop of properties) {
154
- if (prop && (prop.type === 'Property' || prop.type === 'ObjectProperty')) {
155
- const key = 'key' in prop ? prop.key : null;
156
- const value = 'value' in prop ? prop.value : null;
157
-
158
- // Find the resolve property
159
- if (
160
- resolveModifier &&
161
- key &&
162
- key.type === 'Identifier' &&
163
- key.name === 'resolve' &&
164
- value &&
165
- value.type === 'ObjectExpression'
166
- ) {
167
- hasChanges = resolveModifier(value) || hasChanges;
168
- }
169
-
170
- // Find the module property
171
- if (
172
- moduleRulesModifier &&
173
- key &&
174
- key.type === 'Identifier' &&
175
- key.name === 'module' &&
176
- value &&
177
- value.type === 'ObjectExpression'
178
- ) {
179
- hasChanges = moduleRulesModifier(value) || hasChanges;
180
- }
181
- }
182
- }
183
- }
184
-
185
- return this.traverse(path);
186
- },
187
- });
188
-
189
- if (hasChanges) {
190
- const output = recast.print(ast, {
191
- tabWidth: 2,
192
- trailingComma: true,
193
- lineTerminator: '\n',
194
- });
195
- context.updateFile(WEBPACK_CONFIG_PATH, output.code);
196
- additionsDebug(`Updated ${WEBPACK_CONFIG_PATH}`);
197
- }
198
- } catch (error) {
199
- additionsDebug(`Error updating ${WEBPACK_CONFIG_PATH}:`, error);
200
- }
201
- }
202
- }
203
- }
@@ -1,87 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import * as recast from 'recast';
3
-
4
- import { Context } from './context.js';
5
- import { updateExternalsArray, type ExternalsArrayModifier } from './utils.externals.js';
6
-
7
- describe('updateExternalsArray', () => {
8
- describe('new structure (.config/bundler/externals.ts)', () => {
9
- it('should update externals array in externals.ts', () => {
10
- const context = new Context('/virtual');
11
- context.addFile('.config/bundler/externals.ts', `export const externals = ['react', 'react-dom'];`);
12
-
13
- const modifier: ExternalsArrayModifier = (array) => {
14
- array.elements.push(recast.types.builders.literal('i18next'));
15
- return true;
16
- };
17
-
18
- const result = updateExternalsArray(context, modifier);
19
-
20
- expect(result).toBe(true);
21
- const content = context.getFile('.config/bundler/externals.ts') || '';
22
- expect(content).toMatch(/['"]i18next['"]/);
23
- expect(content).toContain("'react'");
24
- expect(content).toContain("'react-dom'");
25
- });
26
-
27
- it('should return false if no changes were made', () => {
28
- const context = new Context('/virtual');
29
- context.addFile('.config/bundler/externals.ts', `export const externals = ['react', 'react-dom'];`);
30
-
31
- const modifier: ExternalsArrayModifier = () => {
32
- return false; // No changes
33
- };
34
-
35
- const result = updateExternalsArray(context, modifier);
36
-
37
- expect(result).toBe(false);
38
- });
39
- });
40
-
41
- describe('legacy structure (.config/webpack/webpack.config.ts)', () => {
42
- it('should update externals array in webpack.config.ts when externals.ts does not exist', () => {
43
- const context = new Context('/virtual');
44
- context.addFile(
45
- '.config/webpack/webpack.config.ts',
46
- `import { Configuration } from 'webpack';
47
- export const config: Configuration = {
48
- externals: ['react', 'react-dom'],
49
- };`
50
- );
51
-
52
- const modifier: ExternalsArrayModifier = (array) => {
53
- array.elements.push(recast.types.builders.literal('i18next'));
54
- return true;
55
- };
56
-
57
- const result = updateExternalsArray(context, modifier);
58
-
59
- expect(result).toBe(true);
60
- const content = context.getFile('.config/webpack/webpack.config.ts') || '';
61
- expect(content).toMatch(/['"]i18next['"]/);
62
- expect(content).toContain("'react'");
63
- expect(content).toContain("'react-dom'");
64
- });
65
-
66
- it('should prefer externals.ts over webpack.config.ts', () => {
67
- const context = new Context('/virtual');
68
- context.addFile('.config/bundler/externals.ts', `export const externals = ['react'];`);
69
- context.addFile('.config/webpack/webpack.config.ts', `export const config = { externals: ['react-dom'] };`);
70
-
71
- const modifier: ExternalsArrayModifier = (array) => {
72
- array.elements.push(recast.types.builders.literal('i18next'));
73
- return true;
74
- };
75
-
76
- const result = updateExternalsArray(context, modifier);
77
-
78
- expect(result).toBe(true);
79
- // Should update externals.ts, not webpack.config.ts
80
- const externalsContent = context.getFile('.config/bundler/externals.ts') || '';
81
- expect(externalsContent).toMatch(/['"]i18next['"]/);
82
-
83
- const webpackContent = context.getFile('.config/webpack/webpack.config.ts') || '';
84
- expect(webpackContent).not.toMatch(/['"]i18next['"]/);
85
- });
86
- });
87
- });