Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(v2): allow extend PostCSS config #4185

Merged
merged 2 commits into from
Feb 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/docusaurus-types/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand All @@ -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[];
Expand Down Expand Up @@ -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>;

Expand Down
37 changes: 22 additions & 15 deletions packages/docusaurus/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
27 changes: 17 additions & 10 deletions packages/docusaurus/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
Expand Down
126 changes: 125 additions & 1 deletion packages/docusaurus/src/webpack/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
]);
});
});
28 changes: 27 additions & 1 deletion packages/docusaurus/src/webpack/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import merge from 'webpack-merge';
import webpack, {
Configuration,
Loader,
NewLoader,
Plugin,
RuleSetRule,
Stats,
Expand All @@ -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';
Expand Down Expand Up @@ -175,6 +176,31 @@ export function applyConfigureWebpack(
return config;
}

export function applyConfigurePostCss(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was wondering if we need a new lifecycle for this case?

Maybe that should be part of configureWebpack? (as the post css loader is part of the webpack config?)

No strong opinions for now, just thinking out loud

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, PostCSS plugins can be added via configureWebpack hook, but for this we need to duplicate a lot Docusaurus core code related to webpack, for example from this comment:

image

(Since this hook should return a valid webpack config, which will be merged with the default config).

Thus, configurePostCss is a handy and useful shortcut that you can use to quickly add the required PostCSS plugins. Or do you know a better way to achieve this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @lex111 , understand, and it will be useful for the upcoming tailwind theme too

What I see is that Gatsby does something similar with a plugin: https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-plugin-postcss/src/gatsby-node.js

And next does not really have a plugin system, but allow a local postcss.config.js and will support tailwind postcss enhancements in core soon: vercel/next.js#20030.

That does not look viable for our usecase so the most convenient is indeed a new lifecycle as themes can enhance the existing config.

Going to finish your implementation and merge, I suspect there is a little bug for which I want to have a test, and will also try to make the loader/option lookup more robust

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,
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI this configurePostCss should be called inside the loop.

Fixed a bug where having multiple applyConfigurePostCss would override each others (ie if a user would add his own configurePostCss, it would override the configurePostCss provided by the classic theme and RTL would fail)

Also find that looking for the presence of postcssOptions is better than the json stringify

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent fix, thanks!

}
}
});

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
Expand Down
39 changes: 39 additions & 0 deletions website/docs/lifecycle-apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down