This commit is contained in:
ozakione 2024-04-04 17:58:53 +02:00
parent 45af11fd13
commit a9d6bcf968
9 changed files with 107 additions and 192 deletions

View file

@ -11,11 +11,4 @@ module.exports = {
url: 'https://your-docusaurus-site.example.com', url: 'https://your-docusaurus-site.example.com',
baseUrl: '/', baseUrl: '/',
favicon: 'img/favicon.ico', favicon: 'img/favicon.ico',
markdown: {
parseFrontMatter: async (params) => {
const result = await params.defaultParseFrontMatter(params);
result.frontMatter.custom_frontMatter = 'added by parseFrontMatter';
return result;
},
},
}; };

View file

@ -5,65 +5,12 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {escapeRegexp} from '@docusaurus/utils'; import {validateShowcaseItem} from '../validation';
import {validateShowcaseFrontMatter} from '../frontMatter'; import type {ShowcaseItem} from '@docusaurus/plugin-content-showcase';
import type {ShowcaseFrontMatter} from '@docusaurus/plugin-content-showcase';
function testField(params: {
prefix: string;
validFrontMatters: ShowcaseFrontMatter[];
convertibleFrontMatter?: [
ConvertibleFrontMatter: {[key: string]: unknown},
ConvertedFrontMatter: ShowcaseFrontMatter,
][];
invalidFrontMatters?: [
InvalidFrontMatter: {[key: string]: unknown},
ErrorMessage: string,
][];
}) {
// eslint-disable-next-line jest/require-top-level-describe
test(`[${params.prefix}] accept valid values`, () => {
params.validFrontMatters.forEach((frontMatter) => {
expect(validateShowcaseFrontMatter(frontMatter)).toEqual(frontMatter);
});
});
// eslint-disable-next-line jest/require-top-level-describe
test(`[${params.prefix}] convert valid values`, () => {
params.convertibleFrontMatter?.forEach(
([convertibleFrontMatter, convertedFrontMatter]) => {
expect(validateShowcaseFrontMatter(convertibleFrontMatter)).toEqual(
convertedFrontMatter,
);
},
);
});
// eslint-disable-next-line jest/require-top-level-describe
test(`[${params.prefix}] throw error for values`, () => {
params.invalidFrontMatters?.forEach(([frontMatter, message]) => {
try {
validateShowcaseFrontMatter(frontMatter);
throw new Error(
`Showcase front matter is expected to be rejected, but was accepted successfully:\n ${JSON.stringify(
frontMatter,
null,
2,
)}`,
);
} catch (err) {
// eslint-disable-next-line jest/no-conditional-expect
expect((err as Error).message).toMatch(
new RegExp(escapeRegexp(message)),
);
}
});
});
}
describe('showcase front matter schema', () => { describe('showcase front matter schema', () => {
it('accepts valid frontmatter', () => { it('accepts valid frontmatter', () => {
const frontMatter: ShowcaseFrontMatter = { const frontMatter: ShowcaseItem = {
title: 'title', title: 'title',
description: 'description', description: 'description',
preview: 'preview', preview: 'preview',
@ -71,37 +18,24 @@ describe('showcase front matter schema', () => {
tags: [], tags: [],
website: 'website', website: 'website',
}; };
expect(validateShowcaseFrontMatter(frontMatter)).toEqual(frontMatter); expect(validateShowcaseItem(frontMatter)).toEqual(frontMatter);
}); });
it('reject invalid frontmatter', () => { it('reject invalid frontmatter', () => {
const frontMatter = {}; const frontMatter = {};
expect(() => expect(() =>
validateShowcaseFrontMatter(frontMatter), validateShowcaseItem(frontMatter),
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`""title" is required. "description" is required. "preview" is required. "website" is required. "source" is required. "tags" is required"`, `""title" is required. "description" is required. "preview" is required. "website" is required. "source" is required. "tags" is required"`,
); );
}); });
});
describe('validateShowcaseFrontMatter full', () => { it('reject invalid frontmatter value', () => {
testField({ const frontMatter = {title: 42};
prefix: 'valid full frontmatter', expect(() =>
validFrontMatters: [ validateShowcaseItem(frontMatter),
{ ).toThrowErrorMatchingInlineSnapshot(
title: 'title', `""title" must be a string. "description" is required. "preview" is required. "website" is required. "source" is required. "tags" is required"`,
description: 'description', );
preview: 'preview',
source: 'source',
tags: [],
website: 'website',
},
],
invalidFrontMatters: [
[
{},
'"title" is required. "description" is required. "preview" is required. "website" is required. "source" is required. "tags" is required',
],
],
}); });
}); });

View file

@ -13,18 +13,13 @@ import {
Globby, Globby,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import Yaml from 'js-yaml'; import Yaml from 'js-yaml';
import {Joi} from '@docusaurus/utils-validation'; import {Joi} from '@docusaurus/utils-validation';
import { import {validateFrontMatterTags, validateShowcaseItem} from './validation';
validateFrontMatterTags, import {getTagsList} from './tags';
validateShowcaseFrontMatter,
} from './frontMatter';
import {tagSchema} from './options';
import type {LoadContext, Plugin} from '@docusaurus/types'; import type {LoadContext, Plugin} from '@docusaurus/types';
import type { import type {
PluginOptions, PluginOptions,
ShowcaseItem, ShowcaseItems,
TagOption,
} from '@docusaurus/plugin-content-showcase'; } from '@docusaurus/plugin-content-showcase';
import type {ShowcaseContentPaths} from './types'; import type {ShowcaseContentPaths} from './types';
@ -35,55 +30,24 @@ export function getContentPathList(
} }
function createTagSchema(tags: string[]): Joi.Schema { function createTagSchema(tags: string[]): Joi.Schema {
return Joi.alternatives().try( return Joi.array().items(Joi.string().valid(...tags)); // Schema for array of strings
Joi.string().valid(...tags), // Schema for single string
Joi.array().items(Joi.string().valid(...tags)), // Schema for array of strings
);
} }
export default function pluginContentShowcase( export default function pluginContentShowcase(
context: LoadContext, context: LoadContext,
options: PluginOptions, options: PluginOptions,
): Plugin<ShowcaseItem | null> { ): Plugin<ShowcaseItems | null> {
const {siteDir, localizationDir} = context; const {siteDir, localizationDir} = context;
const contentPaths: ShowcaseContentPaths = { const contentPaths: ShowcaseContentPaths = {
contentPath: path.resolve(siteDir, options.path), contentPath: path.resolve(siteDir, options.path),
contentPathLocalized: getPluginI18nPath({ contentPathLocalized: getPluginI18nPath({
localizationDir, localizationDir,
pluginName: 'docusaurus-plugin-content-pages', pluginName: 'docusaurus-plugin-content-showcase',
pluginId: options.id, pluginId: options.id,
}), }),
}; };
async function getTagsList(
configTags: string | TagOption[],
): Promise<string[]> {
if (typeof configTags === 'object') {
return Object.keys(configTags);
}
const tagsPath = path.resolve(contentPaths.contentPath, configTags);
try {
const rawYaml = await fs.readFile(tagsPath, 'utf-8');
const unsafeYaml = Yaml.load(rawYaml);
const safeYaml = tagSchema.validate(unsafeYaml);
if (safeYaml.error) {
throw new Error(
`There was an error extracting tags: ${safeYaml.error.message}`,
{cause: safeYaml.error},
);
}
const tagLabels = Object.keys(safeYaml.value);
return tagLabels;
} catch (error) {
throw new Error(`Failed to read tags file for showcase`, {cause: error});
}
}
return { return {
name: 'docusaurus-plugin-content-showcase', name: 'docusaurus-plugin-content-showcase',
@ -95,19 +59,24 @@ export default function pluginContentShowcase(
// ); // );
// }, // },
async loadContent(): Promise<ShowcaseItem | null> { async loadContent(): Promise<ShowcaseItems | null> {
if (!(await fs.pathExists(contentPaths.contentPath))) { if (!(await fs.pathExists(contentPaths.contentPath))) {
return null; throw new Error(
`The showcase content path does not exist: ${contentPaths.contentPath}`,
);
} }
const {include} = options; const {include} = options;
const showcaseFiles = await Globby(include, { const showcaseFiles = await Globby(include, {
cwd: contentPaths.contentPath, cwd: contentPaths.contentPath,
ignore: options.exclude, ignore: [...options.exclude],
}); });
const tagList = await getTagsList(options.tags); const tagList = await getTagsList({
configTags: options.tags,
configPath: contentPaths.contentPath,
});
const createdTagSchema = createTagSchema(tagList); const createdTagSchema = createTagSchema(tagList);
async function processShowcaseSourceFile(relativeSource: string) { async function processShowcaseSourceFile(relativeSource: string) {
@ -119,14 +88,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 data = await fs.readFile(sourcePath, 'utf-8');
// todo remove as ... because bad practice ? // todo remove as ... because bad practice ?
const unsafeYaml = Yaml.load(rawYaml) as {[key: string]: unknown}; const unsafeData = Yaml.load(data) as {[key: string]: unknown};
const yaml = validateShowcaseFrontMatter(unsafeYaml); const showcaseItem = validateShowcaseItem(unsafeData);
validateFrontMatterTags(yaml.tags, createdTagSchema); validateFrontMatterTags(showcaseItem.tags, createdTagSchema);
return yaml; return showcaseItem;
} }
async function doProcessShowcaseSourceFile(relativeSource: string) { async function doProcessShowcaseSourceFile(relativeSource: string) {
@ -160,7 +129,7 @@ export default function pluginContentShowcase(
); );
addRoute({ addRoute({
path: '/showcaseAll', path: options.routeBasePath,
component: '@theme/Showcase', component: '@theme/Showcase',
modules: { modules: {
content: showcaseAllData, content: showcaseAllData,

View file

@ -1,22 +0,0 @@
/**
* 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 type {LoaderContext} from 'webpack';
export default function markdownLoader(
this: LoaderContext<undefined>,
fileString: string,
): void {
const callback = this.async();
// const options = this.getOptions();
// TODO provide additional md processing here? like interlinking pages?
// fileString = linkify(fileString)
return callback(null, fileString);
}

View file

@ -13,8 +13,9 @@ import type {PluginOptions, Options} from '@docusaurus/plugin-content-showcase';
export const DEFAULT_OPTIONS: PluginOptions = { export const DEFAULT_OPTIONS: PluginOptions = {
id: 'showcase', id: 'showcase',
path: 'showcase', // Path to data on filesystem, relative to site dir. path: 'showcase', // Path to data on filesystem, relative to site dir.
routeBasePath: '/', // URL Route. routeBasePath: '/showcase', // URL Route.
include: ['**/*.{yml,yaml}'], include: ['**/*.{yml,yaml}'],
// todo exclude won't work if user pass a custom file name
exclude: [...GlobExcludeDefault, 'tags.*'], exclude: [...GlobExcludeDefault, 'tags.*'],
tags: 'tags.yaml', tags: 'tags.yaml',
}; };

View file

@ -8,15 +8,17 @@
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 TagOption = { type Tag = {
[key: string]: { label: string;
label: string; description: {
description: { message: string;
message: string; id: string;
id: string;
};
color: string;
}; };
color: string;
};
export type TagsOption = {
[tagName: string]: Tag;
}; };
export type PluginOptions = { export type PluginOptions = {
@ -25,32 +27,20 @@ declare module '@docusaurus/plugin-content-showcase' {
routeBasePath: string; routeBasePath: string;
include: string[]; include: string[];
exclude: string[]; exclude: string[];
tags: string | TagOption[]; tags: string | TagsOption;
}; };
type TagType = export type ShowcaseItem = {
| 'favorite'
| 'opensource'
| 'product'
| 'design'
| 'i18n'
| 'versioning'
| 'large'
| 'meta'
| 'personal'
| 'rtl';
export type ShowcaseFrontMatter = {
readonly title: string; readonly title: string;
readonly description: string; readonly description: string;
readonly preview: string | null; // null = use our serverless screenshot service readonly preview: string | null; // null = use our serverless screenshot service
readonly website: string; readonly website: string;
readonly source: string | null; readonly source: string | null;
readonly tags: TagType[]; readonly tags: string[];
}; };
export type ShowcaseItem = { export type ShowcaseItems = {
items: ShowcaseFrontMatter[]; items: ShowcaseItem[];
}; };
export type Options = Partial<PluginOptions>; export type Options = Partial<PluginOptions>;
@ -58,5 +48,5 @@ declare module '@docusaurus/plugin-content-showcase' {
export default function pluginContentShowcase( export default function pluginContentShowcase(
context: LoadContext, context: LoadContext,
options: PluginOptions, options: PluginOptions,
): Promise<Plugin<ShowcaseItem | null>>; ): Promise<Plugin<ShowcaseItems | null>>;
} }

View file

@ -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 fs from 'fs-extra';
import path from 'path';
import Yaml from 'js-yaml';
import {tagSchema} from './options';
import type {TagsOption} from '@docusaurus/plugin-content-showcase';
// todo extract in another file
export async function getTagsList({
configTags,
configPath,
}: {
configTags: string | TagsOption;
configPath: string;
}): Promise<string[]> {
if (typeof configTags === 'object') {
return Object.keys(configTags);
}
const tagsPath = path.resolve(configPath, configTags);
try {
const data = await fs.readFile(tagsPath, 'utf-8');
const unsafeData = Yaml.load(data);
const tags = tagSchema.validate(unsafeData);
if (tags.error) {
throw new Error(
`There was an error extracting tags: ${tags.error.message}`,
{cause: tags.error},
);
}
const tagLabels = Object.keys(tags.value);
return tagLabels;
} catch (error) {
throw new Error(`Failed to read tags file for showcase`, {cause: error});
}
}

View file

@ -6,9 +6,9 @@
*/ */
import {Joi, validateFrontMatter} from '@docusaurus/utils-validation'; import {Joi, validateFrontMatter} from '@docusaurus/utils-validation';
import type {ShowcaseFrontMatter} from '@docusaurus/plugin-content-showcase'; import type {ShowcaseItem} from '@docusaurus/plugin-content-showcase';
const showcaseFrontMatterSchema = Joi.object({ const showcaseItemSchema = Joi.object({
title: Joi.string().required(), title: Joi.string().required(),
description: Joi.string().required(), description: Joi.string().required(),
preview: Joi.string().required(), preview: Joi.string().required(),
@ -17,10 +17,10 @@ const showcaseFrontMatterSchema = Joi.object({
tags: Joi.array().items(Joi.string()).required(), tags: Joi.array().items(Joi.string()).required(),
}); });
export function validateShowcaseFrontMatter(frontMatter: { export function validateShowcaseItem(frontMatter: {
[key: string]: unknown; [key: string]: unknown;
}): ShowcaseFrontMatter { }): ShowcaseItem {
return validateFrontMatter(frontMatter, showcaseFrontMatterSchema); return validateFrontMatter(frontMatter, showcaseItemSchema);
} }
export function validateFrontMatterTags( export function validateFrontMatterTags(

View file

@ -239,7 +239,12 @@ export default async function createConfigAsync() {
], ],
themes: ['live-codeblock', ...dogfoodingThemeInstances], themes: ['live-codeblock', ...dogfoodingThemeInstances],
plugins: [ plugins: [
['content-showcase', {}], [
'content-showcase',
{
routeBasePath: '/showcaseAll',
},
],
[ [
'./src/plugins/changelog/index.js', './src/plugins/changelog/index.js',
{ {