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