Skip to content

Commit 2fb642d

Browse files
lex111slorber
andauthored
feat(v2): allow extend PostCSS config (#4185)
* feat(v2): allow extend PostCSS config * polish the configurePostCss system Co-authored-by: slorber <[email protected]>
1 parent b3b658f commit 2fb642d

File tree

6 files changed

+235
-27
lines changed

6 files changed

+235
-27
lines changed

packages/docusaurus-types/src/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ export type AllContent = Record<
199199
>
200200
>;
201201

202+
// TODO improve type (not exposed by postcss-loader)
203+
export type PostCssOptions = Record<string, any> & {plugins: any[]};
204+
202205
export interface Plugin<T, U = unknown> {
203206
name: string;
204207
loadContent?(): Promise<T>;
@@ -220,6 +223,7 @@ export interface Plugin<T, U = unknown> {
220223
isServer: boolean,
221224
utils: ConfigureWebpackUtils,
222225
): Configuration & {mergeStrategy?: ConfigureWebpackFnMergeStrategy};
226+
configurePostCss?(options: PostCssOptions): PostCssOptions;
223227
getThemePath?(): string;
224228
getTypeScriptThemePath?(): string;
225229
getPathsToWatch?(): string[];
@@ -253,6 +257,7 @@ export interface Plugin<T, U = unknown> {
253257

254258
export type ConfigureWebpackFn = Plugin<unknown>['configureWebpack'];
255259
export type ConfigureWebpackFnMergeStrategy = Record<string, MergeStrategy>;
260+
export type ConfigurePostCssFn = Plugin<unknown>['configurePostCss'];
256261

257262
export type PluginOptions = {id?: string} & Record<string, unknown>;
258263

packages/docusaurus/src/commands/build.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import {handleBrokenLinks} from '../server/brokenLinks';
2020
import {BuildCLIOptions, Props} from '@docusaurus/types';
2121
import createClientConfig from '../webpack/client';
2222
import createServerConfig from '../webpack/server';
23-
import {compile, applyConfigureWebpack} from '../webpack/utils';
23+
import {
24+
compile,
25+
applyConfigureWebpack,
26+
applyConfigurePostCss,
27+
} from '../webpack/utils';
2428
import CleanWebpackPlugin from '../webpack/plugins/CleanWebpackPlugin';
2529
import {loadI18n} from '../server/i18n';
2630
import {mapAsyncSequencial} from '@docusaurus/utils';
@@ -166,24 +170,27 @@ async function buildLocale({
166170
});
167171
}
168172

169-
// Plugin Lifecycle - configureWebpack.
173+
// Plugin Lifecycle - configureWebpack and configurePostCss.
170174
plugins.forEach((plugin) => {
171-
const {configureWebpack} = plugin;
172-
if (!configureWebpack) {
173-
return;
175+
const {configureWebpack, configurePostCss} = plugin;
176+
177+
if (configurePostCss) {
178+
clientConfig = applyConfigurePostCss(configurePostCss, clientConfig);
174179
}
175180

176-
clientConfig = applyConfigureWebpack(
177-
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
178-
clientConfig,
179-
false,
180-
);
181+
if (configureWebpack) {
182+
clientConfig = applyConfigureWebpack(
183+
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
184+
clientConfig,
185+
false,
186+
);
181187

182-
serverConfig = applyConfigureWebpack(
183-
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
184-
serverConfig,
185-
true,
186-
);
188+
serverConfig = applyConfigureWebpack(
189+
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
190+
serverConfig,
191+
true,
192+
);
193+
}
187194
});
188195

189196
// Make sure generated client-manifest is cleaned first so we don't reuse

packages/docusaurus/src/commands/start.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import {load} from '../server';
2424
import {StartCLIOptions} from '@docusaurus/types';
2525
import {CONFIG_FILE_NAME, STATIC_DIR_NAME} from '../constants';
2626
import createClientConfig from '../webpack/client';
27-
import {applyConfigureWebpack, getHttpsConfig} from '../webpack/utils';
27+
import {
28+
applyConfigureWebpack,
29+
applyConfigurePostCss,
30+
getHttpsConfig,
31+
} from '../webpack/utils';
2832
import {getCLIOptionHost, getCLIOptionPort} from './commandUtils';
2933
import {getTranslationsLocaleDirPath} from '../server/translations/translations';
3034

@@ -134,18 +138,21 @@ export default async function start(
134138
],
135139
});
136140

137-
// Plugin Lifecycle - configureWebpack.
141+
// Plugin Lifecycle - configureWebpack and configurePostCss.
138142
plugins.forEach((plugin) => {
139-
const {configureWebpack} = plugin;
140-
if (!configureWebpack) {
141-
return;
143+
const {configureWebpack, configurePostCss} = plugin;
144+
145+
if (configurePostCss) {
146+
config = applyConfigurePostCss(configurePostCss, config);
142147
}
143148

144-
config = applyConfigureWebpack(
145-
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
146-
config,
147-
false,
148-
);
149+
if (configureWebpack) {
150+
config = applyConfigureWebpack(
151+
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
152+
config,
153+
false,
154+
);
155+
}
149156
});
150157

151158
// https://webpack.js.org/configuration/dev-server

packages/docusaurus/src/webpack/__tests__/utils.test.ts

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import {
1212
} from 'webpack';
1313
import path from 'path';
1414

15-
import {applyConfigureWebpack, getFileLoaderUtils} from '../utils';
15+
import {
16+
applyConfigureWebpack,
17+
applyConfigurePostCss,
18+
getFileLoaderUtils,
19+
} from '../utils';
1620
import {
1721
ConfigureWebpackFn,
1822
ConfigureWebpackFnMergeStrategy,
@@ -148,3 +152,123 @@ describe('getFileLoaderUtils()', () => {
148152
);
149153
});
150154
});
155+
156+
describe('extending PostCSS', () => {
157+
test('user plugin should be appended in PostCSS loader', () => {
158+
let webpackConfig: Configuration = {
159+
output: {
160+
path: __dirname,
161+
filename: 'bundle.js',
162+
},
163+
module: {
164+
rules: [
165+
{
166+
test: 'any',
167+
use: [
168+
{
169+
loader: 'some-loader-1',
170+
options: {},
171+
},
172+
{
173+
loader: 'some-loader-2',
174+
options: {},
175+
},
176+
{
177+
loader: 'postcss-loader-1',
178+
options: {
179+
postcssOptions: {
180+
plugins: [['default-postcss-loader-1-plugin']],
181+
},
182+
},
183+
},
184+
{
185+
loader: 'some-loader-3',
186+
options: {},
187+
},
188+
],
189+
},
190+
{
191+
test: '2nd-test',
192+
use: [
193+
{
194+
loader: 'postcss-loader-2',
195+
options: {
196+
postcssOptions: {
197+
plugins: [['default-postcss-loader-2-plugin']],
198+
},
199+
},
200+
},
201+
],
202+
},
203+
],
204+
},
205+
};
206+
207+
function createFakePlugin(name: string) {
208+
return [name, {}];
209+
}
210+
211+
// Run multiple times: ensure last run does not override previous runs
212+
webpackConfig = applyConfigurePostCss((postCssOptions) => {
213+
return {
214+
...postCssOptions,
215+
plugins: [
216+
...postCssOptions.plugins,
217+
createFakePlugin('postcss-plugin-1'),
218+
],
219+
};
220+
}, webpackConfig);
221+
222+
webpackConfig = applyConfigurePostCss((postCssOptions) => {
223+
return {
224+
...postCssOptions,
225+
plugins: [
226+
createFakePlugin('postcss-plugin-2'),
227+
...postCssOptions.plugins,
228+
],
229+
};
230+
}, webpackConfig);
231+
232+
webpackConfig = applyConfigurePostCss((postCssOptions) => {
233+
return {
234+
...postCssOptions,
235+
plugins: [
236+
...postCssOptions.plugins,
237+
createFakePlugin('postcss-plugin-3'),
238+
],
239+
};
240+
}, webpackConfig);
241+
242+
// @ts-expect-error: relax type
243+
const postCssLoader1 = webpackConfig.module?.rules[0].use[2];
244+
expect(postCssLoader1.loader).toEqual('postcss-loader-1');
245+
246+
const pluginNames1 = postCssLoader1.options.postcssOptions.plugins.map(
247+
// @ts-expect-error: relax type
248+
(p: unknown) => p[0],
249+
);
250+
expect(pluginNames1).toHaveLength(4);
251+
expect(pluginNames1).toEqual([
252+
'postcss-plugin-2',
253+
'default-postcss-loader-1-plugin',
254+
'postcss-plugin-1',
255+
'postcss-plugin-3',
256+
]);
257+
258+
// @ts-expect-error: relax type
259+
const postCssLoader2 = webpackConfig.module?.rules[1].use[0];
260+
expect(postCssLoader2.loader).toEqual('postcss-loader-2');
261+
262+
const pluginNames2 = postCssLoader2.options.postcssOptions.plugins.map(
263+
// @ts-expect-error: relax type
264+
(p: unknown) => p[0],
265+
);
266+
expect(pluginNames2).toHaveLength(4);
267+
expect(pluginNames2).toEqual([
268+
'postcss-plugin-2',
269+
'default-postcss-loader-2-plugin',
270+
'postcss-plugin-1',
271+
'postcss-plugin-3',
272+
]);
273+
});
274+
});

packages/docusaurus/src/webpack/utils.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import merge from 'webpack-merge';
1111
import webpack, {
1212
Configuration,
1313
Loader,
14+
NewLoader,
1415
Plugin,
1516
RuleSetRule,
1617
Stats,
@@ -23,7 +24,7 @@ import path from 'path';
2324
import crypto from 'crypto';
2425
import chalk from 'chalk';
2526
import {TransformOptions} from '@babel/core';
26-
import {ConfigureWebpackFn} from '@docusaurus/types';
27+
import {ConfigureWebpackFn, ConfigurePostCssFn} from '@docusaurus/types';
2728
import CssNanoPreset from '@docusaurus/cssnano-preset';
2829
import {version as cacheLoaderVersion} from 'cache-loader/package.json';
2930
import {BABEL_CONFIG_FILE_NAME, STATIC_ASSETS_DIR_NAME} from '../constants';
@@ -175,6 +176,31 @@ export function applyConfigureWebpack(
175176
return config;
176177
}
177178

179+
export function applyConfigurePostCss(
180+
configurePostCss: NonNullable<ConfigurePostCssFn>,
181+
config: Configuration,
182+
): Configuration {
183+
type LocalPostCSSLoader = Loader & {options: {postcssOptions: any}};
184+
185+
function isPostCssLoader(loader: Loader): loader is LocalPostCSSLoader {
186+
// TODO not ideal heuristic but good enough for our usecase?
187+
return !!(loader as any)?.options?.postcssOptions;
188+
}
189+
190+
// Does not handle all edge cases, but good enough for now
191+
config.module?.rules.map((rule) => {
192+
for (const loader of rule.use as NewLoader[]) {
193+
if (isPostCssLoader(loader)) {
194+
loader.options.postcssOptions = configurePostCss(
195+
loader.options.postcssOptions,
196+
);
197+
}
198+
}
199+
});
200+
201+
return config;
202+
}
203+
178204
// See https://webpack.js.org/configuration/stats/#statswarningsfilter
179205
// @slorber: note sure why we have to re-implement this logic
180206
// just know that legacy had this only partially implemented, so completed it

website/docs/lifecycle-apis.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,45 @@ module.exports = function (context, options) {
346346

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

349+
## `configurePostCss(options)`
350+
351+
Modifies [`postcssOptions` of `postcss-loader`](https://webpack.js.org/loaders/postcss-loader/#postcssoptions) during the generation of the client bundle.
352+
353+
Should return the mutated `postcssOptions`.
354+
355+
By default, `postcssOptions` looks like this:
356+
357+
```js
358+
const postcssOptions = {
359+
ident: 'postcss',
360+
plugins: [
361+
require('postcss-preset-env')({
362+
autoprefixer: {
363+
flexbox: 'no-2009',
364+
},
365+
stage: 4,
366+
}),
367+
],
368+
};
369+
```
370+
371+
Example:
372+
373+
```js title="docusaurus-plugin/src/index.js"
374+
module.exports = function (context, options) {
375+
return {
376+
name: 'docusaurus-plugin',
377+
// highlight-start
378+
configurePostCss(postcssOptions) {
379+
// Appends new PostCSS plugin.
380+
postcssOptions.plugins.push(require('postcss-import'));
381+
return postcssOptions;
382+
},
383+
// highlight-end
384+
};
385+
};
386+
```
387+
349388
## `postBuild(props)`
350389

351390
Called when a (production) build finishes.

0 commit comments

Comments
 (0)