feat(v2): allow extend PostCSS config (#4185)

* feat(v2): allow extend PostCSS config

* polish the configurePostCss system

Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
Alexey Pyltsyn 2021-02-09 22:02:54 +03:00 committed by GitHub
parent b3b658f687
commit 2fb642d9ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 235 additions and 27 deletions

View file

@ -199,6 +199,9 @@ export type AllContent = Record<
>
>;
// TODO improve type (not exposed by postcss-loader)
export type PostCssOptions = Record<string, any> & {plugins: any[]};
export interface Plugin<T, U = unknown> {
name: string;
loadContent?(): Promise<T>;
@ -220,6 +223,7 @@ export interface Plugin<T, U = unknown> {
isServer: boolean,
utils: ConfigureWebpackUtils,
): Configuration & {mergeStrategy?: ConfigureWebpackFnMergeStrategy};
configurePostCss?(options: PostCssOptions): PostCssOptions;
getThemePath?(): string;
getTypeScriptThemePath?(): string;
getPathsToWatch?(): string[];
@ -253,6 +257,7 @@ export interface Plugin<T, U = unknown> {
export type ConfigureWebpackFn = Plugin<unknown>['configureWebpack'];
export type ConfigureWebpackFnMergeStrategy = Record<string, MergeStrategy>;
export type ConfigurePostCssFn = Plugin<unknown>['configurePostCss'];
export type PluginOptions = {id?: string} & Record<string, unknown>;

View file

@ -20,7 +20,11 @@ import {handleBrokenLinks} from '../server/brokenLinks';
import {BuildCLIOptions, Props} from '@docusaurus/types';
import createClientConfig from '../webpack/client';
import createServerConfig from '../webpack/server';
import {compile, applyConfigureWebpack} from '../webpack/utils';
import {
compile,
applyConfigureWebpack,
applyConfigurePostCss,
} from '../webpack/utils';
import CleanWebpackPlugin from '../webpack/plugins/CleanWebpackPlugin';
import {loadI18n} from '../server/i18n';
import {mapAsyncSequencial} from '@docusaurus/utils';
@ -166,24 +170,27 @@ async function buildLocale({
});
}
// Plugin Lifecycle - configureWebpack.
// Plugin Lifecycle - configureWebpack and configurePostCss.
plugins.forEach((plugin) => {
const {configureWebpack} = plugin;
if (!configureWebpack) {
return;
const {configureWebpack, configurePostCss} = plugin;
if (configurePostCss) {
clientConfig = applyConfigurePostCss(configurePostCss, clientConfig);
}
clientConfig = applyConfigureWebpack(
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
clientConfig,
false,
);
if (configureWebpack) {
clientConfig = applyConfigureWebpack(
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
clientConfig,
false,
);
serverConfig = applyConfigureWebpack(
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
serverConfig,
true,
);
serverConfig = applyConfigureWebpack(
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
serverConfig,
true,
);
}
});
// Make sure generated client-manifest is cleaned first so we don't reuse

View file

@ -24,7 +24,11 @@ import {load} from '../server';
import {StartCLIOptions} from '@docusaurus/types';
import {CONFIG_FILE_NAME, STATIC_DIR_NAME} from '../constants';
import createClientConfig from '../webpack/client';
import {applyConfigureWebpack, getHttpsConfig} from '../webpack/utils';
import {
applyConfigureWebpack,
applyConfigurePostCss,
getHttpsConfig,
} from '../webpack/utils';
import {getCLIOptionHost, getCLIOptionPort} from './commandUtils';
import {getTranslationsLocaleDirPath} from '../server/translations/translations';
@ -134,18 +138,21 @@ export default async function start(
],
});
// Plugin Lifecycle - configureWebpack.
// Plugin Lifecycle - configureWebpack and configurePostCss.
plugins.forEach((plugin) => {
const {configureWebpack} = plugin;
if (!configureWebpack) {
return;
const {configureWebpack, configurePostCss} = plugin;
if (configurePostCss) {
config = applyConfigurePostCss(configurePostCss, config);
}
config = applyConfigureWebpack(
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
config,
false,
);
if (configureWebpack) {
config = applyConfigureWebpack(
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
config,
false,
);
}
});
// https://webpack.js.org/configuration/dev-server

View file

@ -12,7 +12,11 @@ import {
} from 'webpack';
import path from 'path';
import {applyConfigureWebpack, getFileLoaderUtils} from '../utils';
import {
applyConfigureWebpack,
applyConfigurePostCss,
getFileLoaderUtils,
} from '../utils';
import {
ConfigureWebpackFn,
ConfigureWebpackFnMergeStrategy,
@ -148,3 +152,123 @@ describe('getFileLoaderUtils()', () => {
);
});
});
describe('extending PostCSS', () => {
test('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) => {
return {
...postCssOptions,
plugins: [
...postCssOptions.plugins,
createFakePlugin('postcss-plugin-1'),
],
};
}, webpackConfig);
webpackConfig = applyConfigurePostCss((postCssOptions) => {
return {
...postCssOptions,
plugins: [
createFakePlugin('postcss-plugin-2'),
...postCssOptions.plugins,
],
};
}, webpackConfig);
webpackConfig = applyConfigurePostCss((postCssOptions) => {
return {
...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).toEqual('postcss-loader-1');
const pluginNames1 = postCssLoader1.options.postcssOptions.plugins.map(
// @ts-expect-error: relax type
(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).toEqual('postcss-loader-2');
const pluginNames2 = postCssLoader2.options.postcssOptions.plugins.map(
// @ts-expect-error: relax type
(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',
]);
});
});

View file

@ -11,6 +11,7 @@ import merge from 'webpack-merge';
import webpack, {
Configuration,
Loader,
NewLoader,
Plugin,
RuleSetRule,
Stats,
@ -23,7 +24,7 @@ import path from 'path';
import crypto from 'crypto';
import chalk from 'chalk';
import {TransformOptions} from '@babel/core';
import {ConfigureWebpackFn} from '@docusaurus/types';
import {ConfigureWebpackFn, ConfigurePostCssFn} from '@docusaurus/types';
import CssNanoPreset from '@docusaurus/cssnano-preset';
import {version as cacheLoaderVersion} from 'cache-loader/package.json';
import {BABEL_CONFIG_FILE_NAME, STATIC_ASSETS_DIR_NAME} from '../constants';
@ -175,6 +176,31 @@ export function applyConfigureWebpack(
return config;
}
export function applyConfigurePostCss(
configurePostCss: NonNullable<ConfigurePostCssFn>,
config: Configuration,
): Configuration {
type LocalPostCSSLoader = Loader & {options: {postcssOptions: any}};
function isPostCssLoader(loader: Loader): loader is LocalPostCSSLoader {
// TODO not ideal heuristic but good enough for our usecase?
return !!(loader as any)?.options?.postcssOptions;
}
// Does not handle all edge cases, but good enough for now
config.module?.rules.map((rule) => {
for (const loader of rule.use as NewLoader[]) {
if (isPostCssLoader(loader)) {
loader.options.postcssOptions = configurePostCss(
loader.options.postcssOptions,
);
}
}
});
return config;
}
// See https://webpack.js.org/configuration/stats/#statswarningsfilter
// @slorber: note sure why we have to re-implement this logic
// just know that legacy had this only partially implemented, so completed it

View file

@ -346,6 +346,45 @@ module.exports = function (context, options) {
Read the [webpack-merge strategy doc](https://github.com/survivejs/webpack-merge#merging-with-strategies) for more details.
## `configurePostCss(options)`
Modifies [`postcssOptions` of `postcss-loader`](https://webpack.js.org/loaders/postcss-loader/#postcssoptions) during the generation of the client bundle.
Should return the mutated `postcssOptions`.
By default, `postcssOptions` looks like this:
```js
const postcssOptions = {
ident: 'postcss',
plugins: [
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 4,
}),
],
};
```
Example:
```js title="docusaurus-plugin/src/index.js"
module.exports = function (context, options) {
return {
name: 'docusaurus-plugin',
// highlight-start
configurePostCss(postcssOptions) {
// Appends new PostCSS plugin.
postcssOptions.plugins.push(require('postcss-import'));
return postcssOptions;
},
// highlight-end
};
};
```
## `postBuild(props)`
Called when a (production) build finishes.