@grafana/create-plugin 7.17.0 → 7.19.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 (44) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/codemods/additions/additions.js +7 -0
  3. package/dist/codemods/additions/prompts/rspack.md +120 -0
  4. package/dist/codemods/additions/scripts/docs.js +2 -2
  5. package/dist/codemods/additions/scripts/rspack.js +341 -0
  6. package/dist/codemods/agentic/prompts.js +1 -1
  7. package/dist/codemods/migrations/migrations.js +6 -0
  8. package/dist/codemods/migrations/scripts/020-rename-rspack-feature-flag.js +8 -0
  9. package/dist/codemods/utils.rspack.js +38 -0
  10. package/dist/commands/generate.command.js +1 -9
  11. package/dist/constants.js +0 -1
  12. package/dist/utils/utils.templates.js +4 -4
  13. package/package.json +1 -1
  14. package/src/codemods/additions/additions.test.ts +9 -0
  15. package/src/codemods/additions/additions.ts +7 -0
  16. package/src/codemods/additions/prompts/rspack.md +120 -0
  17. package/src/codemods/additions/scripts/docs.test.ts +4 -4
  18. package/src/codemods/additions/scripts/docs.ts +2 -2
  19. package/src/codemods/additions/scripts/rspack.test.ts +852 -0
  20. package/src/codemods/additions/scripts/rspack.ts +453 -0
  21. package/src/codemods/agentic/prompts.test.ts +11 -2
  22. package/src/codemods/agentic/prompts.ts +1 -1
  23. package/src/codemods/migrations/migrations.ts +7 -0
  24. package/src/codemods/migrations/scripts/020-rename-rspack-feature-flag.test.ts +94 -0
  25. package/src/codemods/migrations/scripts/020-rename-rspack-feature-flag.ts +8 -0
  26. package/src/codemods/utils.rspack.ts +58 -0
  27. package/src/codemods/utils.test.ts +5 -2
  28. package/src/commands/generate.command.ts +2 -10
  29. package/src/constants.ts +0 -1
  30. package/src/types.ts +1 -1
  31. package/src/utils/tests/utils.config.test.ts +10 -0
  32. package/src/utils/utils.config.ts +2 -1
  33. package/src/utils/utils.templates.ts +4 -4
  34. package/templates/common/.config/AGENTS/instructions.md +2 -2
  35. package/templates/common/.config/README.md +56 -0
  36. package/templates/common/.config/bundler/externals.ts +3 -3
  37. package/templates/common/.config/rspack/DiagnosticsRspackPlugin.ts +29 -0
  38. package/templates/common/.config/rspack/LicenseRspackPlugin.ts +43 -0
  39. package/templates/common/.config/rspack/LiveReloadRspackPlugin.ts +95 -0
  40. package/templates/common/.config/rspack/PluginCssRspackPlugin.ts +103 -0
  41. package/templates/common/.config/rspack/rspack.config.ts +79 -48
  42. package/templates/common/.cprc.json +1 -2
  43. package/templates/common/_package.json +22 -22
  44. package/templates/common/.config/rspack/liveReloadPlugin.ts +0 -133
@@ -140,16 +140,8 @@ function getActionsForTemplateFolder({
140
140
  files = files.filter((file) => path.basename(file) !== 'npmrc');
141
141
  }
142
142
 
143
- // filter out frontend bundler based on user choice
144
- files = files.filter((file) => {
145
- if (file.includes('webpack') && templateData.useExperimentalRspack) {
146
- return false;
147
- }
148
- if (file.includes('rspack') && !templateData.useExperimentalRspack) {
149
- return false;
150
- }
151
- return true;
152
- });
143
+ // new plugins always use webpack. the rspack templates are only rendered by `add rspack`
144
+ files = files.filter((file) => !file.includes('rspack'));
153
145
 
154
146
  function getFileExportPath(f: string) {
155
147
  return path.relative(folderPath, path.dirname(f));
package/src/constants.ts CHANGED
@@ -56,7 +56,6 @@ export const EXTRA_TEMPLATE_VARIABLES = {
56
56
 
57
57
  export const DEFAULT_FEATURE_FLAGS = {
58
58
  bundleGrafanaUI: false,
59
- useExperimentalRspack: false,
60
59
  useExperimentalUpdates: true,
61
60
  };
62
61
 
package/src/types.ts CHANGED
@@ -23,7 +23,7 @@ export type TemplateData = {
23
23
  version: string;
24
24
  bundleGrafanaUI: boolean;
25
25
  scenesVersion: string;
26
- useExperimentalRspack: boolean;
26
+ useRspack: boolean;
27
27
  pluginExecutable?: string;
28
28
  frontendBundler: 'webpack' | 'rspack';
29
29
  };
@@ -44,6 +44,16 @@ describe('getConfig', () => {
44
44
  });
45
45
  });
46
46
 
47
+ it('should not let cli args switch new plugins to rspack', async () => {
48
+ mocks.argv = {
49
+ 'feature-flags': 'useRspack',
50
+ };
51
+ const config = getConfig(tmpDir);
52
+
53
+ expect(config.features).toEqual(DEFAULT_FEATURE_FLAGS);
54
+ expect(config.features).not.toHaveProperty('useRspack');
55
+ });
56
+
47
57
  it('should override default feature flags via cli args', async () => {
48
58
  mocks.argv = {
49
59
  'feature-flags': 'bundleGrafanaUI',
@@ -11,7 +11,8 @@ import { EOL } from 'node:os';
11
11
 
12
12
  export type FeatureFlags = {
13
13
  bundleGrafanaUI?: boolean;
14
- useExperimentalRspack?: boolean;
14
+ // set by `add rspack` in .config/.cprc.json. not a default flag, so --feature-flags cannot set it
15
+ useRspack?: boolean;
15
16
  useExperimentalUpdates?: boolean;
16
17
  };
17
18
 
@@ -123,8 +123,8 @@ export function buildTemplateData({
123
123
  bundleGrafanaUI: features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI,
124
124
  scenesVersion: '^8.17.0',
125
125
  pluginExecutable: pluginJson.executable,
126
- useExperimentalRspack: Boolean(features.useExperimentalRspack),
127
- frontendBundler: features.useExperimentalRspack ? 'rspack' : 'webpack',
126
+ useRspack: Boolean(features.useRspack),
127
+ frontendBundler: features.useRspack ? 'rspack' : 'webpack',
128
128
  };
129
129
  }
130
130
 
@@ -140,7 +140,7 @@ export function getTemplateData(cliArgs?: GenerateCliArgs): TemplateData {
140
140
  const { features } = getConfig();
141
141
  const currentVersion = CURRENT_APP_VERSION;
142
142
  const bundleGrafanaUI = features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI;
143
- const frontendBundler = features.useExperimentalRspack ? 'rspack' : 'webpack';
143
+ const frontendBundler = features.useRspack ? 'rspack' : 'webpack';
144
144
 
145
145
  let templateData: TemplateData;
146
146
 
@@ -164,7 +164,7 @@ export function getTemplateData(cliArgs?: GenerateCliArgs): TemplateData {
164
164
  version: currentVersion,
165
165
  bundleGrafanaUI,
166
166
  scenesVersion: '^8.17.0',
167
- useExperimentalRspack: Boolean(features.useExperimentalRspack),
167
+ useRspack: Boolean(features.useRspack),
168
168
  frontendBundler,
169
169
  };
170
170
  // Updating or migrating a plugin
@@ -28,7 +28,7 @@ Your training data about the Grafana API is out of date. Use the official docume
28
28
  - **Do not change plugin ID or plugin type** in `plugin.json`.
29
29
  - Any modifications to `plugin.json` require a **restart of the Grafana server**. Remind the user of this.
30
30
  - Use `secureJsonData` for credentials and secrets; use `jsonData` only for non-sensitive configuration.
31
- - **You must use webpack** with the configuration provided in `.config/` for frontend builds.
31
+ - **You must use {{frontendBundler}}** with the configuration provided in `.config/` for frontend builds.
32
32
  - **You must use mage** with the build targets provided by the Grafana plugin Go SDK for backend builds.
33
- - To extend webpack, prettier, eslint or other tools, use the existing configuration as a base. Follow the guide: https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations.md
33
+ - To extend {{frontendBundler}}, prettier, eslint or other tools, use the existing configuration as a base. Follow the guide: https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations.md
34
34
  - Use **`@grafana/plugin-e2e`** for end-to-end testing. Read @./.config/AGENTS/e2e-testing.md before writing or modifying e2e tests.
@@ -99,6 +99,61 @@ Edit the `tsconfig.json` file in the project root in order to extend the TypeScr
99
99
 
100
100
  ---
101
101
 
102
+ {{#if useRspack}}
103
+ ### Extending the Rspack config
104
+
105
+ Follow these steps to extend the basic Rspack configuration that lives under `.config/`:
106
+
107
+ #### 1. Create a new Rspack configuration file
108
+
109
+ Create a new config file that is going to extend the basic one provided by Grafana.
110
+ It can live in the project root, e.g. `rspack.config.ts`.
111
+
112
+ #### 2. Merge the basic config provided by Grafana and your custom setup
113
+
114
+ We are going to use [`rspack-merge`](https://github.com/rstackjs/rspack-merge) for this.
115
+
116
+ ```typescript
117
+ // rspack.config.ts
118
+ import type { Configuration } from '@rspack/core';
119
+ import { merge } from 'rspack-merge';
120
+ import grafanaConfig, { type Env } from './.config/rspack/rspack.config.ts';
121
+
122
+ const config = async (env: Env): Promise<Configuration> => {
123
+ const baseConfig = await grafanaConfig(env);
124
+
125
+ return merge(baseConfig, {
126
+ // Add custom config here...
127
+ output: {
128
+ asyncChunks: true,
129
+ },
130
+ });
131
+ };
132
+
133
+ export default config;
134
+ ```
135
+
136
+ The Rspack CLI loads this file as native ESM, so use `import.meta.dirname` instead of `__dirname` and give relative imports a `.ts` extension.
137
+
138
+ #### 3. Update the `package.json` to use the new Rspack config
139
+
140
+ We need to update the `scripts` in the `package.json` to use the extended Rspack configuration.
141
+
142
+ **Update for `build`:**
143
+
144
+ ```diff
145
+ -"build": "rspack -c ./.config/rspack/rspack.config.ts --env production",
146
+ +"build": "rspack -c ./rspack.config.ts --env production",
147
+ ```
148
+
149
+ **Update for `dev`:**
150
+
151
+ ```diff
152
+ -"dev": "rspack -w -c ./.config/rspack/rspack.config.ts --env development",
153
+ +"dev": "rspack -w -c ./rspack.config.ts --env development",
154
+ ```
155
+
156
+ {{else}}
102
157
  ### Extending the Webpack config
103
158
 
104
159
  Follow these steps to extend the basic Webpack configuration that lives under `.config/`:
@@ -150,6 +205,7 @@ We need to update the `scripts` in the `package.json` to use the extended Webpac
150
205
  +"dev": "webpack -w -c ./webpack.config.ts --env development",
151
206
  ```
152
207
 
208
+ {{/if}}
153
209
  ### Configure grafana image to use when running docker
154
210
 
155
211
  By default, `grafana-enterprise` will be used as the docker image for all docker related commands. If you want to override this behavior, simply alter the `docker-compose.yaml` by adding the following build arg `grafana_image`.
@@ -1,7 +1,7 @@
1
- {{#unless useExperimentalRspack}}import type { Configuration, ExternalItemFunctionData } from 'webpack';{{/unless}}{{#if useExperimentalRspack}}
2
- import type { RspackOptions, ExternalItemFunctionData } from '@rspack/core';{{/if}}
1
+ {{#unless useRspack}}import type { Configuration, ExternalItemFunctionData } from 'webpack';{{/unless}}{{#if useRspack}}
2
+ import type { ExternalItem, ExternalItemFunctionData } from '@rspack/core';{{/if}}
3
3
 
4
- {{#unless useExperimentalRspack}} type ExternalsType = Configuration['externals'];{{/unless}}{{#if useExperimentalRspack}}type ExternalsType = RspackOptions['externals'];{{/if}}
4
+ {{#unless useRspack}} type ExternalsType = Configuration['externals'];{{/unless}}{{#if useRspack}}type ExternalsType = ExternalItem[];{{/if}}
5
5
 
6
6
  export const externals: ExternalsType = [
7
7
  // Required for dynamic publicPath resolution
@@ -0,0 +1,29 @@
1
+ import { type Compiler } from '@rspack/core';
2
+ import DiagnosticsWebpackPlugin from 'diagnostics-webpack-plugin';
3
+
4
+ type DiagnosticsOptions = ConstructorParameters<typeof DiagnosticsWebpackPlugin>[0];
5
+
6
+ // diagnostics-webpack-plugin validates its options through compiler.hooks.validate, which webpack added
7
+ // in 5.106 and rspack doesn't have yet. a no-op hook lets the plugin run, without its options check.
8
+ // once rspack adds the hook, delete this wrapper and use diagnostics-webpack-plugin directly
9
+ class DiagnosticsRspackPlugin {
10
+ plugin: DiagnosticsWebpackPlugin;
11
+
12
+ constructor(options: DiagnosticsOptions) {
13
+ this.plugin = new DiagnosticsWebpackPlugin(options);
14
+ }
15
+
16
+ apply(compiler: Compiler) {
17
+ const validate = { tap: () => {} };
18
+ const hooks = new Proxy(compiler.hooks, {
19
+ get: (target, prop) => (prop === 'validate' ? validate : Reflect.get(target, prop, target)),
20
+ });
21
+ const compilerWithValidate = new Proxy(compiler, {
22
+ get: (target, prop) => (prop === 'hooks' ? hooks : Reflect.get(target, prop, target)),
23
+ });
24
+ // @ts-expect-error -- the plugin is typed against webpack's Compiler
25
+ this.plugin.apply(compilerWithValidate);
26
+ }
27
+ }
28
+
29
+ export default DiagnosticsRspackPlugin;
@@ -0,0 +1,43 @@
1
+ import { Compilation, type Compiler } from '@rspack/core';
2
+
3
+ const PLUGIN_NAME = 'LicenseRspackPlugin';
4
+ const CHUNK_LICENSE_PATTERN = /\.LICENSE\.txt$/;
5
+
6
+ // This Rspack plugin works alongside SwcJsMinimizerRspackPlugin by taking all license file comments,
7
+ // deduping them and then writing them to a single file.
8
+ export class LicenseRspackPlugin {
9
+ apply(compiler: Compiler) {
10
+ compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
11
+ compilation.hooks.processAssets.tap(
12
+ {
13
+ name: PLUGIN_NAME,
14
+ // after the minimizer (PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE) has extracted the comments
15
+ stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE + 1,
16
+ },
17
+ (assets) => {
18
+ const chunkLicenseFiles = Object.keys(assets)
19
+ .filter((assetName) => CHUNK_LICENSE_PATTERN.test(assetName))
20
+ .sort();
21
+
22
+ if (chunkLicenseFiles.length === 0) {
23
+ return;
24
+ }
25
+
26
+ const comments = new Set<string>();
27
+ for (const assetName of chunkLicenseFiles) {
28
+ const content = assets[assetName].source().toString();
29
+ content
30
+ .split(/\n{2,}/)
31
+ .map((comment) => comment.trim())
32
+ .filter(Boolean)
33
+ .forEach((comment) => comments.add(comment));
34
+ compilation.deleteAsset(assetName);
35
+ }
36
+
37
+ const { RawSource } = compiler.rspack.sources;
38
+ compilation.emitAsset('LICENSE.txt', new RawSource([...comments].join('\n\n') + '\n'));
39
+ }
40
+ );
41
+ });
42
+ }
43
+ }
@@ -0,0 +1,95 @@
1
+ import { createServer } from 'http';
2
+ import { type Compiler } from '@rspack/core';
3
+ import { WebSocketServer } from 'ws';
4
+
5
+ interface RspackLiveReloadPluginOptions {
6
+ port?: number;
7
+ }
8
+
9
+ // Serves /livereload.js and tells connected browsers to reload after every rebuild. The scaffolded
10
+ // Grafana docker image adds <script src="http://localhost:35729/livereload.js"> to its index.html.
11
+ class RspackLiveReloadPlugin {
12
+ port: number;
13
+ httpServer: ReturnType<typeof createServer> | null = null;
14
+ server: WebSocketServer | null = null;
15
+ // each rebuild retries the port, so live reload starts by itself once the port is free again
16
+ hasWarnedDisabled = false;
17
+
18
+ constructor(options: RspackLiveReloadPluginOptions = {}) {
19
+ this.port = options.port ?? 35729;
20
+ }
21
+
22
+ apply(compiler: Compiler) {
23
+ compiler.hooks.afterEmit.tap('RspackLiveReloadPlugin', () => {
24
+ this._startServer();
25
+ this._notifyClients();
26
+ });
27
+ }
28
+
29
+ _startServer() {
30
+ if (this.server) {
31
+ return;
32
+ }
33
+
34
+ this.httpServer = createServer((req, res) => {
35
+ if (req.url === '/livereload.js') {
36
+ res.writeHead(200, { 'Content-Type': 'application/javascript' });
37
+ res.end(this._getLiveReloadScript());
38
+ } else {
39
+ res.writeHead(404);
40
+ res.end('Not Found');
41
+ }
42
+ });
43
+
44
+ this.server = new WebSocketServer({ server: this.httpServer });
45
+ // live reload is a convenience, so a port that is already taken must not take the watch build down.
46
+ // ws re-emits the http server's errors on the WebSocketServer, so both need a listener
47
+ const disableLiveReload = (error: NodeJS.ErrnoException) => {
48
+ if (!this.httpServer) {
49
+ return;
50
+ }
51
+ if (!this.hasWarnedDisabled) {
52
+ this.hasWarnedDisabled = true;
53
+ const reason = error.code === 'EADDRINUSE' ? `port ${this.port} is already in use` : error.message;
54
+ console.warn(`LiveReload disabled: ${reason}. The build keeps watching without reloading the browser.`);
55
+ }
56
+ this.server?.close();
57
+ this.server = null;
58
+ this.httpServer = null;
59
+ };
60
+ this.httpServer.on('error', disableLiveReload);
61
+ this.server.on('error', disableLiveReload);
62
+ this.httpServer.listen(this.port, () => {
63
+ console.log(`LiveReload server started on http://localhost:${this.port}`);
64
+ });
65
+ }
66
+
67
+ _notifyClients() {
68
+ if (!this.server) {
69
+ return;
70
+ }
71
+
72
+ this.server.clients.forEach((client) => {
73
+ if (client.readyState === WebSocket.OPEN) {
74
+ client.send(JSON.stringify({ action: 'reload' }));
75
+ }
76
+ });
77
+ }
78
+
79
+ _getLiveReloadScript() {
80
+ return `
81
+ (function() {
82
+ if (typeof WebSocket === 'undefined') return;
83
+ const ws = new WebSocket('ws://localhost:${this.port}');
84
+ ws.onmessage = function(event) {
85
+ const data = JSON.parse(event.data);
86
+ if (data.action === 'reload') {
87
+ window.location.reload();
88
+ }
89
+ };
90
+ })();
91
+ `;
92
+ }
93
+ }
94
+
95
+ export default RspackLiveReloadPlugin;
@@ -0,0 +1,103 @@
1
+ import { posix } from 'node:path';
2
+ import { Compilation, type Compiler } from '@rspack/core';
3
+
4
+ const PLUGIN_NAME = 'PluginCssRspackPlugin';
5
+ const URL_PATTERN = /url\(\s*(["']?)([^"')]+)\1\s*\)/g;
6
+ const CHARSET_PATTERN = /^@charset[^;]+;/;
7
+ const MODULE_CSS_DEPENDENCY = '"./module.css"';
8
+ // the AMD wrapper rspack writes before minification: define([deps], function(params){
9
+ const AMD_WRAPPER_PATTERN = /^define\(\[([^\]]*)\], function\(([^)]*)\)\{/;
10
+
11
+ interface PluginCssRspackPluginOptions {
12
+ pluginId: string;
13
+ publicPath: string;
14
+ }
15
+
16
+ // Prepares the CSS Grafana loads as module.css and the CSS of lazy chunks.
17
+ export class PluginCssRspackPlugin {
18
+ options: PluginCssRspackPluginOptions;
19
+
20
+ constructor(options: PluginCssRspackPluginOptions) {
21
+ this.options = options;
22
+ }
23
+
24
+ apply(compiler: Compiler) {
25
+ const { ConcatSource, ReplaceSource } = compiler.rspack.sources;
26
+ const { pluginId, publicPath } = this.options;
27
+ const layer = `plugin-${pluginId}`;
28
+
29
+ compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
30
+ compilation.hooks.processAssets.tap(
31
+ {
32
+ name: PLUGIN_NAME,
33
+ // before the CSS minimizer (PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE) so it minifies the result
34
+ stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,
35
+ },
36
+ (assets) => {
37
+ // a 404 on ./module.css fails the whole plugin load, so entries without CSS drop the dependency
38
+ for (const [entryName, entrypoint] of compilation.entrypoints) {
39
+ const files = entrypoint.getFiles().map((file) => file.split('?')[0]);
40
+ const jsFile = `${entryName}.js`;
41
+ if (files.includes(`${entryName}.css`) || !assets[jsFile]) {
42
+ continue;
43
+ }
44
+
45
+ const source = assets[jsFile];
46
+ const js = source.source().toString();
47
+ if (!js.includes(MODULE_CSS_DEPENDENCY)) {
48
+ continue;
49
+ }
50
+
51
+ const wrapper = js.match(AMD_WRAPPER_PATTERN);
52
+ const dependencies = wrapper?.[1].split(',') ?? [];
53
+ const params = wrapper?.[2].split(',').map((param) => param.trim()) ?? [];
54
+ const index = dependencies.indexOf(MODULE_CSS_DEPENDENCY);
55
+ if (!wrapper || index === -1 || dependencies.length !== params.length) {
56
+ throw new Error(`${PLUGIN_NAME}: could not remove ./module.css from the define() wrapper in ${jsFile}.`);
57
+ }
58
+
59
+ // the bundle still reads the external's parameter, so it becomes an undefined local
60
+ const [param] = params.splice(index, 1);
61
+ dependencies.splice(index, 1);
62
+ const updated = new ReplaceSource(source);
63
+ updated.replace(
64
+ 0,
65
+ wrapper[0].length - 1,
66
+ `define([${dependencies.join(',')}], function(${params.join(', ')}){var ${param};`
67
+ );
68
+ compilation.updateAsset(jsFile, updated);
69
+ }
70
+
71
+ for (const assetName of Object.keys(assets)) {
72
+ const fileName = assetName.split('?')[0];
73
+ if (!fileName.endsWith('.css')) {
74
+ continue;
75
+ }
76
+
77
+ const source = assets[assetName];
78
+ const css = source.source().toString();
79
+ const updated = new ReplaceSource(source);
80
+
81
+ // SystemJS and the browser resolve url() against the CSS file, not the page
82
+ for (const match of css.matchAll(URL_PATTERN)) {
83
+ const url = match[2];
84
+ if (!url.startsWith(publicPath)) {
85
+ continue;
86
+ }
87
+ const relativeUrl = posix.relative(posix.dirname(fileName), url.slice(publicPath.length));
88
+ const start = match.index + match[0].indexOf(url);
89
+ updated.replace(start, start + url.length - 1, relativeUrl);
90
+ }
91
+
92
+ // the layer keeps Grafana's unlayered styles winning at equal specificity. @charset must stay first
93
+ const charset = css.match(CHARSET_PATTERN)?.[0];
94
+ if (charset) {
95
+ updated.replace(0, charset.length - 1, '');
96
+ }
97
+ compilation.updateAsset(assetName, new ConcatSource(charset ?? '', `@layer ${layer}{`, updated, '}'));
98
+ }
99
+ }
100
+ );
101
+ });
102
+ }
103
+ }