mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-10 06:42:31 +02:00
wip tags file validation
This commit is contained in:
parent
cdb7c07bdc
commit
46c57d6cd5
12 changed files with 245 additions and 49 deletions
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
exports[`docusaurus-plugin-content-showcase loads simple showcase 1`] = `
|
exports[`docusaurus-plugin-content-showcase loads simple showcase 1`] = `
|
||||||
{
|
{
|
||||||
"website": [
|
"items": [
|
||||||
{
|
{
|
||||||
"description": "World",
|
"description": "World",
|
||||||
"preview": "github.com/ozakione.png",
|
"preview": "github.com/ozakione.png",
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||||
import {validateOptions, DEFAULT_OPTIONS} from '../options';
|
import {validateOptions, DEFAULT_OPTIONS} from '../options';
|
||||||
import type {Options} from '@docusaurus/plugin-content-pages';
|
import type {Options} from '@docusaurus/plugin-content-showcase';
|
||||||
|
|
||||||
function testValidate(options: Options) {
|
function testValidate(options: Options) {
|
||||||
return validateOptions({validate: normalizePluginOptions, options});
|
return validateOptions({validate: normalizePluginOptions, options});
|
||||||
|
@ -60,4 +60,68 @@ describe('normalizeShowcasePluginOptions', () => {
|
||||||
routeBasePath: '/',
|
routeBasePath: '/',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts correctly defined tags file options', () => {
|
||||||
|
const userOptions = {
|
||||||
|
tags: '@site/showcase/tags.yaml',
|
||||||
|
};
|
||||||
|
expect(testValidate(userOptions)).toEqual({
|
||||||
|
...defaultOptions,
|
||||||
|
...userOptions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject badly defined tags file options', () => {
|
||||||
|
const userOptions = {
|
||||||
|
tags: 42,
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidate(
|
||||||
|
// @ts-expect-error: bad attributes
|
||||||
|
userOptions,
|
||||||
|
),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""tags" must be one of [string, array]"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts correctly defined tags object options', () => {
|
||||||
|
const userOptions = {
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
label: 'foo',
|
||||||
|
description: {
|
||||||
|
message: 'bar',
|
||||||
|
id: 'baz',
|
||||||
|
},
|
||||||
|
color: 'red',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(testValidate(userOptions)).toEqual({
|
||||||
|
...defaultOptions,
|
||||||
|
...userOptions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject bedly defined tags object options', () => {
|
||||||
|
const userOptions = {
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
label: 'foo',
|
||||||
|
description: {
|
||||||
|
message: 'bar',
|
||||||
|
id: 'baz',
|
||||||
|
},
|
||||||
|
color: 42,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidate(
|
||||||
|
// @ts-expect-error: bad attributes
|
||||||
|
userOptions,
|
||||||
|
),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`""tags[0].color" must be a string"`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,9 +14,15 @@ import {
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import Yaml from 'js-yaml';
|
import Yaml from 'js-yaml';
|
||||||
|
|
||||||
|
import {Joi} from '@docusaurus/utils-validation';
|
||||||
import {validateShowcaseFrontMatter} from './frontMatter';
|
import {validateShowcaseFrontMatter} from './frontMatter';
|
||||||
|
import {tagSchema} from './options';
|
||||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||||
import type {PluginOptions, Content} from '@docusaurus/plugin-content-showcase';
|
import type {
|
||||||
|
PluginOptions,
|
||||||
|
ShowcaseItem,
|
||||||
|
TagOption,
|
||||||
|
} from '@docusaurus/plugin-content-showcase';
|
||||||
import type {ShowcaseContentPaths} from './types';
|
import type {ShowcaseContentPaths} from './types';
|
||||||
|
|
||||||
export function getContentPathList(
|
export function getContentPathList(
|
||||||
|
@ -25,10 +31,59 @@ export function getContentPathList(
|
||||||
return [contentPaths.contentPathLocalized, contentPaths.contentPath];
|
return [contentPaths.contentPathLocalized, contentPaths.contentPath];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getTagsDefinition(
|
||||||
|
filePath: string | TagOption[],
|
||||||
|
): Promise<string[]> {
|
||||||
|
if (Array.isArray(filePath)) {
|
||||||
|
return filePath.map((tag) => tag.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawYaml = await fs.readFile(filePath, 'utf-8');
|
||||||
|
const unsafeYaml: any = Yaml.load(rawYaml);
|
||||||
|
console.log('unsafeYaml:', unsafeYaml);
|
||||||
|
|
||||||
|
const transformedData = unsafeYaml.tags.map((item: any) => {
|
||||||
|
const [label] = Object.keys(item); // Extract label from object key
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
const {description, color} = item[label]; // Extract description and color
|
||||||
|
return {label, description, color}; // Create new object with transformed structure
|
||||||
|
});
|
||||||
|
console.log('transformedData:', transformedData);
|
||||||
|
|
||||||
|
const safeYaml = tagSchema.validate(transformedData);
|
||||||
|
|
||||||
|
if (safeYaml.error) {
|
||||||
|
throw new Error(`Invalid tags.yaml file: ${safeYaml.error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagLabels = safeYaml.value.map((tag: any) => Object.keys(tag)[0]);
|
||||||
|
return tagLabels;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTagSchema(tags: string[]): Joi.Schema {
|
||||||
|
return Joi.alternatives().try(
|
||||||
|
Joi.string().valid(...tags), // Schema for single string
|
||||||
|
Joi.array().items(Joi.string().valid(...tags)), // Schema for array of strings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateFrontMatterTags(
|
||||||
|
frontMatterTags: string[],
|
||||||
|
tagListSchema: Joi.Schema,
|
||||||
|
): void {
|
||||||
|
const result = tagListSchema.validate(frontMatterTags);
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(
|
||||||
|
`Front matter contains invalid tags: ${result.error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function pluginContentShowcase(
|
export default function pluginContentShowcase(
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
): Plugin<Content | null> {
|
): Plugin<ShowcaseItem | null> {
|
||||||
const {siteDir, localizationDir} = context;
|
const {siteDir, localizationDir} = context;
|
||||||
|
|
||||||
const contentPaths: ShowcaseContentPaths = {
|
const contentPaths: ShowcaseContentPaths = {
|
||||||
|
@ -51,7 +106,7 @@ export default function pluginContentShowcase(
|
||||||
// );
|
// );
|
||||||
// },
|
// },
|
||||||
|
|
||||||
async loadContent(): Promise<Content | null> {
|
async loadContent(): Promise<ShowcaseItem | null> {
|
||||||
const {include} = options;
|
const {include} = options;
|
||||||
|
|
||||||
if (!(await fs.pathExists(contentPaths.contentPath))) {
|
if (!(await fs.pathExists(contentPaths.contentPath))) {
|
||||||
|
@ -64,6 +119,23 @@ export default function pluginContentShowcase(
|
||||||
ignore: options.exclude,
|
ignore: options.exclude,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filteredShowcaseFiles = showcaseFiles.filter(
|
||||||
|
(source) => source !== 'tags.yaml',
|
||||||
|
);
|
||||||
|
|
||||||
|
// todo refactor ugly
|
||||||
|
const tagFilePath = path.join(
|
||||||
|
await getFolderContainingFile(
|
||||||
|
getContentPathList(contentPaths),
|
||||||
|
'tags.yaml',
|
||||||
|
),
|
||||||
|
'tags.yaml',
|
||||||
|
);
|
||||||
|
|
||||||
|
const tagList = await getTagsDefinition(tagFilePath);
|
||||||
|
const createdTagSchema = createTagSchema(tagList);
|
||||||
|
console.log('createdTagSchema:', createdTagSchema.describe());
|
||||||
|
|
||||||
async function processShowcaseSourceFile(relativeSource: string) {
|
async function processShowcaseSourceFile(relativeSource: string) {
|
||||||
// Lookup in localized folder in priority
|
// Lookup in localized folder in priority
|
||||||
const contentPath = await getFolderContainingFile(
|
const contentPath = await getFolderContainingFile(
|
||||||
|
@ -72,9 +144,14 @@ export default function pluginContentShowcase(
|
||||||
);
|
);
|
||||||
|
|
||||||
const sourcePath = path.join(contentPath, relativeSource);
|
const sourcePath = path.join(contentPath, relativeSource);
|
||||||
|
|
||||||
const rawYaml = await fs.readFile(sourcePath, 'utf-8');
|
const rawYaml = await fs.readFile(sourcePath, 'utf-8');
|
||||||
const unsafeYaml = Yaml.load(rawYaml) as {[key: string]: unknown};
|
const unsafeYaml = Yaml.load(rawYaml) as {[key: string]: unknown};
|
||||||
return validateShowcaseFrontMatter(unsafeYaml);
|
const yaml = validateShowcaseFrontMatter(unsafeYaml);
|
||||||
|
|
||||||
|
validateFrontMatterTags(yaml.tags, createdTagSchema);
|
||||||
|
|
||||||
|
return yaml;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doProcessShowcaseSourceFile(relativeSource: string) {
|
async function doProcessShowcaseSourceFile(relativeSource: string) {
|
||||||
|
@ -89,8 +166,8 @@ export default function pluginContentShowcase(
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
website: await Promise.all(
|
items: await Promise.all(
|
||||||
showcaseFiles.map(doProcessShowcaseSourceFile),
|
filteredShowcaseFiles.map(doProcessShowcaseSourceFile),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -104,7 +181,7 @@ export default function pluginContentShowcase(
|
||||||
|
|
||||||
const showcaseAllData = await createData(
|
const showcaseAllData = await createData(
|
||||||
'showcaseAll.json',
|
'showcaseAll.json',
|
||||||
JSON.stringify(content.website),
|
JSON.stringify(content.items),
|
||||||
);
|
);
|
||||||
|
|
||||||
addRoute({
|
addRoute({
|
||||||
|
|
|
@ -12,18 +12,33 @@ import type {PluginOptions, Options} from '@docusaurus/plugin-content-showcase';
|
||||||
|
|
||||||
export const DEFAULT_OPTIONS: PluginOptions = {
|
export const DEFAULT_OPTIONS: PluginOptions = {
|
||||||
id: 'showcase',
|
id: 'showcase',
|
||||||
path: 'src/showcase/website', // Path to data on filesystem, relative to site dir.
|
path: 'showcase', // Path to data on filesystem, relative to site dir.
|
||||||
routeBasePath: '/', // URL Route.
|
routeBasePath: '/', // URL Route.
|
||||||
include: ['**/*.{yml,yaml}'], // Extensions to include.
|
include: ['**/*.{yml,yaml}'], // Extensions to include.
|
||||||
exclude: GlobExcludeDefault,
|
exclude: GlobExcludeDefault,
|
||||||
|
tags: '@site/showcase/tags.yaml',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const tagSchema = Joi.array().items(
|
||||||
|
Joi.object({
|
||||||
|
label: Joi.string().required(),
|
||||||
|
description: Joi.object({
|
||||||
|
message: Joi.string().required(),
|
||||||
|
id: Joi.string().required(),
|
||||||
|
}).required(),
|
||||||
|
color: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const PluginOptionSchema = Joi.object<PluginOptions>({
|
const PluginOptionSchema = Joi.object<PluginOptions>({
|
||||||
path: Joi.string().default(DEFAULT_OPTIONS.path),
|
path: Joi.string().default(DEFAULT_OPTIONS.path),
|
||||||
routeBasePath: RouteBasePathSchema.default(DEFAULT_OPTIONS.routeBasePath),
|
routeBasePath: RouteBasePathSchema.default(DEFAULT_OPTIONS.routeBasePath),
|
||||||
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
|
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
|
||||||
exclude: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.exclude),
|
exclude: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.exclude),
|
||||||
id: Joi.string().default(DEFAULT_OPTIONS.id),
|
id: Joi.string().default(DEFAULT_OPTIONS.id),
|
||||||
|
tags: Joi.alternatives()
|
||||||
|
.try(Joi.string().default(DEFAULT_OPTIONS.tags), tagSchema)
|
||||||
|
.default(DEFAULT_OPTIONS.tags),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function validateOptions({
|
export function validateOptions({
|
||||||
|
|
|
@ -8,8 +8,13 @@
|
||||||
declare module '@docusaurus/plugin-content-showcase' {
|
declare module '@docusaurus/plugin-content-showcase' {
|
||||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||||
|
|
||||||
export type Assets = {
|
export type TagOption = {
|
||||||
image?: string;
|
label: string;
|
||||||
|
description: {
|
||||||
|
message: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginOptions = {
|
export type PluginOptions = {
|
||||||
|
@ -18,9 +23,10 @@ declare module '@docusaurus/plugin-content-showcase' {
|
||||||
routeBasePath: string;
|
routeBasePath: string;
|
||||||
include: string[];
|
include: string[];
|
||||||
exclude: string[];
|
exclude: string[];
|
||||||
|
tags: string | TagOption[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TagType =
|
type TagType =
|
||||||
| 'favorite'
|
| 'favorite'
|
||||||
| 'opensource'
|
| 'opensource'
|
||||||
| 'product'
|
| 'product'
|
||||||
|
@ -41,41 +47,14 @@ declare module '@docusaurus/plugin-content-showcase' {
|
||||||
readonly tags: TagType[];
|
readonly tags: TagType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Content = {
|
export type ShowcaseItem = {
|
||||||
website: {
|
items: ShowcaseFrontMatter[];
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
preview: string | null; // null = use our serverless screenshot service
|
|
||||||
website: string;
|
|
||||||
source: string | null;
|
|
||||||
sourcePath?: string;
|
|
||||||
tags: TagType[];
|
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Options = Partial<PluginOptions>;
|
export type Options = Partial<PluginOptions>;
|
||||||
|
|
||||||
export default function pluginShowcase(
|
export default function pluginContentShowcase(
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
): Promise<Plugin<Content>>;
|
): Promise<Plugin<ShowcaseItem | null>>;
|
||||||
|
|
||||||
export type ShowcaseMetadata = {
|
|
||||||
/** Path to the Markdown source, with `@site` alias. */
|
|
||||||
readonly source: string;
|
|
||||||
/**
|
|
||||||
* Used to generate the page h1 heading, tab title, and pagination title.
|
|
||||||
*/
|
|
||||||
readonly title: string;
|
|
||||||
/** Full link including base URL. */
|
|
||||||
readonly permalink: string;
|
|
||||||
/**
|
|
||||||
* Description used in the meta. Could be an empty string (empty content)
|
|
||||||
*/
|
|
||||||
readonly description: string;
|
|
||||||
/** Front matter, as-is. */
|
|
||||||
readonly frontMatter: Content['website'][number] & {[key: string]: unknown};
|
|
||||||
/** Tags, normalized. */
|
|
||||||
readonly tags: TagType[];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -248,9 +248,9 @@ declare module '@theme/BlogPostItems' {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/ShowcaseDetails' {
|
declare module '@theme/ShowcaseDetails' {
|
||||||
import type {Content} from '@docusaurus/plugin-content-showcase';
|
import type {ShowcaseItem} from '@docusaurus/plugin-content-showcase';
|
||||||
|
|
||||||
export type User = Content['website'][number];
|
export type User = ShowcaseItem['website'][number];
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
content: User;
|
content: User;
|
||||||
|
@ -260,9 +260,9 @@ declare module '@theme/ShowcaseDetails' {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/Showcase' {
|
declare module '@theme/Showcase' {
|
||||||
import type {Content} from '@docusaurus/plugin-content-showcase';
|
import type {ShowcaseItem} from '@docusaurus/plugin-content-showcase';
|
||||||
|
|
||||||
export type User = Content['website'][number];
|
export type User = ShowcaseItem['website'][number];
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
content: User[];
|
content: User[];
|
||||||
|
|
|
@ -239,7 +239,7 @@ export default async function createConfigAsync() {
|
||||||
],
|
],
|
||||||
themes: ['live-codeblock', ...dogfoodingThemeInstances],
|
themes: ['live-codeblock', ...dogfoodingThemeInstances],
|
||||||
plugins: [
|
plugins: [
|
||||||
'showcase',
|
'content-showcase',
|
||||||
[
|
[
|
||||||
'./src/plugins/changelog/index.js',
|
'./src/plugins/changelog/index.js',
|
||||||
{
|
{
|
||||||
|
|
61
website/showcase/tags.yaml
Normal file
61
website/showcase/tags.yaml
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
tags:
|
||||||
|
- favorite:
|
||||||
|
label: 'Favorite'
|
||||||
|
description:
|
||||||
|
message: 'Our favorite Docusaurus sites that you must absolutely check out!'
|
||||||
|
id: 'showcase.tag.favorite.description'
|
||||||
|
color: '#e9669e'
|
||||||
|
- opensource:
|
||||||
|
label: 'Open-Source'
|
||||||
|
description:
|
||||||
|
message: 'Open-Source Docusaurus sites can be useful for inspiration!'
|
||||||
|
id: 'showcase.tag.opensource.description'
|
||||||
|
color: '#39ca30'
|
||||||
|
- product:
|
||||||
|
label: 'Product'
|
||||||
|
description:
|
||||||
|
message: 'Docusaurus sites associated to a commercial product!'
|
||||||
|
id: 'showcase.tag.product.description'
|
||||||
|
color: '#dfd545'
|
||||||
|
- design:
|
||||||
|
label: 'Design'
|
||||||
|
description:
|
||||||
|
message: 'Beautiful Docusaurus sites, polished and standing out from the initial template!'
|
||||||
|
id: 'showcase.tag.design.description'
|
||||||
|
color: '#a44fb7'
|
||||||
|
- i18n:
|
||||||
|
label: 'I18n'
|
||||||
|
description:
|
||||||
|
message: 'Translated Docusaurus sites using the internationalization support with more than 1 locale.'
|
||||||
|
id: 'showcase.tag.i18n.description'
|
||||||
|
color: '#127f82'
|
||||||
|
- versioning:
|
||||||
|
label: 'Versioning'
|
||||||
|
description:
|
||||||
|
message: 'Docusaurus sites using the versioning feature of the docs plugin to manage multiple versions.'
|
||||||
|
id: 'showcase.tag.versioning.description'
|
||||||
|
color: '#fe6829'
|
||||||
|
- large:
|
||||||
|
label: 'Large'
|
||||||
|
description:
|
||||||
|
message: 'Very large Docusaurus sites, including many more pages than the average!'
|
||||||
|
id: 'showcase.tag.large.description'
|
||||||
|
color: '#8c2f00'
|
||||||
|
- meta:
|
||||||
|
label: 'Meta'
|
||||||
|
description:
|
||||||
|
message: 'Docusaurus sites of Meta (formerly Facebook) projects'
|
||||||
|
id: 'showcase.tag.meta.description'
|
||||||
|
color: '#4267b2'
|
||||||
|
- personal:
|
||||||
|
label: 'Personal'
|
||||||
|
description:
|
||||||
|
message: 'Personal websites, blogs and digital gardens built with Docusaurus'
|
||||||
|
id: 'showcase.tag.personal.description'
|
||||||
|
color: '#14cfc3'
|
||||||
|
- rtl:
|
||||||
|
label: 'RTL Direction'
|
||||||
|
description:
|
||||||
|
message: 'Docusaurus sites using the right-to-left reading direction support.'
|
||||||
|
id: 'showcase.tag.rtl.description'
|
||||||
|
color: '#ffcfc3'
|
Loading…
Add table
Add a link
Reference in a new issue