@grafana/create-plugin 6.7.1-canary.2370.20798217827.0 → 6.8.0-canary.2356.20813241719.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codemods/additions/additions.js +3 -3
- package/dist/codemods/additions/scripts/bundle-grafana-ui/index.js +319 -0
- package/dist/codemods/utils.externals.js +116 -0
- package/dist/codemods/utils.js +2 -2
- package/dist/constants.js +0 -1
- package/dist/utils/utils.templates.js +1 -4
- package/package.json +2 -2
- package/src/codemods/additions/additions.ts +3 -3
- package/src/codemods/additions/scripts/bundle-grafana-ui/README.md +68 -0
- package/src/codemods/additions/scripts/bundle-grafana-ui/index.test.ts +511 -0
- package/src/codemods/additions/scripts/bundle-grafana-ui/index.ts +488 -0
- package/src/codemods/utils.externals.test.ts +87 -0
- package/src/codemods/utils.externals.ts +181 -0
- package/src/constants.ts +0 -1
- package/src/types.ts +0 -1
- package/src/utils/tests/utils.config.test.ts +3 -25
- package/src/utils/utils.config.ts +0 -1
- package/src/utils/utils.templates.ts +1 -10
- package/templates/common/.config/webpack/BuildModeWebpackPlugin.ts +1 -1
- package/templates/common/.cprc.json +4 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import * as v from 'valibot';
|
|
2
|
+
import * as recast from 'recast';
|
|
3
|
+
import * as typeScriptParser from 'recast/parsers/typescript.js';
|
|
4
|
+
import { coerce, gte } from 'semver';
|
|
5
|
+
|
|
6
|
+
import type { Context } from '../../../context.js';
|
|
7
|
+
import { additionsDebug } from '../../../utils.js';
|
|
8
|
+
import { updateExternalsArray, type ExternalsArrayModifier } from '../../../utils.externals.js';
|
|
9
|
+
|
|
10
|
+
const { builders } = recast.types;
|
|
11
|
+
|
|
12
|
+
export const schema = v.object({});
|
|
13
|
+
|
|
14
|
+
type BundleGrafanaUIOptions = v.InferOutput<typeof schema>;
|
|
15
|
+
|
|
16
|
+
const PLUGIN_JSON_PATH = 'src/plugin.json';
|
|
17
|
+
const MIN_GRAFANA_VERSION = '10.2.0';
|
|
18
|
+
const WEBPACK_CONFIG_PATH = '.config/webpack/webpack.config.ts';
|
|
19
|
+
const RSPACK_CONFIG_PATH = '.config/rspack/rspack.config.ts';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks if an AST node is a regex matching @grafana/ui
|
|
23
|
+
* The pattern in the AST is "^@grafana\/ui" (backslash-escaped forward slash)
|
|
24
|
+
*/
|
|
25
|
+
function isGrafanaUiRegex(element: recast.types.namedTypes.ASTNode): boolean {
|
|
26
|
+
// Handle RegExpLiteral (TypeScript parser)
|
|
27
|
+
if (element.type === 'RegExpLiteral') {
|
|
28
|
+
const regexNode = element as recast.types.namedTypes.RegExpLiteral;
|
|
29
|
+
return regexNode.pattern === '^@grafana\\/ui' && regexNode.flags === 'i';
|
|
30
|
+
}
|
|
31
|
+
// Handle Literal with regex property (other parsers)
|
|
32
|
+
if (element.type === 'Literal' && 'regex' in element && element.regex) {
|
|
33
|
+
const regex = element.regex as { pattern: string; flags: string };
|
|
34
|
+
return regex.pattern === '^@grafana\\/ui' && regex.flags === 'i';
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Checks if an AST node is a regex matching @grafana/data
|
|
41
|
+
* The pattern in the AST is "^@grafana\/data" (backslash-escaped forward slash)
|
|
42
|
+
*/
|
|
43
|
+
function isGrafanaDataRegex(element: recast.types.namedTypes.ASTNode): boolean {
|
|
44
|
+
// Handle RegExpLiteral (TypeScript parser)
|
|
45
|
+
if (element.type === 'RegExpLiteral') {
|
|
46
|
+
const regexNode = element as recast.types.namedTypes.RegExpLiteral;
|
|
47
|
+
return regexNode.pattern === '^@grafana\\/data' && regexNode.flags === 'i';
|
|
48
|
+
}
|
|
49
|
+
// Handle Literal with regex property (other parsers)
|
|
50
|
+
if (element.type === 'Literal' && 'regex' in element && element.regex) {
|
|
51
|
+
const regex = element.regex as { pattern: string; flags: string };
|
|
52
|
+
return regex.pattern === '^@grafana\\/data' && regex.flags === 'i';
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Removes /^@grafana\/ui/i regex from externals array and adds 'react-inlinesvg'
|
|
59
|
+
* @returns true if changes were made, false otherwise
|
|
60
|
+
*/
|
|
61
|
+
function modifyExternalsArray(externalsArray: recast.types.namedTypes.ArrayExpression): boolean {
|
|
62
|
+
let hasChanges = false;
|
|
63
|
+
let hasGrafanaUiExternal = false;
|
|
64
|
+
let hasReactInlineSvg = false;
|
|
65
|
+
|
|
66
|
+
// Check current state
|
|
67
|
+
for (const element of externalsArray.elements) {
|
|
68
|
+
if (!element) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for /^@grafana\/ui/i regex
|
|
73
|
+
if (isGrafanaUiRegex(element)) {
|
|
74
|
+
hasGrafanaUiExternal = true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for 'react-inlinesvg' string
|
|
78
|
+
if (
|
|
79
|
+
(element.type === 'Literal' || element.type === 'StringLiteral') &&
|
|
80
|
+
'value' in element &&
|
|
81
|
+
typeof element.value === 'string' &&
|
|
82
|
+
element.value === 'react-inlinesvg'
|
|
83
|
+
) {
|
|
84
|
+
hasReactInlineSvg = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Remove /^@grafana\/ui/i if present
|
|
89
|
+
if (hasGrafanaUiExternal) {
|
|
90
|
+
externalsArray.elements = externalsArray.elements.filter((element) => {
|
|
91
|
+
if (!element) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return !isGrafanaUiRegex(element);
|
|
95
|
+
});
|
|
96
|
+
hasChanges = true;
|
|
97
|
+
additionsDebug('Removed /^@grafana\\/ui/i from externals array');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Add 'react-inlinesvg' if not present
|
|
101
|
+
if (!hasReactInlineSvg) {
|
|
102
|
+
// Find the index of /^@grafana\/data/i to insert after it
|
|
103
|
+
let insertIndex = -1;
|
|
104
|
+
for (let i = 0; i < externalsArray.elements.length; i++) {
|
|
105
|
+
const element = externalsArray.elements[i];
|
|
106
|
+
if (element && isGrafanaDataRegex(element)) {
|
|
107
|
+
insertIndex = i + 1;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (insertIndex >= 0) {
|
|
113
|
+
externalsArray.elements.splice(insertIndex, 0, builders.literal('react-inlinesvg'));
|
|
114
|
+
} else {
|
|
115
|
+
// Fallback: append to end
|
|
116
|
+
externalsArray.elements.push(builders.literal('react-inlinesvg'));
|
|
117
|
+
}
|
|
118
|
+
hasChanges = true;
|
|
119
|
+
additionsDebug("Added 'react-inlinesvg' to externals array");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return hasChanges;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Creates a modifier function for updateExternalsArray that removes @grafana/ui
|
|
127
|
+
* and adds react-inlinesvg
|
|
128
|
+
*/
|
|
129
|
+
function createBundleGrafanaUIModifier(): ExternalsArrayModifier {
|
|
130
|
+
return (externalsArray: recast.types.namedTypes.ArrayExpression) => {
|
|
131
|
+
return modifyExternalsArray(externalsArray);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Updates the bundler's resolve configuration to handle ESM imports from @grafana/ui
|
|
137
|
+
* - Adds '.mjs' to resolve.extensions
|
|
138
|
+
* - Adds fullySpecified: false to allow extensionless imports in node_modules
|
|
139
|
+
*/
|
|
140
|
+
function updateBundlerResolveConfig(context: Context): void {
|
|
141
|
+
// Try rspack config first (newer structure)
|
|
142
|
+
if (context.doesFileExist(RSPACK_CONFIG_PATH)) {
|
|
143
|
+
additionsDebug(`Found ${RSPACK_CONFIG_PATH}, updating resolve configuration...`);
|
|
144
|
+
const rspackContent = context.getFile(RSPACK_CONFIG_PATH);
|
|
145
|
+
if (rspackContent) {
|
|
146
|
+
try {
|
|
147
|
+
const ast = recast.parse(rspackContent, {
|
|
148
|
+
parser: typeScriptParser,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
let hasChanges = false;
|
|
152
|
+
|
|
153
|
+
recast.visit(ast, {
|
|
154
|
+
visitObjectExpression(path) {
|
|
155
|
+
const { node } = path;
|
|
156
|
+
const properties = node.properties;
|
|
157
|
+
|
|
158
|
+
if (properties) {
|
|
159
|
+
for (const prop of properties) {
|
|
160
|
+
if (prop && (prop.type === 'Property' || prop.type === 'ObjectProperty')) {
|
|
161
|
+
const key = 'key' in prop ? prop.key : null;
|
|
162
|
+
const value = 'value' in prop ? prop.value : null;
|
|
163
|
+
|
|
164
|
+
// Find the resolve property
|
|
165
|
+
if (
|
|
166
|
+
key &&
|
|
167
|
+
key.type === 'Identifier' &&
|
|
168
|
+
key.name === 'resolve' &&
|
|
169
|
+
value &&
|
|
170
|
+
value.type === 'ObjectExpression'
|
|
171
|
+
) {
|
|
172
|
+
hasChanges = updateResolveObject(value) || hasChanges;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Find the module property to add .mjs rule
|
|
176
|
+
if (
|
|
177
|
+
key &&
|
|
178
|
+
key.type === 'Identifier' &&
|
|
179
|
+
key.name === 'module' &&
|
|
180
|
+
value &&
|
|
181
|
+
value.type === 'ObjectExpression'
|
|
182
|
+
) {
|
|
183
|
+
hasChanges = updateModuleRules(value) || hasChanges;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return this.traverse(path);
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (hasChanges) {
|
|
194
|
+
const output = recast.print(ast, {
|
|
195
|
+
tabWidth: 2,
|
|
196
|
+
trailingComma: true,
|
|
197
|
+
lineTerminator: '\n',
|
|
198
|
+
});
|
|
199
|
+
context.updateFile(RSPACK_CONFIG_PATH, output.code);
|
|
200
|
+
additionsDebug(`Updated ${RSPACK_CONFIG_PATH}`);
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
additionsDebug(`Error updating ${RSPACK_CONFIG_PATH}:`, error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Fall back to webpack config (legacy structure)
|
|
210
|
+
if (context.doesFileExist(WEBPACK_CONFIG_PATH)) {
|
|
211
|
+
additionsDebug(`Found ${WEBPACK_CONFIG_PATH}, updating resolve configuration...`);
|
|
212
|
+
const webpackContent = context.getFile(WEBPACK_CONFIG_PATH);
|
|
213
|
+
if (webpackContent) {
|
|
214
|
+
try {
|
|
215
|
+
const ast = recast.parse(webpackContent, {
|
|
216
|
+
parser: typeScriptParser,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
let hasChanges = false;
|
|
220
|
+
|
|
221
|
+
recast.visit(ast, {
|
|
222
|
+
visitObjectExpression(path) {
|
|
223
|
+
const { node } = path;
|
|
224
|
+
const properties = node.properties;
|
|
225
|
+
|
|
226
|
+
if (properties) {
|
|
227
|
+
for (const prop of properties) {
|
|
228
|
+
if (prop && (prop.type === 'Property' || prop.type === 'ObjectProperty')) {
|
|
229
|
+
const key = 'key' in prop ? prop.key : null;
|
|
230
|
+
const value = 'value' in prop ? prop.value : null;
|
|
231
|
+
|
|
232
|
+
// Find the resolve property
|
|
233
|
+
if (
|
|
234
|
+
key &&
|
|
235
|
+
key.type === 'Identifier' &&
|
|
236
|
+
key.name === 'resolve' &&
|
|
237
|
+
value &&
|
|
238
|
+
value.type === 'ObjectExpression'
|
|
239
|
+
) {
|
|
240
|
+
hasChanges = updateResolveObject(value) || hasChanges;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Find the module property to add .mjs rule
|
|
244
|
+
if (
|
|
245
|
+
key &&
|
|
246
|
+
key.type === 'Identifier' &&
|
|
247
|
+
key.name === 'module' &&
|
|
248
|
+
value &&
|
|
249
|
+
value.type === 'ObjectExpression'
|
|
250
|
+
) {
|
|
251
|
+
hasChanges = updateModuleRules(value) || hasChanges;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return this.traverse(path);
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (hasChanges) {
|
|
262
|
+
const output = recast.print(ast, {
|
|
263
|
+
tabWidth: 2,
|
|
264
|
+
trailingComma: true,
|
|
265
|
+
lineTerminator: '\n',
|
|
266
|
+
});
|
|
267
|
+
context.updateFile(WEBPACK_CONFIG_PATH, output.code);
|
|
268
|
+
additionsDebug(`Updated ${WEBPACK_CONFIG_PATH}`);
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
additionsDebug(`Error updating ${WEBPACK_CONFIG_PATH}:`, error);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Updates module rules to add a rule for .mjs files in node_modules with resolve.fullySpecified: false
|
|
279
|
+
*/
|
|
280
|
+
function updateModuleRules(moduleObject: recast.types.namedTypes.ObjectExpression): boolean {
|
|
281
|
+
if (!moduleObject.properties) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let hasChanges = false;
|
|
286
|
+
let hasMjsRule = false;
|
|
287
|
+
let rulesProperty: recast.types.namedTypes.Property | null = null;
|
|
288
|
+
|
|
289
|
+
// Find the rules property
|
|
290
|
+
for (const prop of moduleObject.properties) {
|
|
291
|
+
if (!prop || (prop.type !== 'Property' && prop.type !== 'ObjectProperty')) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const key = 'key' in prop ? prop.key : null;
|
|
296
|
+
const value = 'value' in prop ? prop.value : null;
|
|
297
|
+
|
|
298
|
+
if (key && key.type === 'Identifier' && key.name === 'rules' && value && value.type === 'ArrayExpression') {
|
|
299
|
+
rulesProperty = prop as recast.types.namedTypes.Property;
|
|
300
|
+
// Check if .mjs rule already exists
|
|
301
|
+
for (const element of value.elements) {
|
|
302
|
+
if (element && element.type === 'ObjectExpression' && element.properties) {
|
|
303
|
+
for (const ruleProp of element.properties) {
|
|
304
|
+
if (
|
|
305
|
+
ruleProp &&
|
|
306
|
+
(ruleProp.type === 'Property' || ruleProp.type === 'ObjectProperty') &&
|
|
307
|
+
'key' in ruleProp &&
|
|
308
|
+
ruleProp.key.type === 'Identifier' &&
|
|
309
|
+
ruleProp.key.name === 'test'
|
|
310
|
+
) {
|
|
311
|
+
const testValue = 'value' in ruleProp ? ruleProp.value : null;
|
|
312
|
+
if (testValue) {
|
|
313
|
+
// Check for RegExpLiteral with .mjs pattern
|
|
314
|
+
if (testValue.type === 'RegExpLiteral' && 'pattern' in testValue && testValue.pattern === '\\.mjs$') {
|
|
315
|
+
hasMjsRule = true;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
// Check for Literal with regex property
|
|
319
|
+
if (
|
|
320
|
+
testValue.type === 'Literal' &&
|
|
321
|
+
'regex' in testValue &&
|
|
322
|
+
testValue.regex &&
|
|
323
|
+
typeof testValue.regex === 'object' &&
|
|
324
|
+
'pattern' in testValue.regex &&
|
|
325
|
+
testValue.regex.pattern === '\\.mjs$'
|
|
326
|
+
) {
|
|
327
|
+
hasMjsRule = true;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
// Check for string literal containing .mjs
|
|
331
|
+
if (
|
|
332
|
+
testValue.type === 'Literal' &&
|
|
333
|
+
'value' in testValue &&
|
|
334
|
+
typeof testValue.value === 'string' &&
|
|
335
|
+
testValue.value.includes('.mjs')
|
|
336
|
+
) {
|
|
337
|
+
hasMjsRule = true;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (hasMjsRule) {
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Add .mjs rule if missing (insert at position 1, after imports-loader rule which must be first)
|
|
353
|
+
if (!hasMjsRule && rulesProperty && 'value' in rulesProperty) {
|
|
354
|
+
const rulesArray = rulesProperty.value as recast.types.namedTypes.ArrayExpression;
|
|
355
|
+
const mjsRule = builders.objectExpression([
|
|
356
|
+
builders.property('init', builders.identifier('test'), builders.literal(/\.mjs$/)),
|
|
357
|
+
builders.property('init', builders.identifier('include'), builders.literal(/node_modules/)),
|
|
358
|
+
builders.property(
|
|
359
|
+
'init',
|
|
360
|
+
builders.identifier('resolve'),
|
|
361
|
+
builders.objectExpression([
|
|
362
|
+
builders.property('init', builders.identifier('fullySpecified'), builders.literal(false)),
|
|
363
|
+
])
|
|
364
|
+
),
|
|
365
|
+
builders.property('init', builders.identifier('type'), builders.literal('javascript/auto')),
|
|
366
|
+
]);
|
|
367
|
+
// Insert at position 1 (second position) to keep imports-loader first
|
|
368
|
+
rulesArray.elements.splice(1, 0, mjsRule);
|
|
369
|
+
hasChanges = true;
|
|
370
|
+
additionsDebug('Added module rule for .mjs files in node_modules with resolve.fullySpecified: false');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return hasChanges;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Updates a resolve object expression to add .mjs extension
|
|
378
|
+
* Note: We don't set fullySpecified: false globally because .mjs files need
|
|
379
|
+
* rule-level configuration to override ESM's strict fully-specified import requirements
|
|
380
|
+
*/
|
|
381
|
+
function updateResolveObject(resolveObject: recast.types.namedTypes.ObjectExpression): boolean {
|
|
382
|
+
if (!resolveObject.properties) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let hasChanges = false;
|
|
387
|
+
let hasMjsExtension = false;
|
|
388
|
+
let extensionsProperty: recast.types.namedTypes.Property | null = null;
|
|
389
|
+
|
|
390
|
+
// Check current state
|
|
391
|
+
for (const prop of resolveObject.properties) {
|
|
392
|
+
if (!prop || (prop.type !== 'Property' && prop.type !== 'ObjectProperty')) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const key = 'key' in prop ? prop.key : null;
|
|
397
|
+
const value = 'value' in prop ? prop.value : null;
|
|
398
|
+
|
|
399
|
+
if (key && key.type === 'Identifier') {
|
|
400
|
+
if (key.name === 'extensions' && value && value.type === 'ArrayExpression') {
|
|
401
|
+
extensionsProperty = prop as recast.types.namedTypes.Property;
|
|
402
|
+
// Check if .mjs is already in the extensions array
|
|
403
|
+
for (const element of value.elements) {
|
|
404
|
+
if (
|
|
405
|
+
element &&
|
|
406
|
+
(element.type === 'Literal' || element.type === 'StringLiteral') &&
|
|
407
|
+
'value' in element &&
|
|
408
|
+
element.value === '.mjs'
|
|
409
|
+
) {
|
|
410
|
+
hasMjsExtension = true;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Add .mjs to extensions if missing
|
|
419
|
+
if (!hasMjsExtension && extensionsProperty && 'value' in extensionsProperty) {
|
|
420
|
+
const extensionsArray = extensionsProperty.value as recast.types.namedTypes.ArrayExpression;
|
|
421
|
+
extensionsArray.elements.push(builders.literal('.mjs'));
|
|
422
|
+
hasChanges = true;
|
|
423
|
+
additionsDebug("Added '.mjs' to resolve.extensions");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return hasChanges;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Ensures plugin.json has grafanaDependency >= 10.2.0
|
|
431
|
+
* Bundling @grafana/ui is only supported from Grafana 10.2.0 onwards
|
|
432
|
+
*/
|
|
433
|
+
function ensureMinGrafanaVersion(context: Context): void {
|
|
434
|
+
if (!context.doesFileExist(PLUGIN_JSON_PATH)) {
|
|
435
|
+
additionsDebug(`${PLUGIN_JSON_PATH} not found, skipping version check`);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const pluginJsonRaw = context.getFile(PLUGIN_JSON_PATH);
|
|
440
|
+
if (!pluginJsonRaw) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
const pluginJson = JSON.parse(pluginJsonRaw);
|
|
446
|
+
|
|
447
|
+
if (!pluginJson.dependencies) {
|
|
448
|
+
pluginJson.dependencies = {};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const currentGrafanaDep = pluginJson.dependencies.grafanaDependency || '>=9.0.0';
|
|
452
|
+
const currentVersion = coerce(currentGrafanaDep.replace(/^[><=]+/, ''));
|
|
453
|
+
const minVersion = coerce(MIN_GRAFANA_VERSION);
|
|
454
|
+
|
|
455
|
+
if (!currentVersion || !minVersion || !gte(currentVersion, minVersion)) {
|
|
456
|
+
const oldVersion = pluginJson.dependencies.grafanaDependency || 'not set';
|
|
457
|
+
pluginJson.dependencies.grafanaDependency = `>=${MIN_GRAFANA_VERSION}`;
|
|
458
|
+
context.updateFile(PLUGIN_JSON_PATH, JSON.stringify(pluginJson, null, 2));
|
|
459
|
+
additionsDebug(
|
|
460
|
+
`Updated grafanaDependency from "${oldVersion}" to ">=${MIN_GRAFANA_VERSION}" - bundling @grafana/ui requires Grafana ${MIN_GRAFANA_VERSION} or higher`
|
|
461
|
+
);
|
|
462
|
+
console.log(
|
|
463
|
+
`\n⚠️ Updated grafanaDependency to ">=${MIN_GRAFANA_VERSION}" because bundling @grafana/ui is only supported from Grafana ${MIN_GRAFANA_VERSION} onwards.\n`
|
|
464
|
+
);
|
|
465
|
+
} else {
|
|
466
|
+
additionsDebug(
|
|
467
|
+
`grafanaDependency "${currentGrafanaDep}" already meets minimum requirement of ${MIN_GRAFANA_VERSION}`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
} catch (error) {
|
|
471
|
+
additionsDebug(`Error updating ${PLUGIN_JSON_PATH}:`, error);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export default function bundleGrafanaUI(context: Context, _options: BundleGrafanaUIOptions): Context {
|
|
476
|
+
additionsDebug('Running bundle-grafana-ui addition...');
|
|
477
|
+
|
|
478
|
+
// Ensure minimum Grafana version requirement
|
|
479
|
+
ensureMinGrafanaVersion(context);
|
|
480
|
+
|
|
481
|
+
// Update externals array using the shared utility
|
|
482
|
+
updateExternalsArray(context, createBundleGrafanaUIModifier());
|
|
483
|
+
|
|
484
|
+
// Update bundler resolve configuration to handle ESM imports
|
|
485
|
+
updateBundlerResolveConfig(context);
|
|
486
|
+
|
|
487
|
+
return context;
|
|
488
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
});
|