mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-09 22:32:53 +02:00
feat(theme): new CSS cascade layers plugin + built-in v4.useCssCascadeLayers
future flag (#11142)
Co-authored-by: slorber <749374+slorber@users.noreply.github.com>
This commit is contained in:
parent
a301b24d64
commit
abd04a2b71
26 changed files with 894 additions and 0 deletions
3
packages/docusaurus-plugin-css-cascade-layers/.npmignore
Normal file
3
packages/docusaurus-plugin-css-cascade-layers/.npmignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
.tsbuildinfo*
|
||||
tsconfig*
|
||||
__tests__
|
7
packages/docusaurus-plugin-css-cascade-layers/README.md
Normal file
7
packages/docusaurus-plugin-css-cascade-layers/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# `@docusaurus/plugin-css-cascade-layers`
|
||||
|
||||
CSS Cascade Layer plugin for Docusaurus
|
||||
|
||||
## Usage
|
||||
|
||||
See [plugin-css-cascade-layers documentation](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-css-cascade-layers).
|
29
packages/docusaurus-plugin-css-cascade-layers/package.json
Normal file
29
packages/docusaurus-plugin-css-cascade-layers/package.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "@docusaurus/plugin-css-cascade-layers",
|
||||
"version": "3.7.0",
|
||||
"description": "CSS Cascade Layer plugin for Docusaurus.",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"watch": "tsc --build --watch"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/docusaurus.git",
|
||||
"directory": "packages/docusaurus-plugin-css-cascade-layers"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.7.0",
|
||||
"@docusaurus/types": "3.7.0",
|
||||
"@docusaurus/utils-validation": "3.7.0",
|
||||
"tslib": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* 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 {
|
||||
generateLayersDeclaration,
|
||||
findLayer,
|
||||
isValidLayerName,
|
||||
} from '../layers';
|
||||
import type {PluginOptions} from '../options';
|
||||
|
||||
describe('isValidLayerName', () => {
|
||||
it('accepts valid names', () => {
|
||||
expect(isValidLayerName('layer1')).toBe(true);
|
||||
expect(isValidLayerName('layer1.layer2')).toBe(true);
|
||||
expect(isValidLayerName('layer-1.layer_2.layer3')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects layer with coma', () => {
|
||||
expect(isValidLayerName('lay,er1')).toBe(false);
|
||||
});
|
||||
it('rejects layer with space', () => {
|
||||
expect(isValidLayerName('lay er1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateLayersDeclaration', () => {
|
||||
it('for list of layers', () => {
|
||||
expect(generateLayersDeclaration(['layer1', 'layer2'])).toBe(
|
||||
'@layer layer1, layer2;',
|
||||
);
|
||||
});
|
||||
|
||||
it('for empty list of layers', () => {
|
||||
// Not useful to generate it, but still valid CSS anyway
|
||||
expect(generateLayersDeclaration([])).toBe('@layer ;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findLayer', () => {
|
||||
const inputFilePath = 'filePath';
|
||||
|
||||
function testFor(layers: PluginOptions['layers']) {
|
||||
return findLayer(inputFilePath, Object.entries(layers));
|
||||
}
|
||||
|
||||
it('for empty layers', () => {
|
||||
expect(testFor({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it('for single matching layer', () => {
|
||||
expect(testFor({layer: (filePath) => filePath === inputFilePath})).toBe(
|
||||
'layer',
|
||||
);
|
||||
});
|
||||
|
||||
it('for single non-matching layer', () => {
|
||||
expect(
|
||||
testFor({layer: (filePath) => filePath !== inputFilePath}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('for multiple matching layers', () => {
|
||||
expect(
|
||||
testFor({
|
||||
layer1: (filePath) => filePath === inputFilePath,
|
||||
layer2: (filePath) => filePath === inputFilePath,
|
||||
layer3: (filePath) => filePath === inputFilePath,
|
||||
}),
|
||||
).toBe('layer1');
|
||||
});
|
||||
|
||||
it('for multiple non-matching layers', () => {
|
||||
expect(
|
||||
testFor({
|
||||
layer1: (filePath) => filePath !== inputFilePath,
|
||||
layer2: (filePath) => filePath !== inputFilePath,
|
||||
layer3: (filePath) => filePath !== inputFilePath,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('for multiple mixed matching layers', () => {
|
||||
expect(
|
||||
testFor({
|
||||
layer1: (filePath) => filePath !== inputFilePath,
|
||||
layer2: (filePath) => filePath === inputFilePath,
|
||||
layer3: (filePath) => filePath !== inputFilePath,
|
||||
layer4: (filePath) => filePath === inputFilePath,
|
||||
}),
|
||||
).toBe('layer2');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* 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 {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||
import {
|
||||
validateOptions,
|
||||
type PluginOptions,
|
||||
type Options,
|
||||
DEFAULT_OPTIONS,
|
||||
} from '../options';
|
||||
import type {Validate} from '@docusaurus/types';
|
||||
|
||||
function testValidateOptions(options: Options) {
|
||||
return validateOptions({
|
||||
validate: normalizePluginOptions as Validate<Options, PluginOptions>,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
describe('validateOptions', () => {
|
||||
it('accepts undefined options', () => {
|
||||
// @ts-expect-error: should error
|
||||
expect(testValidateOptions(undefined)).toEqual(DEFAULT_OPTIONS);
|
||||
});
|
||||
|
||||
it('accepts empty options', () => {
|
||||
expect(testValidateOptions({})).toEqual(DEFAULT_OPTIONS);
|
||||
});
|
||||
|
||||
describe('layers', () => {
|
||||
it('accepts empty layers', () => {
|
||||
expect(testValidateOptions({layers: {}})).toEqual({
|
||||
...DEFAULT_OPTIONS,
|
||||
layers: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts undefined layers', () => {
|
||||
const config: Options = {
|
||||
layers: undefined,
|
||||
};
|
||||
expect(testValidateOptions(config)).toEqual(DEFAULT_OPTIONS);
|
||||
});
|
||||
|
||||
it('accepts custom layers', () => {
|
||||
const config: Options = {
|
||||
layers: {
|
||||
layer1: (filePath: string) => {
|
||||
return !!filePath;
|
||||
},
|
||||
layer2: (filePath: string) => {
|
||||
return !!filePath;
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(testValidateOptions(config)).toEqual({
|
||||
...DEFAULT_OPTIONS,
|
||||
layers: config.layers,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects layer with bad name', () => {
|
||||
const config: Options = {
|
||||
layers: {
|
||||
'layer 1': (filePath) => !!filePath,
|
||||
},
|
||||
};
|
||||
expect(() =>
|
||||
testValidateOptions(config),
|
||||
).toThrowErrorMatchingInlineSnapshot(`""layers.layer 1" is not allowed"`);
|
||||
});
|
||||
|
||||
it('rejects layer with bad value', () => {
|
||||
const config: Options = {
|
||||
layers: {
|
||||
// @ts-expect-error: should error
|
||||
layer1: 'bad value',
|
||||
},
|
||||
};
|
||||
expect(() =>
|
||||
testValidateOptions(config),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`""layers.layer1" must be of type function"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects layer with bad function arity', () => {
|
||||
const config: Options = {
|
||||
layers: {
|
||||
// @ts-expect-error: should error
|
||||
layer1: () => {},
|
||||
},
|
||||
};
|
||||
expect(() =>
|
||||
testValidateOptions(config),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`""layers.layer1" must have an arity of 1"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
68
packages/docusaurus-plugin-css-cascade-layers/src/index.ts
Normal file
68
packages/docusaurus-plugin-css-cascade-layers/src/index.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* 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 path from 'path';
|
||||
import {PostCssPluginWrapInLayer} from './postCssPlugin';
|
||||
import {generateLayersDeclaration} from './layers';
|
||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||
import type {PluginOptions, Options} from './options';
|
||||
|
||||
const PluginName = 'docusaurus-plugin-css-cascade-layers';
|
||||
|
||||
const LayersDeclarationModule = 'layers.css';
|
||||
|
||||
function getLayersDeclarationPath(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
) {
|
||||
const {generatedFilesDir} = context;
|
||||
const pluginId = options.id;
|
||||
if (pluginId !== 'default') {
|
||||
// Since it's only possible to declare a single layer order
|
||||
// using this plugin twice doesn't really make sense
|
||||
throw new Error(
|
||||
'The CSS Cascade Layers plugin does not support multiple instances.',
|
||||
);
|
||||
}
|
||||
return path.join(
|
||||
generatedFilesDir,
|
||||
PluginName,
|
||||
pluginId,
|
||||
LayersDeclarationModule,
|
||||
);
|
||||
}
|
||||
|
||||
export default function pluginCssCascadeLayers(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin | null {
|
||||
const layersDeclarationPath = getLayersDeclarationPath(context, options);
|
||||
|
||||
return {
|
||||
name: PluginName,
|
||||
|
||||
getClientModules() {
|
||||
return [layersDeclarationPath];
|
||||
},
|
||||
|
||||
async contentLoaded({actions}) {
|
||||
await actions.createData(
|
||||
LayersDeclarationModule,
|
||||
generateLayersDeclaration(Object.keys(options.layers)),
|
||||
);
|
||||
},
|
||||
|
||||
configurePostCss(postCssOptions) {
|
||||
postCssOptions.plugins.push(PostCssPluginWrapInLayer(options));
|
||||
return postCssOptions;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export {validateOptions} from './options';
|
||||
|
||||
export type {PluginOptions, Options};
|
27
packages/docusaurus-plugin-css-cascade-layers/src/layers.ts
Normal file
27
packages/docusaurus-plugin-css-cascade-layers/src/layers.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type LayerEntry = [string, (filePath: string) => boolean];
|
||||
|
||||
export function isValidLayerName(layer: string): boolean {
|
||||
// TODO improve validation rule to match spec, not high priority
|
||||
return !layer.includes(',') && !layer.includes(' ');
|
||||
}
|
||||
|
||||
export function generateLayersDeclaration(layers: string[]): string {
|
||||
return `@layer ${layers.join(', ')};`;
|
||||
}
|
||||
|
||||
export function findLayer(
|
||||
filePath: string,
|
||||
layers: LayerEntry[],
|
||||
): string | undefined {
|
||||
// Using find() => layers order matter
|
||||
// The first layer that matches is used in priority even if others match too
|
||||
const layerEntry = layers.find((layer) => layer[1](filePath));
|
||||
return layerEntry?.[0]; // return layer name
|
||||
}
|
87
packages/docusaurus-plugin-css-cascade-layers/src/options.ts
Normal file
87
packages/docusaurus-plugin-css-cascade-layers/src/options.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
/**
|
||||
* 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 {Joi} from '@docusaurus/utils-validation';
|
||||
import {isValidLayerName} from './layers';
|
||||
import type {OptionValidationContext} from '@docusaurus/types';
|
||||
|
||||
export type PluginOptions = {
|
||||
id: string; // plugin id
|
||||
layers: Record<string, (filePath: string) => boolean>;
|
||||
};
|
||||
|
||||
export type Options = {
|
||||
layers?: PluginOptions['layers'];
|
||||
};
|
||||
|
||||
// Not ideal to compute layers using "filePath.includes()"
|
||||
// But this is mostly temporary until we add first-class layers everywhere
|
||||
function layerFor(...params: string[]) {
|
||||
return (filePath: string) => params.some((p) => filePath.includes(p));
|
||||
}
|
||||
|
||||
// Object order matters, it defines the layer order
|
||||
export const DEFAULT_LAYERS: PluginOptions['layers'] = {
|
||||
'docusaurus.infima': layerFor('node_modules/infima/dist'),
|
||||
'docusaurus.theme-common': layerFor(
|
||||
'packages/docusaurus-theme-common/lib',
|
||||
'node_modules/@docusaurus/theme-common/lib',
|
||||
),
|
||||
'docusaurus.theme-classic': layerFor(
|
||||
'packages/docusaurus-theme-classic/lib',
|
||||
'node_modules/@docusaurus/theme-classic/lib',
|
||||
),
|
||||
'docusaurus.core': layerFor(
|
||||
'packages/docusaurus/lib',
|
||||
'node_modules/@docusaurus/core/lib',
|
||||
),
|
||||
'docusaurus.plugin-debug': layerFor(
|
||||
'packages/docusaurus-plugin-debug/lib',
|
||||
'node_modules/@docusaurus/plugin-debug/lib',
|
||||
),
|
||||
'docusaurus.theme-mermaid': layerFor(
|
||||
'packages/docusaurus-theme-mermaid/lib',
|
||||
'node_modules/@docusaurus/theme-mermaid/lib',
|
||||
),
|
||||
'docusaurus.theme-live-codeblock': layerFor(
|
||||
'packages/docusaurus-theme-live-codeblock/lib',
|
||||
'node_modules/@docusaurus/theme-live-codeblock/lib',
|
||||
),
|
||||
'docusaurus.theme-search-algolia.docsearch': layerFor(
|
||||
'node_modules/@docsearch/css/dist',
|
||||
),
|
||||
'docusaurus.theme-search-algolia': layerFor(
|
||||
'packages/docusaurus-theme-search-algolia/lib',
|
||||
'node_modules/@docusaurus/theme-search-algolia/lib',
|
||||
),
|
||||
// docusaurus.website layer ? (declare it, even if empty?)
|
||||
};
|
||||
|
||||
export const DEFAULT_OPTIONS: Partial<PluginOptions> = {
|
||||
id: 'default',
|
||||
layers: DEFAULT_LAYERS,
|
||||
};
|
||||
|
||||
const pluginOptionsSchema = Joi.object<PluginOptions>({
|
||||
layers: Joi.object()
|
||||
.pattern(
|
||||
Joi.custom((val, helpers) => {
|
||||
if (!isValidLayerName(val)) {
|
||||
return helpers.error('any.invalid');
|
||||
}
|
||||
return val;
|
||||
}),
|
||||
Joi.function().arity(1).required(),
|
||||
)
|
||||
.default(DEFAULT_LAYERS),
|
||||
});
|
||||
|
||||
export function validateOptions({
|
||||
validate,
|
||||
options,
|
||||
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
|
||||
return validate(pluginOptionsSchema, options);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* 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 {findLayer} from './layers';
|
||||
import type {Root, PluginCreator} from 'postcss';
|
||||
import type {PluginOptions} from './options';
|
||||
|
||||
function wrapCssRootInLayer(root: Root, layer: string): void {
|
||||
const rootBefore = root.clone();
|
||||
root.removeAll();
|
||||
root.append({
|
||||
type: 'atrule',
|
||||
name: 'layer',
|
||||
params: layer,
|
||||
nodes: rootBefore.nodes,
|
||||
});
|
||||
}
|
||||
|
||||
export const PostCssPluginWrapInLayer: PluginCreator<{
|
||||
layers: PluginOptions['layers'];
|
||||
}> = (options) => {
|
||||
if (!options) {
|
||||
throw new Error('PostCssPluginWrapInLayer options are mandatory');
|
||||
}
|
||||
const layers = Object.entries(options.layers);
|
||||
return {
|
||||
postcssPlugin: 'postcss-wrap-in-layer',
|
||||
Once(root) {
|
||||
const filePath = root.source?.input.file;
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
const layer = findLayer(filePath, layers);
|
||||
if (layer) {
|
||||
wrapCssRootInLayer(root, layer);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
PostCssPluginWrapInLayer.postcss = true;
|
8
packages/docusaurus-plugin-css-cascade-layers/src/types.d.ts
vendored
Normal file
8
packages/docusaurus-plugin-css-cascade-layers/src/types.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/// <reference types="@docusaurus/module-type-aliases" />
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["**/__tests__/**"]
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.7.0",
|
||||
"@docusaurus/plugin-css-cascade-layers": "3.7.0",
|
||||
"@docusaurus/plugin-content-blog": "3.7.0",
|
||||
"@docusaurus/plugin-content-docs": "3.7.0",
|
||||
"@docusaurus/plugin-content-pages": "3.7.0",
|
||||
|
|
|
@ -62,6 +62,13 @@ export default function preset(
|
|||
}
|
||||
|
||||
const plugins: PluginConfig[] = [];
|
||||
|
||||
// TODO Docusaurus v4: temporary due to the opt-in flag
|
||||
// In v4 we'd like to use layers everywhere natively
|
||||
if (siteConfig.future.v4.useCssCascadeLayers) {
|
||||
plugins.push(makePluginConfig('@docusaurus/plugin-css-cascade-layers'));
|
||||
}
|
||||
|
||||
if (docs !== false) {
|
||||
plugins.push(makePluginConfig('@docusaurus/plugin-content-docs', docs));
|
||||
}
|
||||
|
|
|
@ -13,3 +13,11 @@ Hide color mode toggle in small viewports
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Restore some Infima style that broke with CSS Cascade Layers
|
||||
See https://github.com/facebook/docusaurus/pull/11142
|
||||
*/
|
||||
:global(.navbar__items--right) > :last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
|
1
packages/docusaurus-types/src/config.d.ts
vendored
1
packages/docusaurus-types/src/config.d.ts
vendored
|
@ -136,6 +136,7 @@ export type FasterConfig = {
|
|||
|
||||
export type FutureV4Config = {
|
||||
removeLegacyPostBuildHeadAttribute: boolean;
|
||||
useCssCascadeLayers: boolean;
|
||||
};
|
||||
|
||||
export type FutureConfig = {
|
||||
|
|
|
@ -25,6 +25,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
|
|||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
"useCssCascadeLayers": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
|
@ -99,6 +100,7 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
|
|||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
"useCssCascadeLayers": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
|
@ -173,6 +175,7 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
|
|||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
"useCssCascadeLayers": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
|
@ -247,6 +250,7 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
|
|||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
"useCssCascadeLayers": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
|
@ -321,6 +325,7 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
|
|||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
"useCssCascadeLayers": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
|
@ -395,6 +400,7 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
|
|||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
"useCssCascadeLayers": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
|
@ -469,6 +475,7 @@ exports[`loadSiteConfig website with valid async config 1`] = `
|
|||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
"useCssCascadeLayers": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
|
@ -545,6 +552,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
|
|||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
"useCssCascadeLayers": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
|
@ -621,6 +629,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
|
|||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
"useCssCascadeLayers": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
|
@ -700,6 +709,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
|
|||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
"useCssCascadeLayers": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
|
|
|
@ -99,6 +99,7 @@ exports[`load loads props for site with custom i18n path 1`] = `
|
|||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
"useCssCascadeLayers": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
|
|
|
@ -50,6 +50,7 @@ describe('normalizeConfig', () => {
|
|||
future: {
|
||||
v4: {
|
||||
removeLegacyPostBuildHeadAttribute: true,
|
||||
useCssCascadeLayers: true,
|
||||
},
|
||||
experimental_faster: {
|
||||
swcJsLoader: true,
|
||||
|
@ -754,6 +755,7 @@ describe('future', () => {
|
|||
const future: DocusaurusConfig['future'] = {
|
||||
v4: {
|
||||
removeLegacyPostBuildHeadAttribute: true,
|
||||
useCssCascadeLayers: true,
|
||||
},
|
||||
experimental_faster: {
|
||||
swcJsLoader: true,
|
||||
|
@ -1861,6 +1863,7 @@ describe('future', () => {
|
|||
it('accepts v4 - full', () => {
|
||||
const v4: FutureV4Config = {
|
||||
removeLegacyPostBuildHeadAttribute: true,
|
||||
useCssCascadeLayers: true,
|
||||
};
|
||||
expect(
|
||||
normalizeConfig({
|
||||
|
@ -1976,5 +1979,80 @@ describe('future', () => {
|
|||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCssCascadeLayers', () => {
|
||||
it('accepts - undefined', () => {
|
||||
const v4: Partial<FutureV4Config> = {
|
||||
useCssCascadeLayers: undefined,
|
||||
};
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {
|
||||
v4,
|
||||
},
|
||||
}),
|
||||
).toEqual(v4Containing({useCssCascadeLayers: false}));
|
||||
});
|
||||
|
||||
it('accepts - true', () => {
|
||||
const v4: Partial<FutureV4Config> = {
|
||||
useCssCascadeLayers: true,
|
||||
};
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {
|
||||
v4,
|
||||
},
|
||||
}),
|
||||
).toEqual(v4Containing({useCssCascadeLayers: true}));
|
||||
});
|
||||
|
||||
it('accepts - false', () => {
|
||||
const v4: Partial<FutureV4Config> = {
|
||||
useCssCascadeLayers: false,
|
||||
};
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {
|
||||
v4,
|
||||
},
|
||||
}),
|
||||
).toEqual(v4Containing({useCssCascadeLayers: false}));
|
||||
});
|
||||
|
||||
it('rejects - null', () => {
|
||||
const v4: Partial<FutureV4Config> = {
|
||||
// @ts-expect-error: invalid
|
||||
useCssCascadeLayers: 42,
|
||||
};
|
||||
expect(() =>
|
||||
normalizeConfig({
|
||||
future: {
|
||||
v4,
|
||||
},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
""future.v4.useCssCascadeLayers" must be a boolean
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('rejects - number', () => {
|
||||
const v4: Partial<FutureV4Config> = {
|
||||
// @ts-expect-error: invalid
|
||||
useCssCascadeLayers: 42,
|
||||
};
|
||||
expect(() =>
|
||||
normalizeConfig({
|
||||
future: {
|
||||
v4,
|
||||
},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
""future.v4.useCssCascadeLayers" must be a boolean
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -68,11 +68,13 @@ export const DEFAULT_FASTER_CONFIG_TRUE: FasterConfig = {
|
|||
|
||||
export const DEFAULT_FUTURE_V4_CONFIG: FutureV4Config = {
|
||||
removeLegacyPostBuildHeadAttribute: false,
|
||||
useCssCascadeLayers: false,
|
||||
};
|
||||
|
||||
// When using the "v4: true" shortcut
|
||||
export const DEFAULT_FUTURE_V4_CONFIG_TRUE: FutureV4Config = {
|
||||
removeLegacyPostBuildHeadAttribute: true,
|
||||
useCssCascadeLayers: true,
|
||||
};
|
||||
|
||||
export const DEFAULT_FUTURE_CONFIG: FutureConfig = {
|
||||
|
@ -270,6 +272,9 @@ const FUTURE_V4_SCHEMA = Joi.alternatives()
|
|||
removeLegacyPostBuildHeadAttribute: Joi.boolean().default(
|
||||
DEFAULT_FUTURE_V4_CONFIG.removeLegacyPostBuildHeadAttribute,
|
||||
),
|
||||
useCssCascadeLayers: Joi.boolean().default(
|
||||
DEFAULT_FUTURE_V4_CONFIG.useCssCascadeLayers,
|
||||
),
|
||||
}),
|
||||
Joi.boolean()
|
||||
.required()
|
||||
|
|
|
@ -109,6 +109,7 @@ Héctor
|
|||
héllô
|
||||
IANAD
|
||||
Infima
|
||||
infima
|
||||
inlines
|
||||
interactiveness
|
||||
Interpolatable
|
||||
|
|
|
@ -41,3 +41,4 @@ import Readme from "../README.mdx"
|
|||
- [Analytics](/tests/pages/analytics)
|
||||
- [History tests](/tests/pages/history-tests)
|
||||
- [Embeds](/tests/pages/embeds)
|
||||
- [Style Isolation tests](/tests/pages/style-isolation)
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.exampleContainer {
|
||||
border: solid thin;
|
||||
}
|
||||
|
||||
.isolated:not(#a#b) {
|
||||
&,
|
||||
* {
|
||||
@layer docusaurus {
|
||||
all: revert-layer;
|
||||
}
|
||||
/*
|
||||
Yes, unfortunately we need to revert sub-layers one by one
|
||||
See https://bsky.app/profile/sebastienlorber.com/post/3lpqzuxat6s2v
|
||||
*/
|
||||
@layer docusaurus.infima {
|
||||
all: revert-layer;
|
||||
}
|
||||
}
|
||||
}
|
174
website/_dogfooding/_pages tests/style-isolation/index.tsx
Normal file
174
website/_dogfooding/_pages tests/style-isolation/index.tsx
Normal file
|
@ -0,0 +1,174 @@
|
|||
/**
|
||||
* 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 React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
import Layout from '@theme/Layout';
|
||||
import Heading from '@theme/Heading';
|
||||
|
||||
import styles from './index.module.css';
|
||||
|
||||
/* eslint-disable @docusaurus/prefer-docusaurus-heading */
|
||||
|
||||
function ExampleContainer({
|
||||
isolated,
|
||||
children,
|
||||
}: {
|
||||
isolated?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
styles.exampleContainer,
|
||||
isolated ? styles.isolated : undefined,
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ExampleRow({name, children}: {name: string; children: ReactNode}) {
|
||||
return (
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
<td>
|
||||
<ExampleContainer>{children}</ExampleContainer>
|
||||
</td>
|
||||
<td>
|
||||
<ExampleContainer isolated>{children}</ExampleContainer>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function ExamplesTable() {
|
||||
return (
|
||||
<table className="table-auto border-collapse border border-gray-300">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Example</th>
|
||||
<th>Normal</th>
|
||||
<th>Isolated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ExampleRow name="h1">
|
||||
<h1>title</h1>
|
||||
</ExampleRow>
|
||||
|
||||
<ExampleRow name="p">
|
||||
<p>text</p>
|
||||
</ExampleRow>
|
||||
|
||||
<ExampleRow name="a">
|
||||
{/* eslint-disable-next-line */}
|
||||
<a href="https://example.com">link</a>
|
||||
</ExampleRow>
|
||||
|
||||
<ExampleRow name="code">
|
||||
<code>code</code>
|
||||
</ExampleRow>
|
||||
<ExampleRow name="pre > code">
|
||||
<pre>
|
||||
<code>code</code>
|
||||
</pre>
|
||||
</ExampleRow>
|
||||
|
||||
<ExampleRow name="blockquote">
|
||||
<blockquote>some text</blockquote>
|
||||
</ExampleRow>
|
||||
|
||||
<ExampleRow name="button">
|
||||
{/* eslint-disable-next-line */}
|
||||
<button>button</button>
|
||||
</ExampleRow>
|
||||
|
||||
<ExampleRow name="ul">
|
||||
<ul>
|
||||
<li>item1</li>
|
||||
<li>item2</li>
|
||||
</ul>
|
||||
</ExampleRow>
|
||||
|
||||
<ExampleRow name="ol">
|
||||
<ol>
|
||||
<li>item1</li>
|
||||
<li>item2</li>
|
||||
</ol>
|
||||
</ExampleRow>
|
||||
|
||||
<ExampleRow name="kbd">
|
||||
<kbd>kbd</kbd>
|
||||
</ExampleRow>
|
||||
|
||||
<ExampleRow name="shadow">
|
||||
<div className="shadow--tl">shadow (KO)</div>
|
||||
</ExampleRow>
|
||||
|
||||
<ExampleRow name="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Col1</th>
|
||||
<th>Col2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Cell1</td>
|
||||
<td>Cell2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cell3</td>
|
||||
<td>Cell3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ExampleRow>
|
||||
|
||||
<ExampleRow name="Infima button primary">
|
||||
{/* eslint-disable-next-line */}
|
||||
<button className="button button--primary">button</button>
|
||||
</ExampleRow>
|
||||
|
||||
<ExampleRow name="Infima alert danger">
|
||||
<div className="alert alert--danger">danger</div>
|
||||
</ExampleRow>
|
||||
<ExampleRow name="Infima badge success">
|
||||
<div className="badge badge--success">success</div>
|
||||
</ExampleRow>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StyleIsolation(): ReactNode {
|
||||
return (
|
||||
<Layout>
|
||||
<main
|
||||
style={{padding: 30}}
|
||||
className="markdown" // class added on purpose, creates extra pollution
|
||||
>
|
||||
<Heading as="h1">Style Isolation tests</Heading>
|
||||
|
||||
<p>
|
||||
This shows how to isolate your components from Docusaurus global
|
||||
styles. A workaround for{' '}
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://github.com/facebook/docusaurus/issues/6032">
|
||||
this issue
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<ExamplesTable />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
|
@ -199,6 +199,7 @@ export default {
|
|||
future: {
|
||||
v4: {
|
||||
removeLegacyPostBuildHeadAttribute: true,
|
||||
useCssCascadeLayers: true,
|
||||
},
|
||||
experimental_faster: {
|
||||
swcJsLoader: true,
|
||||
|
@ -221,6 +222,7 @@ export default {
|
|||
|
||||
- `v4`: Permits to opt-in for upcoming Docusaurus v4 breaking changes and features, to prepare your site in advance for this new version. Use `true` as a shorthand to enable all the flags.
|
||||
- [`removeLegacyPostBuildHeadAttribute`](https://github.com/facebook/docusaurus/pull/10435): Removes the legacy `plugin.postBuild({head})` API that prevents us from applying useful SSG optimizations ([explanations](https://github.com/facebook/docusaurus/pull/10850)).
|
||||
- [`useCssCascadeLayers`](https://github.com/facebook/docusaurus/pull/11142): This enables the [Docusaurus CSS Cascade Layers plugin](./plugins/plugin-css-cascade-layers.mdx) with pre-configured layers that we plan to apply by default for Docusaurus v4.
|
||||
- `experimental_faster`: An object containing feature flags to make the Docusaurus build faster. This requires adding the `@docusaurus/faster` package to your site's dependencies. Use `true` as a shorthand to enable all flags. Read more on the [Docusaurus Faster](https://github.com/facebook/docusaurus/issues/10556) issue. Available feature flags:
|
||||
- [`swcJsLoader`](https://github.com/facebook/docusaurus/pull/10435): Use [SWC](https://swc.rs/) to transpile JS (instead of [Babel](https://babeljs.io/)).
|
||||
- [`swcJsMinimizer`](https://github.com/facebook/docusaurus/pull/10441): Use [SWC](https://swc.rs/) to minify JS (instead of [Terser](https://github.com/terser/terser)).
|
||||
|
|
|
@ -31,3 +31,4 @@ These plugins will add a useful behavior to your Docusaurus site.
|
|||
- [@docusaurus/plugin-google-analytics](./plugin-google-analytics.mdx)
|
||||
- [@docusaurus/plugin-google-gtag](./plugin-google-gtag.mdx)
|
||||
- [@docusaurus/plugin-google-tag-manager](./plugin-google-tag-manager.mdx)
|
||||
- [@docusaurus/plugin-css-cascade-layers](./plugin-css-cascade-layers.mdx) u
|
||||
|
|
95
website/docs/api/plugins/plugin-css-cascade-layers.mdx
Normal file
95
website/docs/api/plugins/plugin-css-cascade-layers.mdx
Normal file
|
@ -0,0 +1,95 @@
|
|||
---
|
||||
sidebar_position: 9
|
||||
slug: /api/plugins/@docusaurus/plugin-css-cascade-layers
|
||||
---
|
||||
|
||||
# 📦 plugin-css-cascade-layers
|
||||
|
||||
import APITable from '@site/src/components/APITable';
|
||||
|
||||
:::caution Experimental
|
||||
|
||||
This plugin is mostly designed to be used internally by the classic preset through the [Docusaurus `future.v4.useCssCascadeLayers` flag](../docusaurus.config.js.mdx#future), although it can also be used as a standalone plugin. Please [let us know here](https://github.com/facebook/docusaurus/pull/11142) if you have a use case for it and help us design an API that makes sense for the future of Docusaurus.
|
||||
|
||||
:::
|
||||
|
||||
A plugin for wrapping CSS modules of your Docusaurus site in [CSS Cascade Layers](https://css-tricks.com/css-cascade-layers/). This modern CSS feature is widely supported by all browsers. It allows grouping CSS rules in layers of specificity and gives you more control over the CSS cascade.
|
||||
|
||||
Use this plugin to:
|
||||
|
||||
- apply a top-level `@layer myLayer { ... }` block rule around any CSS module, including un-layered third-party CSS.
|
||||
- define an explicit layer ordering
|
||||
|
||||
:::caution
|
||||
|
||||
To use this plugin properly, it's recommended to have a solid understanding of [CSS Cascade Layers](https://css-tricks.com/css-cascade-layers/), the [CSS Cascade](https://developer.mozilla.org/docs/Web/CSS/CSS_cascade/Cascade) and [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Specificity).
|
||||
|
||||
:::
|
||||
|
||||
## Installation {#installation}
|
||||
|
||||
```bash npm2yarn
|
||||
npm install --save @docusaurus/plugin-css-cascade-layers
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
If you use the preset `@docusaurus/preset-classic`, this plugin is automatically configured for you with the [`siteConfig.future.v4.useCssCascadeLayers`](../docusaurus.config.js.mdx#future) flag.
|
||||
|
||||
:::
|
||||
|
||||
## Configuration {#configuration}
|
||||
|
||||
Accepted fields:
|
||||
|
||||
```mdx-code-block
|
||||
<APITable>
|
||||
```
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `layers` | `Layers` | **Built-in layers** | An object representing all the CSS cascade layers you want to use, and whether the layer should be applied to a given file path. See examples and types below. |
|
||||
|
||||
```mdx-code-block
|
||||
</APITable>
|
||||
```
|
||||
|
||||
### Types {#types}
|
||||
|
||||
#### `Layers` {#EditUrlFunction}
|
||||
|
||||
```ts
|
||||
type Layers = Record<
|
||||
string, // layer name
|
||||
(filePath: string) => boolean // layer matcher
|
||||
>;
|
||||
```
|
||||
|
||||
The `layers` object is defined by:
|
||||
|
||||
- key: the name of a layer
|
||||
- value: a function to define if a given CSS module file should be in that layer
|
||||
|
||||
:::caution Order matters
|
||||
|
||||
The object order matters:
|
||||
|
||||
- the keys order defines an explicit CSS layer order
|
||||
- when multiple layers match a file path, only the first layer will apply
|
||||
|
||||
:::
|
||||
|
||||
### Example configuration {#ex-config}
|
||||
|
||||
You can configure this plugin through plugin options.
|
||||
|
||||
```js
|
||||
const options = {
|
||||
layers: {
|
||||
'docusaurus.infima': (filePath) =>
|
||||
filePath.includes('/node_modules/infima/dist'),
|
||||
'docusaurus.theme-classic': (filePath) =>
|
||||
filePath.includes('/node_modules/@docusaurus/theme-classic/lib'),
|
||||
},
|
||||
};
|
||||
```
|
Loading…
Add table
Add a link
Reference in a new issue