Commit f1845d80 by Sebastian Markgraf Committed by Steven Vachon

grafana/toolkit: Add option to override webpack config (#20872)

* Toolkit: Add possibility to add custom webpack config

* Toolkit: Refactor webpack to utilize async-await

* Toolkit: Rename config file and allow named export
parent 34aaa3d1
......@@ -127,6 +127,27 @@ Currently we support following Jest configuration properties:
- [`snapshotSerializers`](https://jest-bot.github.io/jest/docs/configuration.html#snapshotserializers-array-string)
- [`moduleNameMapper`](https://jestjs.io/docs/en/configuration#modulenamemapper-object-string-string)
### How can I customize Webpack rules or plugins?
You can provide your own webpack configuration.
Provide a function implementing `CustomWebpackConfigurationGetter` in a file named `webpack.config.ts`.
You can import the correct interface and Options from `@grafana/toolkit/src/config`.
Example
``` ts
import { CustomWebpackConfigurationGetter } from '@grafana/toolkit/src/config'
import CustomPlugin from 'custom-plugin';
const getWebpackConfig: CustomWebpackConfigurationGetter = (defaultConfig, options) => {
console.log('Custom config');
defaultConfig.plugins.push(new CustomPlugin())
return defaultConfig;
}
export = getWebpackConfig;
```
### How can I style my plugin?
We support pure CSS, SASS, and CSS-in-JS approach (via [Emotion](https://emotion.sh/)).
......
import webpack = require('webpack');
import formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
import clearConsole = require('react-dev-utils/clearConsole');
import { getWebpackConfig } from '../../../config/webpack.plugin.config';
import { loadWebpackConfig } from '../../../config/webpack.plugin.config';
export interface PluginBundleOptions {
watch: boolean;
......@@ -12,7 +12,7 @@ export interface PluginBundleOptions {
// export const bundlePlugin = useSpinner<PluginBundleOptions>('Bundle plugin', ({ watch }) => {
export const bundlePlugin = async ({ watch, production }: PluginBundleOptions) => {
const compiler = webpack(
getWebpackConfig({
await loadWebpackConfig({
watch,
production,
})
......
export { CustomWebpackConfigurationGetter, WebpackConfigurationOptions } from './webpack.plugin.config';
import { CustomWebpackConfigurationGetter } from '../../../webpack.plugin.config';
import _ from 'lodash';
const overrideWebpackConfig: CustomWebpackConfigurationGetter = (originalConfig, options) => {
const config = _.cloneDeep(originalConfig);
config.name = 'customConfig';
return config;
};
export = overrideWebpackConfig;
import { CustomWebpackConfigurationGetter } from '../../../webpack.plugin.config';
import _ from 'lodash';
export const getWebpackConfig: CustomWebpackConfigurationGetter = (originalConfig, options) => {
const config = _.cloneDeep(originalConfig);
config.name = 'customConfig';
return config;
};
/* WRONG CONFIG ON PURPOSE - DO NOT COPY THIS */
const config = {
name: 'test',
};
export = config;
import { findModuleFiles } from './webpack.plugin.config';
const fs = require('fs');
import { findModuleFiles, loadWebpackConfig } from './webpack.plugin.config';
import fs from 'fs';
import * as webpackConfig from './webpack.plugin.config';
jest.mock('fs');
jest.mock('./webpack/loaders', () => ({
getFileLoaders: () => [],
getStylesheetEntries: () => [],
getStyleLoaders: () => [],
}));
const modulePathsMock = [
'some/path/module.ts',
......@@ -15,16 +20,56 @@ const modulePathsMock = [
describe('Plugin webpack config', () => {
describe('findModuleTs', () => {
beforeAll(() => {
fs.statSync.mockReturnValue({
jest.spyOn(fs, 'statSync').mockReturnValue({
isDirectory: () => false,
});
} as any);
});
it('finds module.ts and module.tsx files', () => {
const moduleFiles = findModuleFiles('/', modulePathsMock);
afterAll(() => {
jest.restoreAllMocks();
});
it('finds module.ts and module.tsx files', async () => {
const moduleFiles = await findModuleFiles('/', modulePathsMock);
expect(moduleFiles.length).toBe(2);
// normalize windows path - \\ -> /
expect(moduleFiles.map(p => p.replace(/\\/g, '/'))).toEqual(['/some/path/module.ts', '/some/path/module.tsx']);
});
});
describe('loadWebpackConfig', () => {
beforeAll(() => {
jest.spyOn(webpackConfig, 'findModuleFiles').mockReturnValue(new Promise((res, _) => res([])));
});
afterAll(() => {
jest.restoreAllMocks();
});
it('uses default config if no override exists', async () => {
const spy = jest.spyOn(process, 'cwd');
spy.mockReturnValue(`${__dirname}/mocks/webpack/noOverride/`);
await loadWebpackConfig({});
});
it('calls customConfig if it exists', async () => {
const spy = jest.spyOn(process, 'cwd');
spy.mockReturnValue(`${__dirname}/mocks/webpack/overrides/`);
const config = await loadWebpackConfig({});
expect(config.name).toBe('customConfig');
});
it('loads export named getWebpackConfiguration', async () => {
const spy = jest.spyOn(process, 'cwd');
spy.mockReturnValue(`${__dirname}/mocks/webpack/overridesNamedExport/`);
const config = await loadWebpackConfig({});
expect(config.name).toBe('customConfig');
});
it('throws an error if module does not export function', async () => {
const spy = jest.spyOn(process, 'cwd');
spy.mockReturnValue(`${__dirname}/mocks/webpack/unsupportedOverride/`);
await expect(loadWebpackConfig({})).rejects.toThrowError();
});
});
});
const fs = require('fs');
const util = require('util');
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ReplaceInFileWebpackPlugin = require('replace-in-file-webpack-plugin');
......@@ -7,24 +8,31 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const readdirPromise = util.promisify(fs.readdir);
const accessPromise = util.promisify(fs.access);
import * as webpack from 'webpack';
import { getStyleLoaders, getStylesheetEntries, getFileLoaders } from './webpack/loaders';
interface WebpackConfigurationOptions {
export interface WebpackConfigurationOptions {
watch?: boolean;
production?: boolean;
}
type WebpackConfigurationGetter = (options: WebpackConfigurationOptions) => webpack.Configuration;
type WebpackConfigurationGetter = (options: WebpackConfigurationOptions) => Promise<webpack.Configuration>;
export type CustomWebpackConfigurationGetter = (
originalConfig: webpack.Configuration,
options: WebpackConfigurationOptions
) => webpack.Configuration;
export const findModuleFiles = (base: string, files?: string[], result?: string[]) => {
files = files || fs.readdirSync(base);
export const findModuleFiles = async (base: string, files?: string[], result?: string[]) => {
files = files || (await readdirPromise(base));
result = result || [];
if (files) {
files.forEach(file => {
files.forEach(async file => {
const newbase = path.join(base, file);
if (fs.statSync(newbase).isDirectory()) {
result = findModuleFiles(newbase, fs.readdirSync(newbase), result);
result = await findModuleFiles(newbase, await readdirPromise(newbase), result);
} else {
const filename = path.basename(file);
if (/^module.(t|j)sx?$/.exec(filename)) {
......@@ -56,9 +64,9 @@ const getManualChunk = (id: string) => {
return null;
};
const getEntries = () => {
const getEntries = async () => {
const entries: { [key: string]: string } = {};
const modules = getModuleFiles();
const modules = await getModuleFiles();
modules.forEach(modFile => {
const mod = getManualChunk(modFile);
......@@ -114,7 +122,7 @@ const getCommonPlugins = (options: WebpackConfigurationOptions) => {
];
};
export const getWebpackConfig: WebpackConfigurationGetter = options => {
const getBaseWebpackConfig: WebpackConfigurationGetter = async options => {
const plugins = getCommonPlugins(options);
const optimization: { [key: string]: any } = {};
......@@ -134,7 +142,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => {
},
context: path.join(process.cwd(), 'src'),
devtool: 'source-map',
entry: getEntries(),
entry: await getEntries(),
output: {
filename: '[name].js',
path: path.join(process.cwd(), 'dist'),
......@@ -224,3 +232,26 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => {
optimization,
};
};
export const loadWebpackConfig: WebpackConfigurationGetter = async options => {
const baseConfig = await getBaseWebpackConfig(options);
const customWebpackPath = path.resolve(process.cwd(), 'webpack.config.ts');
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) {
if (err.code === 'ENOENT') {
return baseConfig;
}
throw err;
}
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment