fix(core): configurePostCss() should run after configureWebpack() (#10132)

This commit is contained in:
Sébastien Lorber 2024-05-13 15:03:48 +02:00 committed by GitHub
parent 29b7a4ddbb
commit ff5039f413
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 620 additions and 405 deletions

View file

@ -13,7 +13,7 @@ import {isMatch} from 'picomatch';
import commander from 'commander'; import commander from 'commander';
import webpack from 'webpack'; import webpack from 'webpack';
import {loadContext} from '@docusaurus/core/src/server/site'; import {loadContext} from '@docusaurus/core/src/server/site';
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils'; import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/configure';
import {sortRoutes} from '@docusaurus/core/src/server/plugins/routeConfig'; import {sortRoutes} from '@docusaurus/core/src/server/plugins/routeConfig';
import {posixPath} from '@docusaurus/utils'; import {posixPath} from '@docusaurus/utils';
import {normalizePluginOptions} from '@docusaurus/utils-validation'; import {normalizePluginOptions} from '@docusaurus/utils-validation';

View file

@ -15,11 +15,8 @@ import {handleBrokenLinks} from '../server/brokenLinks';
import {createBuildClientConfig} from '../webpack/client'; import {createBuildClientConfig} from '../webpack/client';
import createServerConfig from '../webpack/server'; import createServerConfig from '../webpack/server';
import { import {executePluginsConfigureWebpack} from '../webpack/configure';
executePluginsConfigurePostCss, import {compile} from '../webpack/utils';
executePluginsConfigureWebpack,
compile,
} from '../webpack/utils';
import {PerfLogger} from '../utils'; import {PerfLogger} from '../utils';
import {loadI18n} from '../server/i18n'; import {loadI18n} from '../server/i18n';
@ -325,10 +322,6 @@ async function getBuildClientConfig({
bundleAnalyzer: cliOptions.bundleAnalyzer ?? false, bundleAnalyzer: cliOptions.bundleAnalyzer ?? false,
}); });
let {config} = result; let {config} = result;
config = executePluginsConfigurePostCss({
plugins,
config,
});
config = executePluginsConfigureWebpack({ config = executePluginsConfigureWebpack({
plugins, plugins,
config, config,

View file

@ -13,12 +13,11 @@ import WebpackDevServer from 'webpack-dev-server';
import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware'; import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware';
import {createPollingOptions} from './watcher'; import {createPollingOptions} from './watcher';
import { import {
executePluginsConfigurePostCss,
executePluginsConfigureWebpack,
formatStatsErrorMessage, formatStatsErrorMessage,
getHttpsConfig, getHttpsConfig,
printStatsWarnings, printStatsWarnings,
} from '../../webpack/utils'; } from '../../webpack/utils';
import {executePluginsConfigureWebpack} from '../../webpack/configure';
import {createStartClientConfig} from '../../webpack/client'; import {createStartClientConfig} from '../../webpack/client';
import type {StartCLIOptions} from './start'; import type {StartCLIOptions} from './start';
import type {Props} from '@docusaurus/types'; import type {Props} from '@docusaurus/types';
@ -135,7 +134,6 @@ async function getStartClientConfig({
minify, minify,
poll, poll,
}); });
config = executePluginsConfigurePostCss({plugins, config});
config = executePluginsConfigureWebpack({ config = executePluginsConfigureWebpack({
plugins, plugins,
config, config,

View file

@ -0,0 +1,458 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import * as path from 'path';
import * as webpack from 'webpack';
import {fromPartial} from '@total-typescript/shoehorn';
import {
applyConfigureWebpack,
applyConfigurePostCss,
executePluginsConfigureWebpack,
} from '../configure';
import type {Configuration} from 'webpack';
import type {LoadedPlugin, Plugin} from '@docusaurus/types';
describe('extending generated webpack config', () => {
it('direct mutation on generated webpack config object', async () => {
// Fake generated webpack config
let config: Configuration = {
output: {
path: __dirname,
filename: 'bundle.js',
},
};
// @ts-expect-error: Testing an edge-case that we did not write types for
const configureWebpack: NonNullable<Plugin['configureWebpack']> = (
generatedConfig,
isServer,
) => {
if (!isServer) {
generatedConfig.entry = 'entry.js';
generatedConfig.output = {
path: path.join(__dirname, 'dist'),
filename: 'new.bundle.js',
};
}
// Implicitly returning undefined to test null-safety
};
config = applyConfigureWebpack(configureWebpack, config, false, undefined, {
content: 42,
});
expect(config).toEqual({
entry: 'entry.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'new.bundle.js',
},
});
const errors = webpack.validate(config);
expect(errors).toBeUndefined();
});
it('webpack-merge with user webpack config object', async () => {
let config: Configuration = {
output: {
path: __dirname,
filename: 'bundle.js',
},
};
const configureWebpack: Plugin['configureWebpack'] = () => ({
entry: 'entry.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'new.bundle.js',
},
});
config = applyConfigureWebpack(configureWebpack, config, false, undefined, {
content: 42,
});
expect(config).toEqual({
entry: 'entry.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'new.bundle.js',
},
});
const errors = webpack.validate(config);
expect(errors).toBeUndefined();
});
it('webpack-merge with custom strategy', async () => {
const config: Configuration = {
module: {
rules: [{use: 'xxx'}, {use: 'yyy'}],
},
};
const createConfigureWebpack =
(mergeStrategy?: {
[key: string]: 'prepend' | 'append';
}): NonNullable<Plugin['configureWebpack']> =>
() => ({
module: {
rules: [{use: 'zzz'}],
},
mergeStrategy,
});
const defaultStrategyMergeConfig = applyConfigureWebpack(
createConfigureWebpack(),
config,
false,
undefined,
{content: 42},
);
expect(defaultStrategyMergeConfig).toEqual({
module: {
rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}],
},
});
const prependRulesStrategyConfig = applyConfigureWebpack(
createConfigureWebpack({'module.rules': 'prepend'}),
config,
false,
undefined,
{content: 42},
);
expect(prependRulesStrategyConfig).toEqual({
module: {
rules: [{use: 'zzz'}, {use: 'xxx'}, {use: 'yyy'}],
},
});
const uselessMergeStrategyConfig = applyConfigureWebpack(
createConfigureWebpack({uselessAttributeName: 'append'}),
config,
false,
undefined,
{content: 42},
);
expect(uselessMergeStrategyConfig).toEqual({
module: {
rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}],
},
});
});
});
describe('extending PostCSS', () => {
it('user plugin should be appended in PostCSS loader', () => {
let webpackConfig: Configuration = {
output: {
path: __dirname,
filename: 'bundle.js',
},
module: {
rules: [
{
test: 'any',
use: [
{
loader: 'some-loader-1',
options: {},
},
{
loader: 'some-loader-2',
options: {},
},
{
loader: 'postcss-loader-1',
options: {
postcssOptions: {
plugins: [['default-postcss-loader-1-plugin']],
},
},
},
{
loader: 'some-loader-3',
options: {},
},
],
},
{
test: '2nd-test',
use: [
{
loader: 'postcss-loader-2',
options: {
postcssOptions: {
plugins: [['default-postcss-loader-2-plugin']],
},
},
},
],
},
],
},
};
function createFakePlugin(name: string) {
return [name, {}];
}
// Run multiple times: ensure last run does not override previous runs
webpackConfig = applyConfigurePostCss(
(postCssOptions) => ({
...postCssOptions,
plugins: [
...postCssOptions.plugins,
createFakePlugin('postcss-plugin-1'),
],
}),
webpackConfig,
);
webpackConfig = applyConfigurePostCss(
(postCssOptions) => ({
...postCssOptions,
plugins: [
createFakePlugin('postcss-plugin-2'),
...postCssOptions.plugins,
],
}),
webpackConfig,
);
webpackConfig = applyConfigurePostCss(
(postCssOptions) => ({
...postCssOptions,
plugins: [
...postCssOptions.plugins,
createFakePlugin('postcss-plugin-3'),
],
}),
webpackConfig,
);
// @ts-expect-error: relax type
const postCssLoader1 = webpackConfig.module?.rules[0].use[2];
expect(postCssLoader1.loader).toBe('postcss-loader-1');
const pluginNames1 = postCssLoader1.options.postcssOptions.plugins.map(
(p: unknown[]) => p[0],
);
expect(pluginNames1).toHaveLength(4);
expect(pluginNames1).toEqual([
'postcss-plugin-2',
'default-postcss-loader-1-plugin',
'postcss-plugin-1',
'postcss-plugin-3',
]);
// @ts-expect-error: relax type
const postCssLoader2 = webpackConfig.module?.rules[1].use[0];
expect(postCssLoader2.loader).toBe('postcss-loader-2');
const pluginNames2 = postCssLoader2.options.postcssOptions.plugins.map(
(p: unknown[]) => p[0],
);
expect(pluginNames2).toHaveLength(4);
expect(pluginNames2).toEqual([
'postcss-plugin-2',
'default-postcss-loader-2-plugin',
'postcss-plugin-1',
'postcss-plugin-3',
]);
});
});
describe('executePluginsConfigureWebpack', () => {
function fakePlugin(partialPlugin: Partial<LoadedPlugin>): LoadedPlugin {
return fromPartial({
...partialPlugin,
});
}
it('can merge Webpack aliases of 2 plugins into base config', () => {
const config = executePluginsConfigureWebpack({
config: {resolve: {alias: {'initial-alias': 'initial-alias-value'}}},
isServer: false,
jsLoader: 'babel',
plugins: [
fakePlugin({
configureWebpack: () => {
return {resolve: {alias: {'p1-alias': 'p1-alias-value'}}};
},
}),
fakePlugin({
configureWebpack: () => {
return {resolve: {alias: {'p2-alias': 'p2-alias-value'}}};
},
}),
],
});
expect(config).toMatchInlineSnapshot(
{},
`
{
"resolve": {
"alias": {
"initial-alias": "initial-alias-value",
"p1-alias": "p1-alias-value",
"p2-alias": "p2-alias-value",
},
},
}
`,
);
});
it('can configurePostCSS() for all loaders added through configureWebpack()', () => {
const config = executePluginsConfigureWebpack({
config: {},
isServer: false,
jsLoader: 'babel',
plugins: [
fakePlugin({
configurePostCss: (postCssOptions) => {
// Imperative mutation should work
postCssOptions.plugins.push('p1-added-postcss-plugin');
return postCssOptions;
},
configureWebpack: () => {
return {
module: {
rules: [
{
test: /\.module.scss$/,
use: 'some-loader',
options: {
postcssOptions: {
plugins: ['p1-initial-postcss-plugin'],
},
},
},
],
},
};
},
}),
fakePlugin({
configurePostCss: (postCssOptions) => {
postCssOptions.plugins.push('p2-added-postcss-plugin');
return postCssOptions;
},
configureWebpack: () => {
return {
module: {
rules: [
{
test: /\.module.scss$/,
use: [
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['p2-initial-postcss-plugin'],
},
},
},
],
},
],
},
};
},
}),
fakePlugin({
configurePostCss: (postCssOptions) => {
// Functional/immutable copy mutation should work
return {
...postCssOptions,
plugins: [...postCssOptions.plugins, 'p3-added-postcss-plugin'],
};
},
configureWebpack: () => {
return {
module: {
rules: [
{
test: /\.module.scss$/,
oneOf: [
{
use: 'some-loader',
options: {
postcssOptions: {
plugins: ['p3-initial-postcss-plugin'],
},
},
},
],
},
],
},
};
},
}),
],
});
expect(config.module.rules).toHaveLength(3);
expect(config.module.rules[0]).toMatchInlineSnapshot(`
{
"options": {
"postcssOptions": {
"plugins": [
"p1-initial-postcss-plugin",
"p1-added-postcss-plugin",
"p2-added-postcss-plugin",
"p3-added-postcss-plugin",
],
},
},
"test": /\\\\\\.module\\.scss\\$/,
"use": "some-loader",
}
`);
expect(config.module.rules[1]).toMatchInlineSnapshot(`
{
"test": /\\\\\\.module\\.scss\\$/,
"use": [
{
"loader": "postcss-loader",
"options": {
"postcssOptions": {
"plugins": [
"p2-initial-postcss-plugin",
"p1-added-postcss-plugin",
"p2-added-postcss-plugin",
"p3-added-postcss-plugin",
],
},
},
},
],
}
`);
expect(config.module.rules[2]).toMatchInlineSnapshot(`
{
"oneOf": [
{
"options": {
"postcssOptions": {
"plugins": [
"p3-initial-postcss-plugin",
"p1-added-postcss-plugin",
"p2-added-postcss-plugin",
"p3-added-postcss-plugin",
],
},
},
"use": "some-loader",
},
],
"test": /\\\\\\.module\\.scss\\$/,
}
`);
});
});

View file

@ -6,15 +6,8 @@
*/ */
import path from 'path'; import path from 'path';
import webpack, {type Configuration, type RuleSetRule} from 'webpack'; import {getCustomizableJSLoader, getHttpsConfig} from '../utils';
import type {RuleSetRule} from 'webpack';
import {
getCustomizableJSLoader,
applyConfigureWebpack,
applyConfigurePostCss,
getHttpsConfig,
} from '../utils';
import type {Plugin} from '@docusaurus/types';
describe('customize JS loader', () => { describe('customize JS loader', () => {
it('getCustomizableJSLoader defaults to babel loader', () => { it('getCustomizableJSLoader defaults to babel loader', () => {
@ -50,255 +43,6 @@ describe('customize JS loader', () => {
}); });
}); });
describe('extending generated webpack config', () => {
it('direct mutation on generated webpack config object', async () => {
// Fake generated webpack config
let config: Configuration = {
output: {
path: __dirname,
filename: 'bundle.js',
},
};
// @ts-expect-error: Testing an edge-case that we did not write types for
const configureWebpack: NonNullable<Plugin['configureWebpack']> = (
generatedConfig,
isServer,
) => {
if (!isServer) {
generatedConfig.entry = 'entry.js';
generatedConfig.output = {
path: path.join(__dirname, 'dist'),
filename: 'new.bundle.js',
};
}
// Implicitly returning undefined to test null-safety
};
config = applyConfigureWebpack(configureWebpack, config, false, undefined, {
content: 42,
});
expect(config).toEqual({
entry: 'entry.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'new.bundle.js',
},
});
const errors = webpack.validate(config);
expect(errors).toBeUndefined();
});
it('webpack-merge with user webpack config object', async () => {
let config: Configuration = {
output: {
path: __dirname,
filename: 'bundle.js',
},
};
const configureWebpack: Plugin['configureWebpack'] = () => ({
entry: 'entry.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'new.bundle.js',
},
});
config = applyConfigureWebpack(configureWebpack, config, false, undefined, {
content: 42,
});
expect(config).toEqual({
entry: 'entry.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'new.bundle.js',
},
});
const errors = webpack.validate(config);
expect(errors).toBeUndefined();
});
it('webpack-merge with custom strategy', async () => {
const config: Configuration = {
module: {
rules: [{use: 'xxx'}, {use: 'yyy'}],
},
};
const createConfigureWebpack =
(mergeStrategy?: {
[key: string]: 'prepend' | 'append';
}): NonNullable<Plugin['configureWebpack']> =>
() => ({
module: {
rules: [{use: 'zzz'}],
},
mergeStrategy,
});
const defaultStrategyMergeConfig = applyConfigureWebpack(
createConfigureWebpack(),
config,
false,
undefined,
{content: 42},
);
expect(defaultStrategyMergeConfig).toEqual({
module: {
rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}],
},
});
const prependRulesStrategyConfig = applyConfigureWebpack(
createConfigureWebpack({'module.rules': 'prepend'}),
config,
false,
undefined,
{content: 42},
);
expect(prependRulesStrategyConfig).toEqual({
module: {
rules: [{use: 'zzz'}, {use: 'xxx'}, {use: 'yyy'}],
},
});
const uselessMergeStrategyConfig = applyConfigureWebpack(
createConfigureWebpack({uselessAttributeName: 'append'}),
config,
false,
undefined,
{content: 42},
);
expect(uselessMergeStrategyConfig).toEqual({
module: {
rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}],
},
});
});
});
describe('extending PostCSS', () => {
it('user plugin should be appended in PostCSS loader', () => {
let webpackConfig: Configuration = {
output: {
path: __dirname,
filename: 'bundle.js',
},
module: {
rules: [
{
test: 'any',
use: [
{
loader: 'some-loader-1',
options: {},
},
{
loader: 'some-loader-2',
options: {},
},
{
loader: 'postcss-loader-1',
options: {
postcssOptions: {
plugins: [['default-postcss-loader-1-plugin']],
},
},
},
{
loader: 'some-loader-3',
options: {},
},
],
},
{
test: '2nd-test',
use: [
{
loader: 'postcss-loader-2',
options: {
postcssOptions: {
plugins: [['default-postcss-loader-2-plugin']],
},
},
},
],
},
],
},
};
function createFakePlugin(name: string) {
return [name, {}];
}
// Run multiple times: ensure last run does not override previous runs
webpackConfig = applyConfigurePostCss(
(postCssOptions) => ({
...postCssOptions,
plugins: [
...postCssOptions.plugins,
createFakePlugin('postcss-plugin-1'),
],
}),
webpackConfig,
);
webpackConfig = applyConfigurePostCss(
(postCssOptions) => ({
...postCssOptions,
plugins: [
createFakePlugin('postcss-plugin-2'),
...postCssOptions.plugins,
],
}),
webpackConfig,
);
webpackConfig = applyConfigurePostCss(
(postCssOptions) => ({
...postCssOptions,
plugins: [
...postCssOptions.plugins,
createFakePlugin('postcss-plugin-3'),
],
}),
webpackConfig,
);
// @ts-expect-error: relax type
const postCssLoader1 = webpackConfig.module?.rules[0].use[2];
expect(postCssLoader1.loader).toBe('postcss-loader-1');
const pluginNames1 = postCssLoader1.options.postcssOptions.plugins.map(
(p: unknown[]) => p[0],
);
expect(pluginNames1).toHaveLength(4);
expect(pluginNames1).toEqual([
'postcss-plugin-2',
'default-postcss-loader-1-plugin',
'postcss-plugin-1',
'postcss-plugin-3',
]);
// @ts-expect-error: relax type
const postCssLoader2 = webpackConfig.module?.rules[1].use[0];
expect(postCssLoader2.loader).toBe('postcss-loader-2');
const pluginNames2 = postCssLoader2.options.postcssOptions.plugins.map(
(p: unknown[]) => p[0],
);
expect(pluginNames2).toHaveLength(4);
expect(pluginNames2).toEqual([
'postcss-plugin-2',
'default-postcss-loader-2-plugin',
'postcss-plugin-1',
'postcss-plugin-3',
]);
});
});
describe('getHttpsConfig', () => { describe('getHttpsConfig', () => {
const originalEnv = process.env; const originalEnv = process.env;

View file

@ -0,0 +1,156 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
mergeWithCustomize,
customizeArray,
customizeObject,
} from 'webpack-merge';
import {getCustomizableJSLoader, getStyleLoaders} from './utils';
import type {Configuration, RuleSetRule} from 'webpack';
import type {
Plugin,
PostCssOptions,
ConfigureWebpackUtils,
LoadedPlugin,
} from '@docusaurus/types';
/**
* Helper function to modify webpack config
* @param configureWebpack a webpack config or a function to modify config
* @param config initial webpack config
* @param isServer indicates if this is a server webpack configuration
* @param jsLoader custom js loader config
* @param content content loaded by the plugin
* @returns final/ modified webpack config
*/
export function applyConfigureWebpack(
configureWebpack: NonNullable<Plugin['configureWebpack']>,
config: Configuration,
isServer: boolean,
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined,
content: unknown,
): Configuration {
// Export some utility functions
const utils: ConfigureWebpackUtils = {
getStyleLoaders,
getJSLoader: getCustomizableJSLoader(jsLoader),
};
if (typeof configureWebpack === 'function') {
const {mergeStrategy, ...res} =
configureWebpack(config, isServer, utils, content) ?? {};
const customizeRules = mergeStrategy ?? {};
return mergeWithCustomize({
customizeArray: customizeArray(customizeRules),
customizeObject: customizeObject(customizeRules),
})(config, res);
}
return config;
}
export function applyConfigurePostCss(
configurePostCss: NonNullable<Plugin['configurePostCss']>,
config: Configuration,
): Configuration {
type LocalPostCSSLoader = object & {
options: {postcssOptions: PostCssOptions};
};
// Not ideal heuristic but good enough for our use-case?
function isPostCssLoader(loader: unknown): loader is LocalPostCSSLoader {
return !!(loader as LocalPostCSSLoader)?.options?.postcssOptions;
}
// Does not handle all edge cases, but good enough for now
function overridePostCssOptions(entry: RuleSetRule) {
if (isPostCssLoader(entry)) {
entry.options.postcssOptions = configurePostCss(
entry.options.postcssOptions,
);
} else if (Array.isArray(entry.oneOf)) {
entry.oneOf.forEach((r) => {
if (r) {
overridePostCssOptions(r);
}
});
} else if (Array.isArray(entry.use)) {
entry.use
.filter((u) => typeof u === 'object')
.forEach((rule) => overridePostCssOptions(rule as RuleSetRule));
}
}
config.module?.rules?.forEach((rule) =>
overridePostCssOptions(rule as RuleSetRule),
);
return config;
}
// Plugin Lifecycle - configurePostCss()
function executePluginsConfigurePostCss({
plugins,
config,
}: {
plugins: LoadedPlugin[];
config: Configuration;
}): Configuration {
let resultConfig = config;
plugins.forEach((plugin) => {
const {configurePostCss} = plugin;
if (configurePostCss) {
resultConfig = applyConfigurePostCss(
configurePostCss.bind(plugin),
resultConfig,
);
}
});
return resultConfig;
}
// Plugin Lifecycle - configureWebpack()
export function executePluginsConfigureWebpack({
plugins,
config,
isServer,
jsLoader,
}: {
plugins: LoadedPlugin[];
config: Configuration;
isServer: boolean;
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined;
}): Configuration {
// Step1 - Configure Webpack
let resultConfig = config;
plugins.forEach((plugin) => {
const {configureWebpack} = plugin;
if (configureWebpack) {
resultConfig = applyConfigureWebpack(
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
resultConfig,
isServer,
jsLoader,
plugin.content,
);
}
});
// Step2 - For client code, configure PostCSS
// The order matters! We want to configure PostCSS on loaders
// that were potentially added by configureWebpack
// See https://github.com/facebook/docusaurus/issues/10106
// Note: it's useless to configure postCSS for the server
if (!isServer) {
resultConfig = executePluginsConfigurePostCss({
plugins,
config: resultConfig,
});
}
return resultConfig;
}

View file

@ -11,20 +11,9 @@ import crypto from 'crypto';
import logger from '@docusaurus/logger'; import logger from '@docusaurus/logger';
import {BABEL_CONFIG_FILE_NAME} from '@docusaurus/utils'; import {BABEL_CONFIG_FILE_NAME} from '@docusaurus/utils';
import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import {
mergeWithCustomize,
customizeArray,
customizeObject,
} from 'webpack-merge';
import webpack, {type Configuration, type RuleSetRule} from 'webpack'; import webpack, {type Configuration, type RuleSetRule} from 'webpack';
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages'; import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
import type {TransformOptions} from '@babel/core'; import type {TransformOptions} from '@babel/core';
import type {
Plugin,
PostCssOptions,
ConfigureWebpackUtils,
LoadedPlugin,
} from '@docusaurus/types';
export function formatStatsErrorMessage( export function formatStatsErrorMessage(
statsJson: ReturnType<webpack.Stats['toJson']> | undefined, statsJson: ReturnType<webpack.Stats['toJson']> | undefined,
@ -181,129 +170,6 @@ export const getCustomizableJSLoader =
? getDefaultBabelLoader({isServer, babelOptions}) ? getDefaultBabelLoader({isServer, babelOptions})
: jsLoader(isServer); : jsLoader(isServer);
/**
* Helper function to modify webpack config
* @param configureWebpack a webpack config or a function to modify config
* @param config initial webpack config
* @param isServer indicates if this is a server webpack configuration
* @param jsLoader custom js loader config
* @param content content loaded by the plugin
* @returns final/ modified webpack config
*/
export function applyConfigureWebpack(
configureWebpack: NonNullable<Plugin['configureWebpack']>,
config: Configuration,
isServer: boolean,
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined,
content: unknown,
): Configuration {
// Export some utility functions
const utils: ConfigureWebpackUtils = {
getStyleLoaders,
getJSLoader: getCustomizableJSLoader(jsLoader),
};
if (typeof configureWebpack === 'function') {
const {mergeStrategy, ...res} =
configureWebpack(config, isServer, utils, content) ?? {};
const customizeRules = mergeStrategy ?? {};
return mergeWithCustomize({
customizeArray: customizeArray(customizeRules),
customizeObject: customizeObject(customizeRules),
})(config, res);
}
return config;
}
export function applyConfigurePostCss(
configurePostCss: NonNullable<Plugin['configurePostCss']>,
config: Configuration,
): Configuration {
type LocalPostCSSLoader = object & {
options: {postcssOptions: PostCssOptions};
};
// Not ideal heuristic but good enough for our use-case?
function isPostCssLoader(loader: unknown): loader is LocalPostCSSLoader {
return !!(loader as LocalPostCSSLoader)?.options?.postcssOptions;
}
// Does not handle all edge cases, but good enough for now
function overridePostCssOptions(entry: RuleSetRule) {
if (isPostCssLoader(entry)) {
entry.options.postcssOptions = configurePostCss(
entry.options.postcssOptions,
);
} else if (Array.isArray(entry.oneOf)) {
entry.oneOf.forEach((r) => {
if (r) {
overridePostCssOptions(r);
}
});
} else if (Array.isArray(entry.use)) {
entry.use
.filter((u) => typeof u === 'object')
.forEach((rule) => overridePostCssOptions(rule as RuleSetRule));
}
}
config.module?.rules?.forEach((rule) =>
overridePostCssOptions(rule as RuleSetRule),
);
return config;
}
// Plugin Lifecycle - configurePostCss()
export function executePluginsConfigurePostCss({
plugins,
config,
}: {
plugins: LoadedPlugin[];
config: Configuration;
}): Configuration {
let resultConfig = config;
plugins.forEach((plugin) => {
const {configurePostCss} = plugin;
if (configurePostCss) {
resultConfig = applyConfigurePostCss(
configurePostCss.bind(plugin),
resultConfig,
);
}
});
return resultConfig;
}
// Plugin Lifecycle - configureWebpack()
export function executePluginsConfigureWebpack({
plugins,
config,
isServer,
jsLoader,
}: {
plugins: LoadedPlugin[];
config: Configuration;
isServer: boolean;
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined;
}): Configuration {
let resultConfig = config;
plugins.forEach((plugin) => {
const {configureWebpack} = plugin;
if (configureWebpack) {
resultConfig = applyConfigureWebpack(
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
resultConfig,
isServer,
jsLoader,
plugin.content,
);
}
});
return resultConfig;
}
declare global { declare global {
interface Error { interface Error {
/** @see https://webpack.js.org/api/node/#error-handling */ /** @see https://webpack.js.org/api/node/#error-handling */