From 433f1dc014ec32feac9207651c37c64cbf746f72 Mon Sep 17 00:00:00 2001 From: Alexey Pyltsyn Date: Sat, 6 Feb 2021 22:20:16 +0300 Subject: [PATCH 1/2] feat(v2): allow extend PostCSS config --- packages/docusaurus-types/src/index.d.ts | 2 + packages/docusaurus/src/commands/build.ts | 37 +++++++++------ packages/docusaurus/src/commands/start.ts | 27 +++++++---- .../src/webpack/__tests__/utils.test.ts | 47 ++++++++++++++++++- packages/docusaurus/src/webpack/utils.ts | 30 +++++++++++- website/docs/lifecycle-apis.md | 35 ++++++++++++++ 6 files changed, 151 insertions(+), 27 deletions(-) diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 28e0906a127e..63e8e46055f5 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -220,6 +220,7 @@ export interface Plugin { isServer: boolean, utils: ConfigureWebpackUtils, ): Configuration & {mergeStrategy?: ConfigureWebpackFnMergeStrategy}; + configurePostCss?(options: {[name: string]: any}): Configuration; getThemePath?(): string; getTypeScriptThemePath?(): string; getPathsToWatch?(): string[]; @@ -253,6 +254,7 @@ export interface Plugin { export type ConfigureWebpackFn = Plugin['configureWebpack']; export type ConfigureWebpackFnMergeStrategy = Record; +export type ConfigurePostCssFn = Plugin['configurePostCss']; export type PluginOptions = {id?: string} & Record; diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index 34b1557535c6..0ec5f13cfa2d 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -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 diff --git a/packages/docusaurus/src/commands/start.ts b/packages/docusaurus/src/commands/start.ts index 47af0df45170..40a023e0cc57 100644 --- a/packages/docusaurus/src/commands/start.ts +++ b/packages/docusaurus/src/commands/start.ts @@ -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 diff --git a/packages/docusaurus/src/webpack/__tests__/utils.test.ts b/packages/docusaurus/src/webpack/__tests__/utils.test.ts index 16385d022a60..82de0267b91b 100644 --- a/packages/docusaurus/src/webpack/__tests__/utils.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/utils.test.ts @@ -12,10 +12,16 @@ import { } from 'webpack'; import path from 'path'; -import {applyConfigureWebpack, getFileLoaderUtils} from '../utils'; +import { + applyConfigureWebpack, + applyConfigurePostCss, + getFileLoaderUtils, + getStyleLoaders, +} from '../utils'; import { ConfigureWebpackFn, ConfigureWebpackFnMergeStrategy, + ConfigurePostCssFn, } from '@docusaurus/types'; describe('extending generated webpack config', () => { @@ -148,3 +154,42 @@ describe('getFileLoaderUtils()', () => { ); }); }); + +describe('extending PostCSS', () => { + test('user plugin should be appended in PostCSS loader', () => { + let config: Configuration = { + output: { + path: __dirname, + filename: 'bundle.js', + }, + module: { + rules: [ + { + test: /\.css$/, + use: getStyleLoaders(false), + }, + ], + }, + }; + const postCssPlugin = jest.fn(() => { + return { + postcssPlugin: 'appended-plugin', + }; + }); + const configurePostCss: ConfigurePostCssFn = (postCssConfig) => { + postCssConfig.plugins.push(postCssPlugin()); + return postCssConfig; + }; + + config = applyConfigurePostCss(configurePostCss, config); + + const postCssLoader = config.module.rules[0].use.slice(-1)[0]; + const postCssPlugins = postCssLoader.options.postcssOptions.plugins.map( + (plugin) => { + return plugin.postcssPlugin; + }, + ); + + expect(postCssPlugins).toContain('appended-plugin'); + }); +}); diff --git a/packages/docusaurus/src/webpack/utils.ts b/packages/docusaurus/src/webpack/utils.ts index 726fd21a187f..a99b5c724d46 100644 --- a/packages/docusaurus/src/webpack/utils.ts +++ b/packages/docusaurus/src/webpack/utils.ts @@ -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,33 @@ export function applyConfigureWebpack( return config; } +export function applyConfigurePostCss( + configurePostCss: ConfigurePostCssFn, + config: Configuration, +): Configuration { + const isPostCssLoader = (loader) => + JSON.stringify(loader).includes('postcss-loader'); + const postCssLoader = getStyleLoaders(false).find((loader) => + isPostCssLoader(loader), + ) as NewLoader; + const mutatedPostCssOptions = configurePostCss!( + postCssLoader?.options?.postcssOptions, + ); + + config.module?.rules + .filter((rule) => rule.test!.toString().includes('.css')) + .forEach((rule) => { + for (const loader of rule.use as NewLoader[]) { + if (isPostCssLoader(loader)) { + console.log(1, loader); + loader.options!.postcssOptions = mutatedPostCssOptions; + } + } + }); + + 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 diff --git a/website/docs/lifecycle-apis.md b/website/docs/lifecycle-apis.md index 9b6caec277f9..325a3fe193a6 100644 --- a/website/docs/lifecycle-apis.md +++ b/website/docs/lifecycle-apis.md @@ -346,6 +346,41 @@ 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 generating client bundle. Should return mutated options. + +By default, `postcssOptions` looks like this: + +```js +postcssOptions: { + ident: 'postcss', + plugins: [ + require('postcss-preset-env')({ + autoprefixer: { + flexbox: 'no-2009', + }, + stage: 4, + }), + ], +}, +``` + +Example: + +```js {4-11} title="docusaurus-plugin/src/index.js" +module.exports = function (context, options) { + return { + name: 'docusaurus-plugin', + configurePostCss(options) { + // Appends new PostCSS plugin. + options.plugins.push(require('postcss-import')); + return options; + }, + }; +}; +``` + ## `postBuild(props)` Called when a (production) build finishes. From 10e843c8f1b34aeb1585d52e6393aa5425f0bfb8 Mon Sep 17 00:00:00 2001 From: slorber Date: Tue, 9 Feb 2021 19:37:21 +0100 Subject: [PATCH 2/2] polish the configurePostCss system --- packages/docusaurus-types/src/index.d.ts | 5 +- .../src/webpack/__tests__/utils.test.ts | 117 +++++++++++++++--- packages/docusaurus/src/webpack/utils.ts | 34 +++-- website/docs/lifecycle-apis.md | 18 +-- 4 files changed, 129 insertions(+), 45 deletions(-) diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 63e8e46055f5..4602f2e999b5 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -199,6 +199,9 @@ export type AllContent = Record< > >; +// TODO improve type (not exposed by postcss-loader) +export type PostCssOptions = Record & {plugins: any[]}; + export interface Plugin { name: string; loadContent?(): Promise; @@ -220,7 +223,7 @@ export interface Plugin { isServer: boolean, utils: ConfigureWebpackUtils, ): Configuration & {mergeStrategy?: ConfigureWebpackFnMergeStrategy}; - configurePostCss?(options: {[name: string]: any}): Configuration; + configurePostCss?(options: PostCssOptions): PostCssOptions; getThemePath?(): string; getTypeScriptThemePath?(): string; getPathsToWatch?(): string[]; diff --git a/packages/docusaurus/src/webpack/__tests__/utils.test.ts b/packages/docusaurus/src/webpack/__tests__/utils.test.ts index 82de0267b91b..4cef4e213866 100644 --- a/packages/docusaurus/src/webpack/__tests__/utils.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/utils.test.ts @@ -16,12 +16,10 @@ import { applyConfigureWebpack, applyConfigurePostCss, getFileLoaderUtils, - getStyleLoaders, } from '../utils'; import { ConfigureWebpackFn, ConfigureWebpackFnMergeStrategy, - ConfigurePostCssFn, } from '@docusaurus/types'; describe('extending generated webpack config', () => { @@ -157,7 +155,7 @@ describe('getFileLoaderUtils()', () => { describe('extending PostCSS', () => { test('user plugin should be appended in PostCSS loader', () => { - let config: Configuration = { + let webpackConfig: Configuration = { output: { path: __dirname, filename: 'bundle.js', @@ -165,31 +163,112 @@ describe('extending PostCSS', () => { module: { rules: [ { - test: /\.css$/, - use: getStyleLoaders(false), + 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']], + }, + }, + }, + ], }, ], }, }; - const postCssPlugin = jest.fn(() => { + + function createFakePlugin(name: string) { + return [name, {}]; + } + + // Run multiple times: ensure last run does not override previous runs + webpackConfig = applyConfigurePostCss((postCssOptions) => { return { - postcssPlugin: 'appended-plugin', + ...postCssOptions, + plugins: [ + ...postCssOptions.plugins, + createFakePlugin('postcss-plugin-1'), + ], }; - }); - const configurePostCss: ConfigurePostCssFn = (postCssConfig) => { - postCssConfig.plugins.push(postCssPlugin()); - return postCssConfig; - }; + }, webpackConfig); - config = applyConfigurePostCss(configurePostCss, config); + webpackConfig = applyConfigurePostCss((postCssOptions) => { + return { + ...postCssOptions, + plugins: [ + createFakePlugin('postcss-plugin-2'), + ...postCssOptions.plugins, + ], + }; + }, webpackConfig); - const postCssLoader = config.module.rules[0].use.slice(-1)[0]; - const postCssPlugins = postCssLoader.options.postcssOptions.plugins.map( - (plugin) => { - return plugin.postcssPlugin; - }, + 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', + ]); - expect(postCssPlugins).toContain('appended-plugin'); + // @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', + ]); }); }); diff --git a/packages/docusaurus/src/webpack/utils.ts b/packages/docusaurus/src/webpack/utils.ts index a99b5c724d46..5ff76cd1cc5c 100644 --- a/packages/docusaurus/src/webpack/utils.ts +++ b/packages/docusaurus/src/webpack/utils.ts @@ -177,28 +177,26 @@ export function applyConfigureWebpack( } export function applyConfigurePostCss( - configurePostCss: ConfigurePostCssFn, + configurePostCss: NonNullable, config: Configuration, ): Configuration { - const isPostCssLoader = (loader) => - JSON.stringify(loader).includes('postcss-loader'); - const postCssLoader = getStyleLoaders(false).find((loader) => - isPostCssLoader(loader), - ) as NewLoader; - const mutatedPostCssOptions = configurePostCss!( - postCssLoader?.options?.postcssOptions, - ); + type LocalPostCSSLoader = Loader & {options: {postcssOptions: any}}; - config.module?.rules - .filter((rule) => rule.test!.toString().includes('.css')) - .forEach((rule) => { - for (const loader of rule.use as NewLoader[]) { - if (isPostCssLoader(loader)) { - console.log(1, loader); - loader.options!.postcssOptions = mutatedPostCssOptions; - } + 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; } diff --git a/website/docs/lifecycle-apis.md b/website/docs/lifecycle-apis.md index 325a3fe193a6..c41b3ac9143a 100644 --- a/website/docs/lifecycle-apis.md +++ b/website/docs/lifecycle-apis.md @@ -348,12 +348,14 @@ Read the [webpack-merge strategy doc](https://github.com/survivejs/webpack-merge ## `configurePostCss(options)` -Modifies [`postcssOptions` of `postcss-loader`](https://webpack.js.org/loaders/postcss-loader/#postcssoptions) during generating client bundle. Should return mutated 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 -postcssOptions: { +const postcssOptions = { ident: 'postcss', plugins: [ require('postcss-preset-env')({ @@ -363,20 +365,22 @@ postcssOptions: { stage: 4, }), ], -}, +}; ``` Example: -```js {4-11} title="docusaurus-plugin/src/index.js" +```js title="docusaurus-plugin/src/index.js" module.exports = function (context, options) { return { name: 'docusaurus-plugin', - configurePostCss(options) { + // highlight-start + configurePostCss(postcssOptions) { // Appends new PostCSS plugin. - options.plugins.push(require('postcss-import')); - return options; + postcssOptions.plugins.push(require('postcss-import')); + return postcssOptions; }, + // highlight-end }; }; ```