@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.
- package/CHANGELOG.md +24 -0
- package/dist/codemods/additions/additions.js +7 -0
- package/dist/codemods/additions/prompts/rspack.md +118 -0
- package/dist/codemods/additions/scripts/docs.js +2 -2
- package/dist/codemods/additions/scripts/rspack.js +322 -0
- package/dist/codemods/agentic/prompts.js +1 -1
- package/dist/codemods/migrations/migrations.js +13 -1
- package/dist/codemods/migrations/scripts/019-remove-glob-dependency.js +93 -0
- package/dist/codemods/migrations/scripts/020-rename-rspack-feature-flag.js +8 -0
- package/dist/codemods/utils.ast.js +44 -1
- package/dist/codemods/utils.rspack.js +38 -0
- package/dist/commands/generate.command.js +1 -9
- package/dist/constants.js +0 -1
- package/dist/utils/utils.templates.js +4 -4
- package/package.json +1 -1
- package/src/codemods/additions/additions.test.ts +9 -0
- package/src/codemods/additions/additions.ts +7 -0
- package/src/codemods/additions/prompts/rspack.md +118 -0
- package/src/codemods/additions/scripts/docs.test.ts +4 -4
- package/src/codemods/additions/scripts/docs.ts +2 -2
- package/src/codemods/additions/scripts/rspack.test.ts +768 -0
- package/src/codemods/additions/scripts/rspack.ts +428 -0
- package/src/codemods/agentic/prompts.test.ts +11 -2
- package/src/codemods/agentic/prompts.ts +1 -1
- package/src/codemods/migrations/migrations.ts +14 -1
- package/src/codemods/migrations/scripts/019-remove-glob-dependency.test.ts +146 -0
- package/src/codemods/migrations/scripts/019-remove-glob-dependency.ts +121 -0
- package/src/codemods/migrations/scripts/020-rename-rspack-feature-flag.test.ts +94 -0
- package/src/codemods/migrations/scripts/020-rename-rspack-feature-flag.ts +8 -0
- package/src/codemods/utils.ast.test.ts +151 -0
- package/src/codemods/utils.ast.ts +60 -0
- package/src/codemods/utils.rspack.ts +58 -0
- package/src/codemods/utils.test.ts +5 -2
- package/src/commands/generate.command.ts +2 -10
- package/src/constants.ts +0 -1
- package/src/types.ts +1 -1
- package/src/utils/tests/utils.config.test.ts +10 -0
- package/src/utils/utils.config.ts +2 -1
- package/src/utils/utils.templates.ts +4 -4
- package/templates/common/.config/AGENTS/instructions.md +2 -2
- package/templates/common/.config/README.md +56 -0
- package/templates/common/.config/bundler/externals.ts +2 -2
- package/templates/common/.config/bundler/utils.ts +10 -9
- package/templates/common/.config/rspack/DiagnosticsRspackPlugin.ts +29 -0
- package/templates/common/.config/rspack/LicenseRspackPlugin.ts +43 -0
- package/templates/common/.config/rspack/LiveReloadRspackPlugin.ts +95 -0
- package/templates/common/.config/rspack/rspack.config.ts +41 -32
- package/templates/common/.cprc.json +1 -2
- package/templates/common/_package.json +19 -20
- 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(
|
|
240
|
-
|
|
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
|
-
//
|
|
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
package/src/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
127
|
-
frontendBundler: features.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|