Skip to content

Commit

Permalink
feat(v2): configureWebpack merge strategy + use file-loader for commo…
Browse files Browse the repository at this point in the history
…n asset types (#2994)

* Add some default asset loaders
Add webpack merge strategy feature to enable plugins to prepend some webpack configuration (like the ideal image plugin that should override the default image loader)

* Add documentation for using assets from markdown

* add path prefix for webpack file loader

* renaming

* document Merge strategies

* rename mergeStrategies -> mergeStrategy
  • Loading branch information
slorber authored Jul 1, 2020
1 parent a5b2b60 commit 8aa6ef4
Show file tree
Hide file tree
Showing 13 changed files with 304 additions and 38 deletions.
3 changes: 3 additions & 0 deletions packages/docusaurus-plugin-ideal-image/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export default function (

configureWebpack(_config: Configuration, isServer: boolean) {
return {
mergeStrategy: {
'module.rules': 'prepend',
},
module: {
rules: [
{
Expand Down
3 changes: 2 additions & 1 deletion packages/docusaurus-types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dependencies": {
"@types/webpack": "^4.41.0",
"commander": "^4.0.1",
"querystring": "0.2.0"
"querystring": "0.2.0",
"webpack-merge": "^4.2.2"
}
}
6 changes: 5 additions & 1 deletion packages/docusaurus-types/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import {Loader, Configuration} from 'webpack';
import {Command} from 'commander';
import {ParsedUrlQueryInput} from 'querystring';
import {MergeStrategy} from 'webpack-merge';

export interface DocusaurusConfig {
baseUrl: string;
Expand Down Expand Up @@ -118,7 +119,7 @@ export interface Plugin<T, U = unknown> {
config: Configuration,
isServer: boolean,
utils: ConfigureWebpackUtils,
): Configuration;
): Configuration & {mergeStrategy?: ConfigureWebpackFnMergeStrategy};
getThemePath?(): string;
getTypeScriptThemePath?(): string;
getPathsToWatch?(): string[];
Expand All @@ -131,6 +132,9 @@ export interface Plugin<T, U = unknown> {
};
}

export type ConfigureWebpackFn = Plugin<unknown>['configureWebpack'];
export type ConfigureWebpackFnMergeStrategy = Record<string, MergeStrategy>;

export type PluginConfig =
| [string, Record<string, unknown>]
| [string]
Expand Down
2 changes: 2 additions & 0 deletions packages/docusaurus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"detect-port": "^1.3.0",
"eta": "^1.1.1",
"express": "^4.17.1",
"file-loader": "^6.0.0",
"fs-extra": "^8.1.0",
"globby": "^10.0.1",
"html-minifier-terser": "^5.0.5",
Expand Down Expand Up @@ -90,6 +91,7 @@
"shelljs": "^0.8.4",
"std-env": "^2.2.1",
"terser-webpack-plugin": "^2.3.5",
"url-loader": "^4.1.0",
"wait-file": "^1.0.5",
"webpack": "^4.41.2",
"webpack-bundle-analyzer": "^3.6.1",
Expand Down
79 changes: 68 additions & 11 deletions packages/docusaurus/src/webpack/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,42 @@
* LICENSE file in the root directory of this source tree.
*/

import {validate} from 'webpack';
import {
// @ts-expect-error: seems it's not in the typedefs???
validate,
Configuration,
} from 'webpack';
import path from 'path';

import {applyConfigureWebpack} from '../utils';
import {
ConfigureWebpackFn,
ConfigureWebpackFnMergeStrategy,
} from '@docusaurus/types';

describe('extending generated webpack config', () => {
test('direct mutation on generated webpack config object', async () => {
// fake generated webpack config
let config = {
let config: Configuration = {
output: {
path: __dirname,
filename: 'bundle.js',
},
};

/* eslint-disable */
const configureWebpack = (generatedConfig, isServer) => {
const configureWebpack: ConfigureWebpackFn = (
generatedConfig,
isServer,
) => {
if (!isServer) {
generatedConfig.entry = 'entry.js';
generatedConfig.output = {
path: path.join(__dirname, 'dist'),
filename: 'new.bundle.js',
};
}
return {};
};
/* eslint-enable */

config = applyConfigureWebpack(configureWebpack, config, false);
expect(config).toEqual({
Expand All @@ -45,23 +55,20 @@ describe('extending generated webpack config', () => {
});

test('webpack-merge with user webpack config object', async () => {
// fake generated webpack config
let config = {
let config: Configuration = {
output: {
path: __dirname,
filename: 'bundle.js',
},
};

/* eslint-disable */
const configureWebpack = {
const configureWebpack: ConfigureWebpackFn = () => ({
entry: 'entry.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'new.bundle.js',
},
};
/* eslint-enable */
});

config = applyConfigureWebpack(configureWebpack, config, false);
expect(config).toEqual({
Expand All @@ -74,4 +81,54 @@ describe('extending generated webpack config', () => {
const errors = validate(config);
expect(errors.length).toBe(0);
});

test('webpack-merge with custom strategy', async () => {
const config: Configuration = {
module: {
rules: [{use: 'xxx'}, {use: 'yyy'}],
},
};

const createConfigureWebpack: (
mergeStrategy?: ConfigureWebpackFnMergeStrategy,
) => ConfigureWebpackFn = (mergeStrategy) => () => ({
module: {
rules: [{use: 'zzz'}],
},
mergeStrategy,
});

const defaultStrategyMergeConfig = applyConfigureWebpack(
createConfigureWebpack(),
config,
false,
);
expect(defaultStrategyMergeConfig).toEqual({
module: {
rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}],
},
});

const prependRulesStrategyConfig = applyConfigureWebpack(
createConfigureWebpack({'module.rules': 'prepend'}),
config,
false,
);
expect(prependRulesStrategyConfig).toEqual({
module: {
rules: [{use: 'zzz'}, {use: 'xxx'}, {use: 'yyy'}],
},
});

const uselessMergeStrategyConfig = applyConfigureWebpack(
createConfigureWebpack({uselessAttributeName: 'append'}),
config,
false,
);
expect(uselessMergeStrategyConfig).toEqual({
module: {
rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}],
},
});
});
});
12 changes: 11 additions & 1 deletion packages/docusaurus/src/webpack/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import TerserPlugin from 'terser-webpack-plugin';
import {Configuration, Loader} from 'webpack';

import {Props} from '@docusaurus/types';
import {getBabelLoader, getCacheLoader, getStyleLoaders} from './utils';
import {
getBabelLoader,
getCacheLoader,
getStyleLoaders,
getFileLoaderUtils,
} from './utils';
import {BABEL_CONFIG_FILE_NAME} from '../constants';

const CSS_REGEX = /\.css$/;
Expand Down Expand Up @@ -48,6 +53,8 @@ export function createBaseConfig(
BABEL_CONFIG_FILE_NAME,
);

const fileLoaderUtils = getFileLoaderUtils();

return {
mode: isProd ? 'production' : 'development',
output: {
Expand Down Expand Up @@ -158,6 +165,9 @@ export function createBaseConfig(
},
module: {
rules: [
fileLoaderUtils.rules.images(),
fileLoaderUtils.rules.media(),
fileLoaderUtils.rules.otherAssets(),
{
test: /\.(j|t)sx?$/,
exclude: excludeJS,
Expand Down
82 changes: 66 additions & 16 deletions packages/docusaurus/src/webpack/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import env from 'std-env';
import merge from 'webpack-merge';
import {Configuration, Loader} from 'webpack';
import {Configuration, Loader, RuleSetRule} from 'webpack';
import {TransformOptions} from '@babel/core';
import {ConfigureWebpackUtils} from '@docusaurus/types';

import {ConfigureWebpackFn} from '@docusaurus/types';
import {version as cacheLoaderVersion} from 'cache-loader/package.json';

// Utility method to get style loaders
Expand Down Expand Up @@ -120,31 +119,82 @@ export function getBabelLoader(
* @returns final/ modified webpack config
*/
export function applyConfigureWebpack(
configureWebpack:
| Configuration
| ((
config: Configuration,
isServer: boolean,
utils: ConfigureWebpackUtils,
) => Configuration),
configureWebpack: ConfigureWebpackFn,
config: Configuration,
isServer: boolean,
): Configuration {
if (typeof configureWebpack === 'object') {
return merge(config, configureWebpack);
}

// Export some utility functions
const utils = {
getStyleLoaders,
getCacheLoader,
getBabelLoader,
};
if (typeof configureWebpack === 'function') {
const res = configureWebpack(config, isServer, utils);
const {mergeStrategy, ...res} = configureWebpack(config, isServer, utils);
if (res && typeof res === 'object') {
return merge(config, res);
return merge.strategy(mergeStrategy ?? {})(config, res);
}
}
return config;
}

// Inspired by https://github.com/gatsbyjs/gatsby/blob/8e6e021014da310b9cc7d02e58c9b3efe938c665/packages/gatsby/src/utils/webpack-utils.ts#L447
export function getFileLoaderUtils() {
const assetsRelativeRoot = 'assets/';

const loaders = {
file: (options = {}) => {
return {
loader: require.resolve(`file-loader`),
options: {
name: `${assetsRelativeRoot}[name]-[hash].[ext]`,
...options,
},
};
},
url: (options = {}) => {
return {
loader: require.resolve(`url-loader`),
options: {
limit: 10000,
name: `${assetsRelativeRoot}[name]-[hash].[ext]`,
fallback: require.resolve(`file-loader`),
...options,
},
};
},
};

const rules = {
/**
* Loads image assets, inlines images via a data URI if they are below
* the size threshold
*/
images: (): RuleSetRule => {
return {
use: [loaders.url()],
test: /\.(ico|svg|jpg|jpeg|png|gif|webp)(\?.*)?$/,
};
},

/**
* Loads audio and video and inlines them via a data URI if they are below
* the size threshold
*/
media: (): RuleSetRule => {
return {
use: [loaders.url()],
test: /\.(mp4|webm|ogv|wav|mp3|m4a|aac|oga|flac)$/,
};
},

otherAssets: (): RuleSetRule => {
return {
use: [loaders.file()],
test: /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/,
};
},
};

return {loaders, rules};
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file added website/docs/assets/docusaurus-asset-example.xyz
Binary file not shown.
22 changes: 22 additions & 0 deletions website/docs/lifecycle-apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,28 @@ module.exports = function (context, options) {
};
```

### Merge strategy

We merge the Webpack configuration parts of plugins into the global Webpack config using [webpack-merge](https://github.com/survivejs/webpack-merge).

It is possible to specify the merge strategy. For example, if you want a webpack rule to be prepended instead of appended:

```js {4-11} title="docusaurus-plugin/src/index.js"
module.exports = function (context, options) {
return {
name: 'custom-docusaurus-plugin',
configureWebpack(config, isServer, utils) {
return {
mergeStrategy: {'module.rules': 'prepend'},
module: {rules: [myRuleToPrepend]},
};
},
};
};
```

Read the [webpack-merge strategy doc](https://github.com/survivejs/webpack-merge#merging-with-strategies) for more details.

## `postBuild(props)`

Called when a (production) build finishes.
Expand Down
Loading

0 comments on commit 8aa6ef4

Please sign in to comment.