grafana/scripts/webpack/webpack.prod.ts
Jack Westbrook 0a10a5d133
Build: Refactor Webpack config to TS (#121181)
* build(webpack): add tsconfig.json for Node strip-types compliance

* build(webpack): extract shared esbuild options to esbuild.ts

* build(webpack): add package.json to declare ESM module type

* build(webpack): convert sass.rule to TypeScript

* build(webpack): convert CorsWorkerPlugin to TypeScript

* build(webpack): convert FeatureFlaggedSriPlugin to TypeScript

* build(webpack): convert webpack.common to TypeScript, add theme entries

* build(webpack): convert webpack.dev to TypeScript, remove esbuild duplication

* build(webpack): convert webpack.prod to TypeScript, remove esbuild duplication

* build(webpack): fix TypeScript types in webpack.prod transform callback

* build(webpack): convert webpack.stats to TypeScript

* build(webpack): update scripts to use TypeScript webpack configs

* build(webpack): simplify env-util to use import.meta.dirname directly

* build(webpack): tidy up plugins

* build(webpack): move rules for ts and sass into single module

* build(webpack): consolidate shared config into common, move splitChunks to prod

- Move MiniCssExtractPlugin, esbuildRule and sassRule into common so both
  dev and prod configs share them without duplication
- Move splitChunks/runtimeChunk optimisation to webpack.prod only (not
  needed in dev)
- Use require() for SubresourceIntegrityPlugin to work around broken ESM
  build (waysact/webpack-subresource-integrity#236)
- Refactor conditional plugin logic in dev from ternary to if-blocks

* build(webpack): remove dead import and clarify webpack destructure pattern

- Remove unused MiniCssExtractPlugin import from webpack.prod (moved to common)
- Add comment explaining why DefinePlugin/EnvironmentPlugin are destructured
  from the default webpack import rather than using named ESM imports

* style(webpack): reorder consts

* chore(env-util): fix up env-util and webpack configs so tests continue to run

* refactor(env-util): accept grafanaRoot param instead of relying on __dirname

Removes the global.__dirname mutation hack in webpack.common.ts by making
the grafana root path an explicit argument to getEnvConfig. Each caller
resolves its own root and passes it in, removing the implicit path-depth
contract and the CJS/ESM compatibility workaround.

* build(webpack): remove unused angular chunk group
2026-04-28 12:45:31 +02:00

139 lines
4.6 KiB
TypeScript

import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import { EsbuildPlugin } from 'esbuild-loader';
import { createRequire } from 'node:module';
import path from 'node:path';
import webpack, { type Configuration } from 'webpack';
import WebpackAssetsManifest from 'webpack-assets-manifest';
import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
import { merge } from 'webpack-merge';
import FeatureFlaggedSRIPlugin from './plugins/FeatureFlaggedSriPlugin.ts';
import { esbuildOptions } from './rules.ts';
import common, { type Env } from './webpack.common.ts';
// SRI plugin has broken esm builds so we use require.
// https://github.com/waysact/webpack-subresource-integrity/issues/236
const require = createRequire(import.meta.url);
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');
export default (env: Env = {}) => {
const prodConfig: Configuration = {
mode: 'production',
devtool: process.env.NO_SOURCEMAP === '1' ? false : 'source-map',
output: {
crossOriginLoading: 'anonymous',
},
optimization: {
nodeEnv: 'production',
minimize: Number(env.noMinify) !== 1,
minimizer: [new EsbuildPlugin(esbuildOptions), new CssMinimizerPlugin()],
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
minChunks: 1,
cacheGroups: {
moment: {
test: /[\\/]node_modules[\\/]moment[\\/].*[jt]sx?$/,
chunks: 'initial',
priority: 20,
enforce: true,
},
defaultVendors: {
test: /[\\/]node_modules[\\/].*[jt]sx?$/,
chunks: 'initial',
priority: -10,
reuseExistingChunk: true,
enforce: true,
},
default: {
priority: -20,
chunks: 'all',
test: /.*[jt]sx?$/,
reuseExistingChunk: true,
},
},
},
},
// enable persistent cache for faster builds
cache:
Number(env.noMinify) === 1
? false
: {
type: 'filesystem',
name: 'grafana-default-production',
buildDependencies: {
config: [import.meta.filename],
},
},
plugins: [
new SubresourceIntegrityPlugin(),
new FeatureFlaggedSRIPlugin(),
/**
* I know we have two manifest plugins here.
* WebpackManifestPlugin was only used in prod before and does not support integrity hashes
*/
new WebpackAssetsManifest({
entrypoints: true,
integrity: true,
integrityHashes: ['sha384', 'sha512'],
publicPath: true,
// This transform filters down the assets to only include the ones that are part of the entrypoints
// this is all that the backend requires.
transform(assets, manifest) {
const entrypointsKey = manifest.options.entrypointsKey;
if (typeof entrypointsKey !== 'string') {
return assets;
}
const entrypointsValue = assets[entrypointsKey];
const entrypointAssets = isEntrypointsMap(entrypointsValue)
? Object.values(entrypointsValue).flatMap((entry) => [
...(entry.assets.js || []),
...(entry.assets.css || []),
])
: [];
const filteredAssets = Object.entries(assets).filter(([assetFileName]) => {
const asset = assets[assetFileName];
return isAssetEntry(asset) && entrypointAssets.includes(asset.src);
});
const result = Object.fromEntries(filteredAssets);
result[entrypointsKey] = entrypointsValue;
return result;
},
output: env.react19 ? 'assets-manifest-react19.json' : 'assets-manifest.json',
}),
new WebpackManifestPlugin({
fileName: path.join(process.cwd(), env.react19 ? 'manifest-react19.json' : 'manifest.json'),
filter: (file) => !file.name.endsWith('.map'),
}),
function () {
this.hooks.done.tap('Done', function (stats) {
if (stats.compilation.errors && stats.compilation.errors.length) {
console.log(stats.compilation.errors);
process.exit(1);
}
});
},
],
};
return merge(common(env), prodConfig);
};
interface EntrypointAssets {
assets: { js?: string[]; css?: string[] };
}
function isEntrypointsMap(value: unknown): value is Record<string, EntrypointAssets> {
return typeof value === 'object' && value !== null;
}
function isAssetEntry(value: unknown): value is { src: string } {
return typeof value === 'object' && value !== null && 'src' in value;
}