mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Build: Enable long term caching for frontend assets (#47625)
* build(webpack): move CopyUniconsPlugin into own file * chore(webpack): delete unused blobUrl and compile loaders * build(webpack): prefer contenthash over fullhash for longer caching * build(webpack): set optimization.moduleIds named only in dev * build(webpack): introduce HTMLWebpackCSSChunks so templates can access theme css by name * feat: inject css files with contenthash in html templates * revert(error-template): remove ContentDeliveryURL from CSS href * refactor(index-template): update grafanaBootData.themePaths * chore(webpack): add typescript annotations for CopyUniconsPlugin
This commit is contained in:
parent
20a83ba14f
commit
78bef7a26a
@ -10,7 +10,11 @@
|
||||
|
||||
<base href="[[.AppSubUrl]]/" />
|
||||
|
||||
<link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].<%= compilation.hash %>.css" />
|
||||
[[ if eq .Theme "light" ]]
|
||||
<link rel="stylesheet" href="public/build/<%= htmlWebpackPlugin.files.cssChunks.light %>" />
|
||||
[[ else ]]
|
||||
<link rel="stylesheet" href="public/build/<%= htmlWebpackPlugin.files.cssChunks.dark %>" />
|
||||
[[ end ]]
|
||||
|
||||
<link rel="icon" type="image/png" href="public/img/fav32.png" />
|
||||
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28" />
|
||||
|
@ -20,10 +20,12 @@
|
||||
<link rel="icon" type="image/png" href="[[.FavIcon]]" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="[[.AppleTouchIcon]]" />
|
||||
<link rel="mask-icon" href="[[.ContentDeliveryURL]]public/img/grafana_mask_icon.svg" color="#F05A28" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="[[.ContentDeliveryURL]]public/build/grafana.[[ .Theme ]].<%= compilation.hash %>.css"
|
||||
/>
|
||||
|
||||
[[ if eq .Theme "light" ]]
|
||||
<link rel="stylesheet" href="[[.ContentDeliveryURL]]public/build/<%= htmlWebpackPlugin.files.cssChunks.light %>" />
|
||||
[[ else ]]
|
||||
<link rel="stylesheet" href="[[.ContentDeliveryURL]]public/build/<%= htmlWebpackPlugin.files.cssChunks.dark %>" />
|
||||
[[ end ]]
|
||||
|
||||
<script nonce="[[.Nonce]]">
|
||||
performance.mark('frontend_boot_css_time_seconds');
|
||||
@ -251,8 +253,8 @@
|
||||
settings: [[.Settings]],
|
||||
navTree: [[.NavTree]],
|
||||
themePaths: {
|
||||
light: '[[.ContentDeliveryURL]]public/build/grafana.light.<%= compilation.hash %>.css',
|
||||
dark: '[[.ContentDeliveryURL]]public/build/grafana.dark.<%= compilation.hash %>.css'
|
||||
light: '[[.ContentDeliveryURL]]public/build/<%= htmlWebpackPlugin.files.cssChunks.light %>',
|
||||
dark: '[[.ContentDeliveryURL]]public/build/<%= htmlWebpackPlugin.files.cssChunks.dark %>'
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,8 +0,0 @@
|
||||
const loaderUtils = require('loader-utils');
|
||||
|
||||
module.exports = function blobUrl(source) {
|
||||
const { type } = loaderUtils.getOptions(this) || {};
|
||||
return `module.exports = URL.createObjectURL(new Blob([${JSON.stringify(source)}]${
|
||||
type ? `, { type: ${JSON.stringify(type)} }` : ''
|
||||
}));`;
|
||||
};
|
@ -1,100 +0,0 @@
|
||||
const loaderUtils = require('loader-utils');
|
||||
const ExternalsPlugin = require('webpack/lib/ExternalsPlugin');
|
||||
const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
|
||||
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
|
||||
const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
|
||||
const WebWorkerTemplatePlugin = require('webpack/lib/webworker/WebWorkerTemplatePlugin');
|
||||
|
||||
const COMPILATION_METADATA = Symbol('COMPILATION_METADATA');
|
||||
|
||||
module.exports.COMPILATION_METADATA = COMPILATION_METADATA;
|
||||
|
||||
module.exports.pitch = function pitch(remainingRequest) {
|
||||
const { target, plugins = [], output, emit } = loaderUtils.getOptions(this) || {};
|
||||
|
||||
if (target !== 'worker') {
|
||||
throw new Error(`Unsupported compile target: ${JSON.stringify(target)}`);
|
||||
}
|
||||
|
||||
this.cacheable(false);
|
||||
|
||||
const { filename, options = {} } = getOutputFilename(output, { target });
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const currentCompilation = this._compilation;
|
||||
|
||||
const outputFilename = loaderUtils.interpolateName(this, filename, {
|
||||
context: options.context || currentCompilation.options.context,
|
||||
regExp: options.regExp,
|
||||
});
|
||||
|
||||
const outputOptions = {
|
||||
filename: outputFilename,
|
||||
chunkFilename: `${outputFilename}.[id]`,
|
||||
namedChunkFilename: null,
|
||||
};
|
||||
|
||||
const compilerOptions = currentCompilation.compiler.options;
|
||||
const childCompiler = currentCompilation.createChildCompiler('worker', outputOptions, [
|
||||
// https://github.com/webpack/webpack/blob/master/lib/WebpackOptionsApply.js
|
||||
new WebWorkerTemplatePlugin(outputOptions),
|
||||
new LoaderTargetPlugin('webworker'),
|
||||
...(this.target === 'web' || this.target === 'webworker' ? [] : [new NodeTargetPlugin()]),
|
||||
|
||||
// https://github.com/webpack-contrib/worker-loader/issues/95#issuecomment-352856617
|
||||
...(compilerOptions.externals ? [new ExternalsPlugin(compilerOptions.externals)] : []),
|
||||
|
||||
...plugins,
|
||||
|
||||
new SingleEntryPlugin(this.context, `!!${remainingRequest}`, 'main'),
|
||||
]);
|
||||
|
||||
const subCache = `subcache ${__dirname} ${remainingRequest}`;
|
||||
|
||||
childCompiler.plugin('compilation', (compilation) => {
|
||||
if (!compilation.cache) {
|
||||
return;
|
||||
}
|
||||
if (!(subCache in compilation.cache)) {
|
||||
Object.assign(compilation.cache, { [subCache]: {} });
|
||||
}
|
||||
Object.assign(compilation, { cache: compilation.cache[subCache] });
|
||||
});
|
||||
|
||||
const callback = this.async();
|
||||
|
||||
childCompiler.runAsChild((error, entries, compilation) => {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
if (entries.length === 0) {
|
||||
return callback(null, null);
|
||||
}
|
||||
const mainFilename = entries[0].files[0];
|
||||
if (emit === false) {
|
||||
delete currentCompilation.assets[mainFilename];
|
||||
}
|
||||
callback(null, compilation.assets[mainFilename].source(), null, {
|
||||
[COMPILATION_METADATA]: entries[0].files,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function getOutputFilename(options, { target }) {
|
||||
if (!options) {
|
||||
return { filename: `[fullhash].${target}.js`, options: undefined };
|
||||
}
|
||||
if (typeof options === 'string') {
|
||||
return { filename: options, options: undefined };
|
||||
}
|
||||
if (typeof options === 'object') {
|
||||
return {
|
||||
filename: options.filename,
|
||||
options: {
|
||||
context: options.context,
|
||||
regExp: options.regExp,
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error(`Invalid compile output options: ${options}`);
|
||||
}
|
39
scripts/webpack/plugins/CopyUniconsPlugin.js
Normal file
39
scripts/webpack/plugins/CopyUniconsPlugin.js
Normal file
@ -0,0 +1,39 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
class CopyUniconsPlugin {
|
||||
/**
|
||||
* @param {import('webpack').Compiler} compiler
|
||||
*/
|
||||
apply(compiler) {
|
||||
compiler.hooks.afterEnvironment.tap(
|
||||
'CopyUniconsPlugin',
|
||||
/**
|
||||
* @param {import('webpack').Compilation} compilation
|
||||
*/
|
||||
() => {
|
||||
let destDir = path.resolve(__dirname, '../../../public/img/icons/unicons');
|
||||
|
||||
if (!fs.pathExistsSync(destDir)) {
|
||||
let srcDir = path.join(
|
||||
path.dirname(require.resolve('iconscout-unicons-tarball/package.json')),
|
||||
'unicons/svg/line'
|
||||
);
|
||||
fs.copySync(srcDir, destDir);
|
||||
}
|
||||
|
||||
let solidDestDir = path.resolve(__dirname, '../../../public/img/icons/solid');
|
||||
|
||||
if (!fs.pathExistsSync(solidDestDir)) {
|
||||
let srcDir = path.join(
|
||||
path.dirname(require.resolve('iconscout-unicons-tarball/package.json')),
|
||||
'unicons/svg/solid'
|
||||
);
|
||||
fs.copySync(srcDir, solidDestDir);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CopyUniconsPlugin;
|
42
scripts/webpack/plugins/HTMLWebpackCSSChunks.js
Normal file
42
scripts/webpack/plugins/HTMLWebpackCSSChunks.js
Normal file
@ -0,0 +1,42 @@
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
/*
|
||||
* This plugin returns the css associated with entrypoints. Those chunks can be found
|
||||
* in `htmlWebpackPlugin.files.cssChunks`.
|
||||
* The HTML Webpack plugin removed the chunks object in v5 in favour of an array however if we want
|
||||
* to do anything smart with hashing (e.g. [contenthash]) we need a map of { themeName: chunkNameWithHash }.
|
||||
*/
|
||||
class HTMLWebpackCSSChunks {
|
||||
/**
|
||||
* @param {import('webpack').Compiler} compiler
|
||||
*/
|
||||
apply(compiler) {
|
||||
compiler.hooks.compilation.tap(
|
||||
'HTMLWebpackCSSChunks',
|
||||
/**
|
||||
* @param {import('webpack').Compilation} compilation
|
||||
*/
|
||||
(compilation) => {
|
||||
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
|
||||
'HTMLWebpackCSSChunks',
|
||||
(data, cb) => {
|
||||
data.assets.cssChunks = {};
|
||||
|
||||
for (const entryPoint of compilation.entrypoints.values()) {
|
||||
for (const chunk of entryPoint.chunks) {
|
||||
const cssFile = [...chunk.files].find((file) => file.endsWith('.css'));
|
||||
if (cssFile !== undefined) {
|
||||
data.assets.cssChunks[chunk.name] = cssFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cb(null, data);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HTMLWebpackCSSChunks;
|
@ -1,36 +1,10 @@
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const CopyUniconsPlugin = require('./plugins/CopyUniconsPlugin');
|
||||
const CorsWorkerPlugin = require('./plugins/CorsWorkerPlugin');
|
||||
|
||||
class CopyUniconsPlugin {
|
||||
apply(compiler) {
|
||||
compiler.hooks.afterEnvironment.tap('CopyUniconsPlugin', () => {
|
||||
let destDir = path.resolve(__dirname, '../../public/img/icons/unicons');
|
||||
|
||||
if (!fs.pathExistsSync(destDir)) {
|
||||
let srcDir = path.join(
|
||||
path.dirname(require.resolve('iconscout-unicons-tarball/package.json')),
|
||||
'unicons/svg/line'
|
||||
);
|
||||
fs.copySync(srcDir, destDir);
|
||||
}
|
||||
|
||||
let solidDestDir = path.resolve(__dirname, '../../public/img/icons/solid');
|
||||
|
||||
if (!fs.pathExistsSync(solidDestDir)) {
|
||||
let srcDir = path.join(
|
||||
path.dirname(require.resolve('iconscout-unicons-tarball/package.json')),
|
||||
'unicons/svg/solid'
|
||||
);
|
||||
fs.copySync(srcDir, solidDestDir);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
target: 'web',
|
||||
entry: {
|
||||
@ -39,16 +13,14 @@ module.exports = {
|
||||
output: {
|
||||
clean: true,
|
||||
path: path.resolve(__dirname, '../../public/build'),
|
||||
filename: '[name].[fullhash].js',
|
||||
filename: '[name].[contenthash].js',
|
||||
// Keep publicPath relative for host.com/grafana/ deployments
|
||||
publicPath: 'public/build/',
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.es6', '.js', '.json', '.svg'],
|
||||
alias: {
|
||||
// storybook v6 bump caused the app to bundle multiple versions of react breaking hooks
|
||||
// make sure to resolve only from the project: https://github.com/facebook/react/issues/13991#issuecomment-435587809
|
||||
// some of data source pluginis use global Prism object to add the language definition
|
||||
// some of data source plugins use global Prism object to add the language definition
|
||||
// we want to have same Prism object in core and in grafana/ui
|
||||
prismjs: require.resolve('prismjs'),
|
||||
},
|
||||
@ -134,13 +106,12 @@ module.exports = {
|
||||
{
|
||||
test: /\.(svg|ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/,
|
||||
loader: 'file-loader',
|
||||
options: { name: 'static/img/[name].[hash:8].[ext]' },
|
||||
options: { name: 'static/img/[name].[contenthash:8].[ext]' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// https://webpack.js.org/plugins/split-chunks-plugin/#split-chunks-example-3
|
||||
optimization: {
|
||||
moduleIds: 'named',
|
||||
runtimeChunk: 'single',
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
|
@ -8,6 +8,7 @@ const path = require('path');
|
||||
const { DefinePlugin } = require('webpack');
|
||||
const { merge } = require('webpack-merge');
|
||||
|
||||
const HTMLWebpackCSSChunks = require('./plugins/HTMLWebpackCSSChunks');
|
||||
const common = require('./webpack.common.js');
|
||||
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
|
||||
@ -51,11 +52,11 @@ module.exports = (env = {}) =>
|
||||
// https://webpack.js.org/guides/build-performance/#output-without-path-info
|
||||
output: {
|
||||
pathinfo: false,
|
||||
filename: '[name].js',
|
||||
},
|
||||
|
||||
// https://webpack.js.org/guides/build-performance/#avoid-extra-optimization-steps
|
||||
optimization: {
|
||||
moduleIds: 'named',
|
||||
runtimeChunk: true,
|
||||
removeAvailableModules: false,
|
||||
removeEmptyChunks: false,
|
||||
@ -91,7 +92,7 @@ module.exports = (env = {}) =>
|
||||
extensions: ['.ts', '.tsx'],
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'grafana.[name].[fullhash].css',
|
||||
filename: 'grafana.[name].[contenthash].css',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: path.resolve(__dirname, '../../public/views/error.html'),
|
||||
@ -103,11 +104,11 @@ module.exports = (env = {}) =>
|
||||
new HtmlWebpackPlugin({
|
||||
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
||||
template: path.resolve(__dirname, '../../public/views/index-template.html'),
|
||||
hash: true,
|
||||
inject: false,
|
||||
chunksSortMode: 'none',
|
||||
excludeChunks: ['dark', 'light'],
|
||||
}),
|
||||
new HTMLWebpackCSSChunks(),
|
||||
new DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: JSON.stringify('development'),
|
||||
|
@ -7,6 +7,7 @@ const path = require('path');
|
||||
const { DefinePlugin } = require('webpack');
|
||||
const { merge } = require('webpack-merge');
|
||||
|
||||
const HTMLWebpackCSSChunks = require('./plugins/HTMLWebpackCSSChunks');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
module.exports = merge(common, {
|
||||
@ -77,16 +78,16 @@ module.exports = merge(common, {
|
||||
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'grafana.[name].[fullhash].css',
|
||||
filename: 'grafana.[name].[contenthash].css',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
||||
template: path.resolve(__dirname, '../../public/views/index-template.html'),
|
||||
hash: true,
|
||||
inject: false,
|
||||
chunksSortMode: 'none',
|
||||
excludeChunks: ['dark', 'light'],
|
||||
}),
|
||||
new HTMLWebpackCSSChunks(),
|
||||
new ReactRefreshWebpackPlugin(),
|
||||
new DefinePlugin({
|
||||
'process.env': {
|
||||
|
@ -7,6 +7,7 @@ const path = require('path');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const { merge } = require('webpack-merge');
|
||||
|
||||
const HTMLWebpackCSSChunks = require('./plugins/HTMLWebpackCSSChunks');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
module.exports = (env = {}) =>
|
||||
@ -63,7 +64,7 @@ module.exports = (env = {}) =>
|
||||
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'grafana.[name].[fullhash].css',
|
||||
filename: 'grafana.[name].[contenthash].css',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: path.resolve(__dirname, '../../public/views/error.html'),
|
||||
@ -79,6 +80,7 @@ module.exports = (env = {}) =>
|
||||
excludeChunks: ['manifest', 'dark', 'light'],
|
||||
chunksSortMode: 'none',
|
||||
}),
|
||||
new HTMLWebpackCSSChunks(),
|
||||
function () {
|
||||
this.hooks.done.tap('Done', function (stats) {
|
||||
if (stats.compilation.errors && stats.compilation.errors.length) {
|
||||
|
Loading…
Reference in New Issue
Block a user