Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions packages/docusaurus-plugin-css-cascade-layers/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.tsbuildinfo*
tsconfig*
__tests__
7 changes: 7 additions & 0 deletions packages/docusaurus-plugin-css-cascade-layers/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 29 additions & 0 deletions packages/docusaurus-plugin-css-cascade-layers/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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');
});
});
Original file line number Diff line number Diff line change
@@ -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 changes: 68 additions & 0 deletions packages/docusaurus-plugin-css-cascade-layers/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 27 additions & 0 deletions packages/docusaurus-plugin-css-cascade-layers/src/layers.ts
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading