@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.
- package/CHANGELOG.md +19 -0
- package/dist/codemods/additions/additions.js +7 -0
- package/dist/codemods/additions/prompts/rspack.md +120 -0
- package/dist/codemods/additions/scripts/docs.js +2 -2
- package/dist/codemods/additions/scripts/rspack.js +341 -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 +120 -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 +852 -0
- package/src/codemods/additions/scripts/rspack.ts +453 -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 +3 -3
- 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/PluginCssRspackPlugin.ts +103 -0
- package/templates/common/.config/rspack/rspack.config.ts +79 -48
- package/templates/common/.cprc.json +1 -2
- package/templates/common/_package.json +22 -22
- 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
|
-
//
|
|
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.
|
|
@@ -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
|
|
2
|
-
import type {
|
|
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
|
|
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
|
+
}
|