The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/packages/grafana-toolkit/src/config/webpack.plugin.config.ts

289 lines
8.9 KiB

import * as webpack from 'webpack';
import { getStyleLoaders, getStylesheetEntries, getFileLoaders } from './webpack/loaders';
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
const ReplaceInFileWebpackPlugin = require('replace-in-file-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const util = require('util');
const readdirPromise = util.promisify(fs.readdir);
const accessPromise = util.promisify(fs.access);
export interface WebpackConfigurationOptions {
watch?: boolean;
production?: boolean;
preserveConsole?: boolean;
}
type WebpackConfigurationGetter = (options: WebpackConfigurationOptions) => Promise<webpack.Configuration>;
export type CustomWebpackConfigurationGetter = (
originalConfig: webpack.Configuration,
options: WebpackConfigurationOptions
) => webpack.Configuration;
export const findModuleFiles = async (base: string, files?: string[], result?: string[]) => {
files = files || (await readdirPromise(base));
result = result || [];
if (files) {
await Promise.all(
files.map(async (file) => {
const newbase = path.join(base, file);
if (fs.statSync(newbase).isDirectory()) {
result = await findModuleFiles(newbase, await readdirPromise(newbase), result);
} else {
const filename = path.basename(file);
if (/^module.(t|j)sx?$/.exec(filename)) {
// @ts-ignore
result.push(newbase);
}
}
})
);
}
return result;
};
const getModuleFiles = () => {
return findModuleFiles(path.resolve(process.cwd(), 'src'));
};
const getManualChunk = (id: string) => {
if (id.endsWith('module.ts') || id.endsWith('module.js') || id.endsWith('module.tsx')) {
const idx = id.lastIndexOf(path.sep + 'src' + path.sep);
if (idx > 0) {
const name = id.substring(idx + 5, id.lastIndexOf('.'));
return {
name,
module: id,
};
}
}
return null;
};
const getEntries = async () => {
const entries: { [key: string]: string } = {};
const modules = await getModuleFiles();
modules.forEach((modFile) => {
const mod = getManualChunk(modFile);
// @ts-ignore
entries[mod.name] = mod.module;
});
return {
...entries,
...getStylesheetEntries(),
};
};
const getCommonPlugins = (options: WebpackConfigurationOptions) => {
const hasREADME = fs.existsSync(path.resolve(process.cwd(), 'src', 'README.md'));
const packageJson = require(path.resolve(process.cwd(), 'package.json'));
return [
new MiniCssExtractPlugin({
// both options are optional
filename: 'styles/[name].css',
}),
new CopyWebpackPlugin({
patterns: [
// If src/README.md exists use it; otherwise the root README
{ from: hasREADME ? 'README.md' : '../README.md', to: '.', force: true, priority: 1, noErrorOnMissing: true },
{ from: 'plugin.json', to: '.', noErrorOnMissing: true },
{ from: '**/README.md', to: '[path]README.md', priority: 0, noErrorOnMissing: true },
{ from: '../LICENSE', to: '.', noErrorOnMissing: true },
{ from: '../CHANGELOG.md', to: '.', force: true, noErrorOnMissing: true },
{ from: '**/*.{json,svg,png,html}', to: '.', noErrorOnMissing: true },
{ from: 'img/**/*', to: '.', noErrorOnMissing: true },
{ from: 'libs/**/*', to: '.', noErrorOnMissing: true },
{ from: 'static/**/*', to: '.', noErrorOnMissing: true },
],
}),
new ReplaceInFileWebpackPlugin([
{
dir: 'dist',
files: ['plugin.json', 'README.md'],
rules: [
{
search: '%VERSION%',
replace: packageJson.version,
},
{
search: '%TODAY%',
replace: new Date().toISOString().substring(0, 10),
},
],
},
]),
new ForkTsCheckerWebpackPlugin({
typescript: { configFile: path.join(process.cwd(), 'tsconfig.json') },
issue: {
include: [{ file: '**/*.{ts,tsx}' }],
},
}),
];
};
const getBaseWebpackConfig: WebpackConfigurationGetter = async (options) => {
const plugins = getCommonPlugins(options);
const optimization: { [key: string]: any } = {};
if (options.production) {
const compressOptions = { drop_console: !options.preserveConsole, drop_debugger: true };
optimization.minimizer = [
new TerserPlugin({ terserOptions: { compress: compressOptions } }),
new CssMinimizerPlugin(),
];
optimization.chunkIds = 'total-size';
optimization.moduleIds = 'size';
} else if (options.watch) {
plugins.push(new HtmlWebpackPlugin());
}
return {
mode: options.production ? 'production' : 'development',
target: 'web',
context: path.join(process.cwd(), 'src'),
devtool: 'source-map',
entry: await getEntries(),
output: {
filename: '[name].js',
path: path.join(process.cwd(), 'dist'),
libraryTarget: 'amd',
publicPath: '/',
},
performance: { hints: false },
externals: [
'lodash',
'jquery',
'moment',
'slate',
'emotion',
'@emotion/react',
'@emotion/css',
'prismjs',
'slate-plain-serializer',
'slate-react',
'react',
'react-dom',
'react-redux',
'redux',
'rxjs',
'react-router',
'react-router-dom',
'd3',
'angular',
'@grafana/ui',
'@grafana/runtime',
'@grafana/data',
({ request }, callback) => {
const prefix = 'grafana/';
if (request?.indexOf(prefix) === 0) {
return callback(undefined, request.slice(prefix.length));
}
callback();
},
],
plugins,
resolve: {
extensions: ['.ts', '.tsx', '.js'],
modules: [path.resolve(process.cwd(), 'src'), 'node_modules'],
fallback: {
buffer: false,
fs: false,
stream: false,
http: false,
https: false,
string_decoder: false,
os: false,
timers: false,
},
},
module: {
rules: [
{
test: /\.[tj]sx?$/,
use: {
loader: require.resolve('babel-loader'),
options: {
cacheDirectory: true,
cacheCompression: false,
presets: [
[require.resolve('@babel/preset-env'), { modules: false }],
[
require.resolve('@babel/preset-typescript'),
{
allowNamespaces: true,
allowDeclareFields: true,
},
],
[require.resolve('@babel/preset-react')],
],
plugins: [
[
require.resolve('@babel/plugin-transform-typescript'),
{
allowNamespaces: true,
allowDeclareFields: true,
},
],
require.resolve('@babel/plugin-proposal-class-properties'),
[require.resolve('@babel/plugin-proposal-object-rest-spread'), { loose: true }],
require.resolve('@babel/plugin-transform-react-constant-elements'),
require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'),
require.resolve('@babel/plugin-proposal-optional-chaining'),
require.resolve('@babel/plugin-syntax-dynamic-import'),
require.resolve('babel-plugin-angularjs-annotate'),
],
},
},
exclude: /node_modules/,
},
...getStyleLoaders(),
{
test: /\.html$/,
exclude: [/node_modules/],
use: {
loader: require.resolve('html-loader'),
},
},
...getFileLoaders(),
],
},
optimization,
};
};
export const loadWebpackConfig: WebpackConfigurationGetter = async (options) => {
const baseConfig = await getBaseWebpackConfig(options);
const customWebpackPath = path.resolve(process.cwd(), 'webpack.config.js');
try {
await accessPromise(customWebpackPath);
const customConfig = require(customWebpackPath);
const configGetter = customConfig.getWebpackConfig || customConfig;
if (typeof configGetter !== 'function') {
throw Error(
'Custom webpack config needs to export a function implementing CustomWebpackConfigurationGetter. Function needs to be ' +
'module export or named "getWebpackConfig"'
);
}
return (configGetter as CustomWebpackConfigurationGetter)(baseConfig, options);
} catch (err: any) {
if (err.code === 'ENOENT') {
return baseConfig;
}
throw err;
}
};