feat: support several APIs to modify webpack (#532)

* feat: support several APIs to modify webpack

* chore: optimize code

* fix: test case

* chore: optimize code
This commit is contained in:
ClarkXia 2022-09-21 16:55:22 +08:00
parent 86b234162d
commit c38af1ce64
6 changed files with 372 additions and 0 deletions

View File

@ -0,0 +1,5 @@
# Changelog
## 1.0.0
- [feat] support several APIs to modify webpack.

View File

@ -0,0 +1,59 @@
# @ice/webpack-modify
This package providers several APIs to simplify the modification of webpack configurations.
## Usage
```js
import { removeLoader, modifyLoader, addLoader, removePlugin } from '@ice/webpack-modify';
let modifiedConfig = {};
const webpackConfig = {
module: {
rules: [
{
test: /\.css$/,
use: [{ loader: "style-loader" }, { loader: "sass-loader" }],
},
],
},
};
// remove loader
modifiedConfig = removeLoader(webpackConfig, {
rule: 'css',
loader: 'style-loader',
});
// modify loader
modifiedConfig = modifyLoader(webpackConfig, {
rule: 'css',
loader: 'style-loader',
options: () => ({}),
});
// add loader
modifiedConfig = addLoader(webpackConfig, {
rule: 'css',
before: 'style-loader'
});
modifiedConfig = addLoader(webpackConfig, {
rule: 'css',
after: 'style-loader'
});
// modify webpack rule options
modifiedConfig = modifyRule(webpackConfig, {
rule: 'css',
options: () => {
return [
{
loader: 'css-loader',
options: () => ([]),
},
]
},
})
// remove plugin
modifiedConfig = removePlugin(webpackConfig, {
pluginName: 'AssetsManifestPlugin',
});
```

View File

@ -0,0 +1,25 @@
{
"name": "@ice/webpack-modify",
"version": "1.0.0",
"repository": "ice-lab/ice-next",
"bugs": "https://github.com/ice-lab/ice-next/issues",
"homepage": "https://next.ice.work",
"type": "module",
"main": "./esm/index.js",
"exports": "./esm/index.js",
"files": [
"esm",
"!esm/**/*.map"
],
"dependencies": {
"consola": "^2.15.3"
},
"devDependencies": {
"@ice/webpack-config": "^1.0.0",
"webpack": "^5.73.0"
},
"scripts": {
"watch": "tsc -w",
"build": "tsc"
}
}

View File

@ -0,0 +1,114 @@
import type { Configuration, RuleSetRule, RuleSetUseItem } from 'webpack';
import consola from 'consola';
interface RemoveOptions {
rule: string;
loader: string;
}
function findLoader(webpackConfig: Configuration, ruleName: string) {
// Find webpack loader by options
const targetRule = webpackConfig?.module?.rules?.find((rule) => {
return typeof rule === 'object' &&
rule.test instanceof RegExp &&
rule.test.source.includes(ruleName);
});
if (!targetRule) {
consola.warn(`Can not find webpack rule with rule.test of ${ruleName}`);
}
return targetRule;
}
export function removeLoader(webpackConfig: Configuration, options: RemoveOptions): Configuration {
const { rule: ruleName, loader: loaderName } = options;
// Find webpack loader by options
const targetRule = findLoader(webpackConfig, ruleName) as RuleSetRule;
if (targetRule && Array.isArray(targetRule?.use)) {
targetRule.use = targetRule.use.filter((ruleItem) => {
const matched = typeof ruleItem === 'object' && ruleItem.loader?.match(new RegExp(`[@/\\\\]${loaderName}`));
return !matched;
});
}
return webpackConfig;
}
interface ModifyRuleOptions {
rule: string;
options: (rule: RuleSetRule) => RuleSetRule;
}
export function modifyRule(webpackConfig: Configuration, options: ModifyRuleOptions) {
const { rule: ruleName, options: modifyOptions } = options;
const modifiedRules = webpackConfig?.module?.rules?.map((rule) => {
if (typeof rule === 'object' && rule.test instanceof RegExp && rule.test.source.includes(ruleName)) {
return modifyOptions(rule);
}
return rule;
});
return {
...webpackConfig,
module: {
...(webpackConfig.module || {}),
rules: modifiedRules,
},
};
}
type LoaderOptions = string | { [index: string]: any };
interface ModifyLoaderOptions {
rule: string;
loader: string;
options: (loaderOptions?: LoaderOptions) => LoaderOptions;
}
export function modifyLoader(webpackConfig: Configuration, options: ModifyLoaderOptions) {
const { rule: ruleName, loader: loaderName, options: modifyOptions } = options;
// Find webpack loader by options
const targetRule = findLoader(webpackConfig, ruleName) as RuleSetRule;
if (targetRule && Array.isArray(targetRule?.use)) {
targetRule.use = targetRule.use.map((ruleItem) => {
if (typeof ruleItem === 'object' && ruleItem.loader?.match(new RegExp(`[@/\\\\]${loaderName}`))) {
return {
...ruleItem,
options: modifyOptions(ruleItem.options),
};
}
return ruleItem;
});
}
return webpackConfig;
}
interface AddLoaderOptions {
rule: string;
useItem: RuleSetUseItem;
before?: string;
after?: string;
}
export function addLoader(webpackConfig: Configuration, options: AddLoaderOptions) {
const { rule: ruleName, after, before, useItem } = options;
const targetRule = findLoader(webpackConfig, ruleName) as RuleSetRule;
if (targetRule && Array.isArray(targetRule?.use)) {
const loaderIndex = targetRule.use.findIndex((ruleItem) => {
const matchLoader = after || before;
return typeof ruleItem === 'object' && matchLoader && ruleItem.loader?.match(new RegExp(`[@/\\\\]${matchLoader}`));
});
if (loaderIndex > -1) {
const spliceIndex = before ? loaderIndex : loaderIndex + 1;
targetRule.use.splice(spliceIndex, 0, useItem);
}
}
return webpackConfig;
}
export function removePlugin(webpackConfig: Configuration, pluginName: string) {
const webpackPlugins = (webpackConfig.plugins || []).filter((plugin) => {
return !(plugin?.constructor?.name === pluginName);
});
return {
...webpackConfig,
plugins: webpackPlugins,
};
}

View File

@ -0,0 +1,158 @@
import { expect, it, describe } from 'vitest';
import type { Configuration } from 'webpack';
import { removeLoader, addLoader, modifyLoader, modifyRule, removePlugin } from '../src/index';
describe('test webpack config modify', () => {
const getWebpackConfig = () => ({
module: {
rules: [
{
test: /.css$/,
use: [
{
loader: '/absoulte/path/to/postcss-loader',
},
{
loader: '/absoulte/path/to/css-loader',
},
],
},
],
},
});
it('remove loader', () => {
const webpackConfig: Configuration = getWebpackConfig();
expect(removeLoader(webpackConfig, {
rule: '.css',
loader: 'css-loader',
})).toStrictEqual({
module: {
rules: [{
test: /.css$/,
use: [
{
loader: '/absoulte/path/to/postcss-loader',
},
],
}],
},
});
});
it('add loader', () => {
expect(addLoader(getWebpackConfig(), {
rule: '.css',
useItem: {
loader: 'custom-loader',
},
before: 'css-loader',
})).toStrictEqual({
module: {
rules: [{
test: /.css$/,
use: [
{
loader: '/absoulte/path/to/postcss-loader',
},
{
loader: 'custom-loader',
},
{
loader: '/absoulte/path/to/css-loader',
},
],
}],
},
});
expect(addLoader(getWebpackConfig(), {
rule: '.css',
useItem: {
loader: 'custom-loader',
},
after: 'css-loader',
})).toStrictEqual({
module: {
rules: [{
test: /.css$/,
use: [
{
loader: '/absoulte/path/to/postcss-loader',
},
{
loader: '/absoulte/path/to/css-loader',
},
{
loader: 'custom-loader',
},
],
}],
},
});
});
it('modify loader', () => {
expect(modifyLoader(getWebpackConfig(), {
rule: '.css',
loader: 'css-loader',
options: () => ({ module: true }),
})).toStrictEqual({
module: {
rules: [{
test: /.css$/,
use: [
{
loader: '/absoulte/path/to/postcss-loader',
},
{
loader: '/absoulte/path/to/css-loader',
options: { module: true },
},
],
}],
},
});
});
it('modify rule', () => {
const webpackConfig = getWebpackConfig();
webpackConfig.module.rules.push({
test: /.less/,
use: [],
});
expect(modifyRule(webpackConfig, {
rule: '.css',
options: () => ({
test: /.css$/,
use: [],
}),
})).toStrictEqual({
module: {
rules: [{
test: /.css$/,
use: [],
}, {
test: /.less/,
use: [],
}],
},
});
});
it('remove plugin', () => {
class TestPlugin {}
expect(removePlugin({
plugins: [
// @ts-ignore fake webpack plugin
new TestPlugin(),
],
}, 'TestPlugin')).toStrictEqual({
plugins: [],
});
});
it('miss loader', () => {
expect(removeLoader(getWebpackConfig(), {
rule: '.css',
loader: 'test-loader',
})).toStrictEqual(getWebpackConfig());
expect(removeLoader(getWebpackConfig(), {
rule: '.test',
loader: 'css-loader',
})).toStrictEqual(getWebpackConfig());
});
});

View File

@ -1066,6 +1066,17 @@ importers:
webpack: 5.74.0_3cawqt5e67dzz3yqkvwzyd4tq4
webpack-dev-server: 4.10.0_webpack@5.74.0
packages/webpack-modify:
specifiers:
'@ice/webpack-config': ^1.0.0
consola: ^2.15.3
webpack: ^5.73.0
dependencies:
consola: 2.15.3
devDependencies:
'@ice/webpack-config': link:../webpack-config
webpack: 5.74.0
website:
specifiers:
'@algolia/client-search': ^4.9.1