feat(v2): add option validation for remaining official plugins (#2970)

* feat(v2): add option validation lifecycle method

* chore(v2): add dependencies

* chore(v2): add yup dependency

* feat(v2): add option validation for plugin-content-docs

* chore(v2): add facebook copyright

* refactor(v2): remove unused variable

* chore(v2): add dependencies

* chore(v2): add copyright

* fix(v2): use strict for option validation

* feat(v2): add option validation for plugin-content-pages

* feat(v2): add schema for plugin-google-analytics and plugin-google-gtag

* feat(v2): add option validation for plugin-sitemap

* chore(v2): add dependency for yup

* fix(v2): remove strict to allow normalization

* refactor(v2): refactor validate method

* feat(v2): modify existing tests

* feat(v2): add tests for plugin normalization

* style(v2): use a more descriptive filename for schema

* feat(v2): add normalization tests

* feat(v2): add more tests for option validation

* refactor(v2): remove unused code

* refactor(v2): remove unused code

* refactor(v2): refactor methods and types

* feat(v2): replace Yup with Joi

* fix(v2): fix plugin-content-docs schema

* feat(v2): modify tests for plugin-content-docs

* fix(v2): fix a typo

* refactor(v2): improve tests and refactor code

* feat(v2): support both commonjs and ES modules

* refactor(v2): refactor validateOption method

* style(v2): fix eslint errors and typo in types

* chore(v2): remove unused yup dependency

* style(v2): standardize naming across official plugins

* chore(v2): update test snapshots

* chore(v2): remove obsolete snapshots

* chore(v2): fix a typo and check test

* feat(v2): add validation for new field

* feat(v2): add test for new field
This commit is contained in:
Teik Jun 2020-06-26 21:14:59 +08:00 committed by GitHub
parent 3213955e72
commit 0f59cd1599
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 444 additions and 169 deletions

View file

@ -20,8 +20,7 @@
"eta": "^1.1.1", "eta": "^1.1.1",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"globby": "^10.0.1", "globby": "^10.0.1",
"lodash": "^4.17.15", "lodash": "^4.17.15"
"yup": "^0.29.0"
}, },
"peerDependencies": { "peerDependencies": {
"@docusaurus/core": "^2.0.0", "@docusaurus/core": "^2.0.0",
@ -30,8 +29,5 @@
}, },
"engines": { "engines": {
"node": ">=10.9.0" "node": ">=10.9.0"
},
"devDependencies": {
"@types/yup": "^0.29.0"
} }
} }

View file

@ -9,7 +9,7 @@ import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import pluginContentBlog from '../index'; import pluginContentBlog from '../index';
import {DocusaurusConfig, LoadContext} from '@docusaurus/types'; import {DocusaurusConfig, LoadContext} from '@docusaurus/types';
import {PluginOptionSchema} from '../validation'; import {PluginOptionSchema} from '../pluginOptionSchema';
function validateAndNormalize(schema, options) { function validateAndNormalize(schema, options) {
const {value, error} = schema.validate(options); const {value, error} = schema.validate(options);

View file

@ -5,11 +5,11 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {PluginOptionSchema, DefaultOptions} from '../validation'; import {PluginOptionSchema, DEFAULT_OPTIONS} from '../pluginOptionSchema';
test('normalize options', () => { test('normalize options', () => {
const {value} = PluginOptionSchema.validate({}); const {value} = PluginOptionSchema.validate({});
expect(value).toEqual(DefaultOptions); expect(value).toEqual(DEFAULT_OPTIONS);
}); });
test('validate options', () => { test('validate options', () => {
@ -20,7 +20,7 @@ test('validate options', () => {
routeBasePath: 'not_blog', routeBasePath: 'not_blog',
}); });
expect(value).toEqual({ expect(value).toEqual({
...DefaultOptions, ...DEFAULT_OPTIONS,
postsPerPage: 5, postsPerPage: 5,
include: ['api/*', 'docs/*'], include: ['api/*', 'docs/*'],
routeBasePath: 'not_blog', routeBasePath: 'not_blog',
@ -54,7 +54,7 @@ test('convert all feed type to array with other feed type', () => {
feedOptions: {type: 'all'}, feedOptions: {type: 'all'},
}); });
expect(value).toEqual({ expect(value).toEqual({
...DefaultOptions, ...DEFAULT_OPTIONS,
feedOptions: {type: ['rss', 'atom']}, feedOptions: {type: ['rss', 'atom']},
}); });
}); });

View file

@ -21,7 +21,7 @@ import {
BlogPaginated, BlogPaginated,
BlogPost, BlogPost,
} from './types'; } from './types';
import {PluginOptionSchema} from './validation'; import {PluginOptionSchema} from './pluginOptionSchema';
import { import {
LoadContext, LoadContext,
PluginContentLoadedActions, PluginContentLoadedActions,

View file

@ -7,7 +7,7 @@
import * as Joi from '@hapi/joi'; import * as Joi from '@hapi/joi';
export const DefaultOptions = { export const DEFAULT_OPTIONS = {
feedOptions: {}, feedOptions: {},
beforeDefaultRehypePlugins: [], beforeDefaultRehypePlugins: [],
beforeDefaultRemarkPlugins: [], beforeDefaultRemarkPlugins: [],
@ -27,22 +27,22 @@ export const DefaultOptions = {
}; };
export const PluginOptionSchema = Joi.object({ export const PluginOptionSchema = Joi.object({
path: Joi.string().default(DefaultOptions.path), path: Joi.string().default(DEFAULT_OPTIONS.path),
routeBasePath: Joi.string().default(DefaultOptions.routeBasePath), routeBasePath: Joi.string().default(DEFAULT_OPTIONS.routeBasePath),
include: Joi.array().items(Joi.string()).default(DefaultOptions.include), include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
postsPerPage: Joi.number() postsPerPage: Joi.number()
.integer() .integer()
.min(1) .min(1)
.default(DefaultOptions.postsPerPage), .default(DEFAULT_OPTIONS.postsPerPage),
blogListComponent: Joi.string().default(DefaultOptions.blogListComponent), blogListComponent: Joi.string().default(DEFAULT_OPTIONS.blogListComponent),
blogPostComponent: Joi.string().default(DefaultOptions.blogPostComponent), blogPostComponent: Joi.string().default(DEFAULT_OPTIONS.blogPostComponent),
blogTagsListComponent: Joi.string().default( blogTagsListComponent: Joi.string().default(
DefaultOptions.blogTagsListComponent, DEFAULT_OPTIONS.blogTagsListComponent,
), ),
blogTagsPostsComponent: Joi.string().default( blogTagsPostsComponent: Joi.string().default(
DefaultOptions.blogTagsPostsComponent, DEFAULT_OPTIONS.blogTagsPostsComponent,
), ),
showReadingTime: Joi.bool().default(DefaultOptions.showReadingTime), showReadingTime: Joi.bool().default(DEFAULT_OPTIONS.showReadingTime),
remarkPlugins: Joi.array() remarkPlugins: Joi.array()
.items( .items(
Joi.alternatives().try( Joi.alternatives().try(
@ -52,19 +52,19 @@ export const PluginOptionSchema = Joi.object({
.length(2), .length(2),
), ),
) )
.default(DefaultOptions.remarkPlugins), .default(DEFAULT_OPTIONS.remarkPlugins),
rehypePlugins: Joi.array() rehypePlugins: Joi.array()
.items(Joi.string()) .items(Joi.string())
.default(DefaultOptions.rehypePlugins), .default(DEFAULT_OPTIONS.rehypePlugins),
editUrl: Joi.string().uri(), editUrl: Joi.string().uri(),
truncateMarker: Joi.object().default(DefaultOptions.truncateMarker), truncateMarker: Joi.object().default(DEFAULT_OPTIONS.truncateMarker),
admonitions: Joi.object().default(DefaultOptions.admonitions), admonitions: Joi.object().default(DEFAULT_OPTIONS.admonitions),
beforeDefaultRemarkPlugins: Joi.array() beforeDefaultRemarkPlugins: Joi.array()
.items(Joi.object()) .items(Joi.object())
.default(DefaultOptions.beforeDefaultRemarkPlugins), .default(DEFAULT_OPTIONS.beforeDefaultRemarkPlugins),
beforeDefaultRehypePlugins: Joi.array() beforeDefaultRehypePlugins: Joi.array()
.items(Joi.object()) .items(Joi.object())
.default(DefaultOptions.beforeDefaultRehypePlugins), .default(DEFAULT_OPTIONS.beforeDefaultRehypePlugins),
feedOptions: Joi.object({ feedOptions: Joi.object({
type: Joi.alternatives().conditional( type: Joi.alternatives().conditional(
Joi.string().equal('all', 'rss', 'atom'), Joi.string().equal('all', 'rss', 'atom'),
@ -76,5 +76,5 @@ export const PluginOptionSchema = Joi.object({
description: Joi.string(), description: Joi.string(),
copyright: Joi.string(), copyright: Joi.string(),
language: Joi.string(), language: Joi.string(),
}).default(DefaultOptions.feedOptions), }).default(DEFAULT_OPTIONS.feedOptions),
}); });

View file

@ -13,7 +13,8 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"commander": "^5.0.0", "commander": "^5.0.0",
"picomatch": "^2.1.1" "picomatch": "^2.1.1",
"@types/hapi__joi": "^17.1.2"
}, },
"dependencies": { "dependencies": {
"@docusaurus/mdx-loader": "^2.0.0-alpha.58", "@docusaurus/mdx-loader": "^2.0.0-alpha.58",
@ -22,6 +23,7 @@
"execa": "^3.4.0", "execa": "^3.4.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"globby": "^10.0.1", "globby": "^10.0.1",
"@hapi/joi": "17.1.1",
"import-fresh": "^3.2.1", "import-fresh": "^3.2.1",
"loader-utils": "^1.2.3", "loader-utils": "^1.2.3",
"lodash.flatmap": "^4.5.0", "lodash.flatmap": "^4.5.0",

View file

@ -12,6 +12,7 @@ import commander from 'commander';
import fs from 'fs-extra'; import fs from 'fs-extra';
import pluginContentDocs from '../index'; import pluginContentDocs from '../index';
import loadEnv from '../env'; import loadEnv from '../env';
import normalizePluginOptions from './pluginOptionSchema.test';
import {loadContext} from '@docusaurus/core/src/server/index'; import {loadContext} from '@docusaurus/core/src/server/index';
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils'; import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils';
import {RouteConfig} from '@docusaurus/types'; import {RouteConfig} from '@docusaurus/types';
@ -42,9 +43,12 @@ test('site with wrong sidebar file', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site'); const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
const context = loadContext(siteDir); const context = loadContext(siteDir);
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json'); const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
const plugin = pluginContentDocs(context, { const plugin = pluginContentDocs(
sidebarPath, context,
}); normalizePluginOptions({
sidebarPath,
}),
);
await expect(plugin.loadContent()).rejects.toThrowErrorMatchingSnapshot(); await expect(plugin.loadContent()).rejects.toThrowErrorMatchingSnapshot();
}); });
@ -54,7 +58,7 @@ describe('empty/no docs website', () => {
test('no files in docs folder', async () => { test('no files in docs folder', async () => {
await fs.ensureDir(path.join(siteDir, 'docs')); await fs.ensureDir(path.join(siteDir, 'docs'));
const plugin = pluginContentDocs(context, {}); const plugin = pluginContentDocs(context, normalizePluginOptions({}));
const content = await plugin.loadContent(); const content = await plugin.loadContent();
const {docsMetadata, docsSidebars} = content; const {docsMetadata, docsSidebars} = content;
expect(docsMetadata).toMatchInlineSnapshot(`Object {}`); expect(docsMetadata).toMatchInlineSnapshot(`Object {}`);
@ -73,7 +77,12 @@ describe('empty/no docs website', () => {
}); });
test('docs folder does not exist', async () => { test('docs folder does not exist', async () => {
const plugin = pluginContentDocs(context, {path: '/path/does/not/exist/'}); const plugin = pluginContentDocs(
context,
normalizePluginOptions({
path: '/path/does/not/exist/',
}),
);
const content = await plugin.loadContent(); const content = await plugin.loadContent();
expect(content).toBeNull(); expect(content).toBeNull();
}); });
@ -84,11 +93,14 @@ describe('simple website', () => {
const context = loadContext(siteDir); const context = loadContext(siteDir);
const sidebarPath = path.join(siteDir, 'sidebars.json'); const sidebarPath = path.join(siteDir, 'sidebars.json');
const pluginPath = 'docs'; const pluginPath = 'docs';
const plugin = pluginContentDocs(context, { const plugin = pluginContentDocs(
path: pluginPath, context,
sidebarPath, normalizePluginOptions({
homePageId: 'hello', path: pluginPath,
}); sidebarPath,
homePageId: 'hello',
}),
);
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name); const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
test('extendCli - docsVersion', () => { test('extendCli - docsVersion', () => {
@ -215,11 +227,14 @@ describe('versioned website', () => {
const context = loadContext(siteDir); const context = loadContext(siteDir);
const sidebarPath = path.join(siteDir, 'sidebars.json'); const sidebarPath = path.join(siteDir, 'sidebars.json');
const routeBasePath = 'docs'; const routeBasePath = 'docs';
const plugin = pluginContentDocs(context, { const plugin = pluginContentDocs(
routeBasePath, context,
sidebarPath, normalizePluginOptions({
homePageId: 'hello', routeBasePath,
}); sidebarPath,
homePageId: 'hello',
}),
);
const env = loadEnv(siteDir); const env = loadEnv(siteDir);
const {docsDir: versionedDir} = env.versioning; const {docsDir: versionedDir} = env.versioning;
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name); const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);

View file

@ -0,0 +1,83 @@
/**
* 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 {PluginOptionSchema, DEFAULT_OPTIONS} from '../pluginOptionSchema';
export default function normalizePluginOptions(options) {
const {value, error} = PluginOptionSchema.validate(options, {
convert: false,
});
if (error) {
throw error;
} else {
return value;
}
}
describe('normalizeDocsPluginOptions', () => {
test('should return default options for undefined user options', async () => {
const {value} = await PluginOptionSchema.validate({});
expect(value).toEqual(DEFAULT_OPTIONS);
});
test('should accept correctly defined user options', async () => {
const userOptions = {
path: 'my-docs', // Path to data on filesystem, relative to site dir.
routeBasePath: 'my-docs', // URL Route.
homePageId: 'home', // Document id for docs home page.
include: ['**/*.{md,mdx}'], // Extensions to include.
sidebarPath: 'my-sidebar', // Path to sidebar configuration for showing a list of markdown pages.
docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem',
remarkPlugins: [],
rehypePlugins: [],
showLastUpdateTime: true,
showLastUpdateAuthor: true,
admonitions: {},
excludeNextVersionDocs: true,
};
const {value} = await PluginOptionSchema.validate(userOptions);
expect(value).toEqual(userOptions);
});
test('should reject bad path inputs', () => {
expect(() => {
normalizePluginOptions({
path: 2,
});
}).toThrowErrorMatchingInlineSnapshot(`"\\"path\\" must be a string"`);
});
test('should reject bad include inputs', () => {
expect(() => {
normalizePluginOptions({
include: '**/*.{md,mdx}',
});
}).toThrowErrorMatchingInlineSnapshot(`"\\"include\\" must be an array"`);
});
test('should reject bad showLastUpdateTime inputs', () => {
expect(() => {
normalizePluginOptions({
showLastUpdateTime: 'true',
});
}).toThrowErrorMatchingInlineSnapshot(
`"\\"showLastUpdateTime\\" must be a boolean"`,
);
});
test('should reject bad remarkPlugins input', () => {
expect(() => {
normalizePluginOptions({
remarkPlugins: 'remark-math',
});
}).toThrowErrorMatchingInlineSnapshot(
`"\\"remarkPlugins\\" must be an array"`,
);
});
});

View file

@ -24,7 +24,7 @@ describe('loadSidebars', () => {
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });
test('sidebars shortand and longform lead to exact same sidebar', async () => { test('sidebars shorthand and longform lead to exact same sidebar', async () => {
const sidebarPath1 = path.join(fixtureDir, 'sidebars-category.js'); const sidebarPath1 = path.join(fixtureDir, 'sidebars-category.js');
const sidebarPath2 = path.join( const sidebarPath2 = path.join(
fixtureDir, fixtureDir,

View file

@ -18,7 +18,13 @@ import {
objectWithKeySorted, objectWithKeySorted,
aliasedSitePath, aliasedSitePath,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import {LoadContext, Plugin, RouteConfig} from '@docusaurus/types'; import {
LoadContext,
Plugin,
RouteConfig,
OptionValidationContext,
ValidationResult,
} from '@docusaurus/types';
import createOrder from './order'; import createOrder from './order';
import loadSidebars from './sidebars'; import loadSidebars from './sidebars';
@ -47,24 +53,8 @@ import {
import {Configuration} from 'webpack'; import {Configuration} from 'webpack';
import {docsVersion} from './version'; import {docsVersion} from './version';
import {VERSIONS_JSON_FILE} from './constants'; import {VERSIONS_JSON_FILE} from './constants';
import {PluginOptionSchema} from './pluginOptionSchema';
const REVERSED_DOCS_HOME_PAGE_ID = '_index'; import {ValidationError} from '@hapi/joi';
const DEFAULT_OPTIONS: PluginOptions = {
path: 'docs', // Path to data on filesystem, relative to site dir.
routeBasePath: 'docs', // URL Route.
homePageId: REVERSED_DOCS_HOME_PAGE_ID, // Document id for docs home page.
include: ['**/*.{md,mdx}'], // Extensions to include.
sidebarPath: '', // Path to sidebar configuration for showing a list of markdown pages.
docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem',
remarkPlugins: [],
rehypePlugins: [],
showLastUpdateTime: false,
showLastUpdateAuthor: false,
admonitions: {},
excludeNextVersionDocs: false,
};
function getFirstDocLinkOfSidebar( function getFirstDocLinkOfSidebar(
sidebarItems: DocsSidebarItem[], sidebarItems: DocsSidebarItem[],
@ -84,9 +74,8 @@ function getFirstDocLinkOfSidebar(
export default function pluginContentDocs( export default function pluginContentDocs(
context: LoadContext, context: LoadContext,
opts: Partial<PluginOptions>, options: PluginOptions,
): Plugin<LoadedContent | null> { ): Plugin<LoadedContent | null, typeof PluginOptionSchema> {
const options: PluginOptions = {...DEFAULT_OPTIONS, ...opts};
const homePageDocsRoutePath = const homePageDocsRoutePath =
options.routeBasePath === '' ? '/' : options.routeBasePath; options.routeBasePath === '' ? '/' : options.routeBasePath;
@ -551,3 +540,14 @@ Available document ids=
}, },
}; };
} }
export function validateOptions({
validate,
options,
}: OptionValidationContext<PluginOptions, ValidationError>): ValidationResult<
PluginOptions,
ValidationError
> {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}

View file

@ -0,0 +1,54 @@
/**
* 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 * as Joi from '@hapi/joi';
import {PluginOptions} from './types';
const REVERSED_DOCS_HOME_PAGE_ID = '_index';
export const DEFAULT_OPTIONS: PluginOptions = {
path: 'docs', // Path to data on filesystem, relative to site dir.
routeBasePath: 'docs', // URL Route.
homePageId: REVERSED_DOCS_HOME_PAGE_ID, // Document id for docs home page.
include: ['**/*.{md,mdx}'], // Extensions to include.
sidebarPath: '', // Path to sidebar configuration for showing a list of markdown pages.
docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem',
remarkPlugins: [],
rehypePlugins: [],
showLastUpdateTime: false,
showLastUpdateAuthor: false,
admonitions: {},
excludeNextVersionDocs: false,
};
export const PluginOptionSchema = Joi.object({
path: Joi.string().default(DEFAULT_OPTIONS.path),
editUrl: Joi.string().uri(),
routeBasePath: Joi.string().default(DEFAULT_OPTIONS.routeBasePath),
homePageId: Joi.string().default(DEFAULT_OPTIONS.homePageId),
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
sidebarPath: Joi.string().default(DEFAULT_OPTIONS.sidebarPath),
docLayoutComponent: Joi.string().default(DEFAULT_OPTIONS.docLayoutComponent),
docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent),
remarkPlugins: Joi.array()
.items(
Joi.array().items(Joi.function(), Joi.object()).length(2),
Joi.function(),
)
.default(DEFAULT_OPTIONS.remarkPlugins),
rehypePlugins: Joi.array()
.items(Joi.string())
.default(DEFAULT_OPTIONS.rehypePlugins),
showLastUpdateTime: Joi.bool().default(DEFAULT_OPTIONS.showLastUpdateTime),
showLastUpdateAuthor: Joi.bool().default(
DEFAULT_OPTIONS.showLastUpdateAuthor,
),
admonitions: Joi.object().default(DEFAULT_OPTIONS.admonitions),
excludeNextVersionDocs: Joi.bool().default(
DEFAULT_OPTIONS.excludeNextVersionDocs,
),
});

View file

@ -11,10 +11,14 @@
"access": "public" "access": "public"
}, },
"license": "MIT", "license": "MIT",
"devDependencies": {
"@types/hapi__joi": "^17.1.2"
},
"dependencies": { "dependencies": {
"@docusaurus/types": "^2.0.0-alpha.58", "@docusaurus/types": "^2.0.0-alpha.58",
"@docusaurus/utils": "^2.0.0-alpha.58", "@docusaurus/utils": "^2.0.0-alpha.58",
"globby": "^10.0.1" "globby": "^10.0.1",
"@hapi/joi": "17.1.1"
}, },
"peerDependencies": { "peerDependencies": {
"@docusaurus/core": "^2.0.0", "@docusaurus/core": "^2.0.0",

View file

@ -9,6 +9,7 @@ import path from 'path';
import pluginContentPages from '../index'; import pluginContentPages from '../index';
import {LoadContext} from '@docusaurus/types'; import {LoadContext} from '@docusaurus/types';
import normalizePluginOptions from './pluginOptionSchema.test';
describe('docusaurus-plugin-content-pages', () => { describe('docusaurus-plugin-content-pages', () => {
test('simple pages', async () => { test('simple pages', async () => {
@ -23,9 +24,12 @@ describe('docusaurus-plugin-content-pages', () => {
siteConfig, siteConfig,
} as LoadContext; } as LoadContext;
const pluginPath = 'src/pages'; const pluginPath = 'src/pages';
const plugin = pluginContentPages(context, { const plugin = pluginContentPages(
path: pluginPath, context,
}); normalizePluginOptions({
path: pluginPath,
}),
);
const pagesMetadatas = await plugin.loadContent(); const pagesMetadatas = await plugin.loadContent();
expect(pagesMetadatas).toEqual([ expect(pagesMetadatas).toEqual([

View file

@ -0,0 +1,49 @@
/**
* 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 {PluginOptionSchema, DEFAULT_OPTIONS} from '../pluginOptionSchema';
export default function normalizePluginOptions(options) {
const {value, error} = PluginOptionSchema.validate(options, {
convert: false,
});
if (error) {
throw error;
} else {
return value;
}
}
describe('normalizePagesPluginOptions', () => {
test('should return default options for undefined user options', async () => {
const {value} = await PluginOptionSchema.validate({});
expect(value).toEqual(DEFAULT_OPTIONS);
});
test('should fill in default options for partially defined user options', async () => {
const {value} = await PluginOptionSchema.validate({path: 'src/pages'});
expect(value).toEqual(DEFAULT_OPTIONS);
});
test('should accept correctly defined user options', async () => {
const userOptions = {
path: 'src/my-pages',
routeBasePath: 'my-pages',
include: ['**/*.{js,jsx,ts,tsx}'],
};
const {value} = await PluginOptionSchema.validate(userOptions);
expect(value).toEqual(userOptions);
});
test('should reject bad path inputs', () => {
expect(() => {
normalizePluginOptions({
path: 42,
});
}).toThrowErrorMatchingInlineSnapshot(`"\\"path\\" must be a string"`);
});
});

View file

@ -9,21 +9,21 @@ import globby from 'globby';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {encodePath, fileToPath, aliasedSitePath} from '@docusaurus/utils'; import {encodePath, fileToPath, aliasedSitePath} from '@docusaurus/utils';
import {LoadContext, Plugin} from '@docusaurus/types'; import {
LoadContext,
Plugin,
OptionValidationContext,
ValidationResult,
} from '@docusaurus/types';
import {PluginOptions, LoadedContent} from './types'; import {PluginOptions, LoadedContent} from './types';
import {PluginOptionSchema} from './pluginOptionSchema';
const DEFAULT_OPTIONS: PluginOptions = { import {ValidationError} from '@hapi/joi';
path: 'src/pages', // Path to data on filesystem, relative to site dir.
routeBasePath: '', // URL Route.
include: ['**/*.{js,jsx,ts,tsx}'], // Extensions to include.
};
export default function pluginContentPages( export default function pluginContentPages(
context: LoadContext, context: LoadContext,
opts: Partial<PluginOptions>, options: PluginOptions,
): Plugin<LoadedContent | null> { ): Plugin<LoadedContent | null, typeof PluginOptionSchema> {
const options = {...DEFAULT_OPTIONS, ...opts};
const contentPath = path.resolve(context.siteDir, options.path); const contentPath = path.resolve(context.siteDir, options.path);
return { return {
@ -81,3 +81,14 @@ export default function pluginContentPages(
}, },
}; };
} }
export function validateOptions({
validate,
options,
}: OptionValidationContext<PluginOptions, ValidationError>): ValidationResult<
PluginOptions,
ValidationError
> {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}

View file

@ -0,0 +1,20 @@
/**
* 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 * as Joi from '@hapi/joi';
import {PluginOptions} from './types';
export const DEFAULT_OPTIONS: PluginOptions = {
path: 'src/pages', // Path to data on filesystem, relative to site dir.
routeBasePath: '', // URL Route.
include: ['**/*.{js,jsx,ts,tsx}'], // Extensions to include.
};
export const PluginOptionSchema = Joi.object({
path: Joi.string().default(DEFAULT_OPTIONS.path),
routeBasePath: Joi.string().default(DEFAULT_OPTIONS.routeBasePath),
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
});

View file

@ -11,10 +11,14 @@
"access": "public" "access": "public"
}, },
"license": "MIT", "license": "MIT",
"devDependencies": {
"@types/hapi__joi": "^17.1.2"
},
"dependencies": { "dependencies": {
"@docusaurus/types": "^2.0.0-alpha.58", "@docusaurus/types": "^2.0.0-alpha.58",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"sitemap": "^3.2.2" "sitemap": "^3.2.2",
"@hapi/joi": "17.1.1"
}, },
"peerDependencies": { "peerDependencies": {
"@docusaurus/core": "^2.0.0" "@docusaurus/core": "^2.0.0"

View file

@ -0,0 +1,64 @@
/**
* 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 {PluginOptionSchema, DEFAULT_OPTIONS} from '../pluginOptionSchema';
function normalizePluginOptions(options) {
const {value, error} = PluginOptionSchema.validate(options, {
convert: false,
});
if (error) {
throw error;
} else {
return value;
}
}
describe('normalizeSitemapPluginOptions', () => {
test('should return default values for empty user options', async () => {
const {value} = await PluginOptionSchema.validate({});
expect(value).toEqual(DEFAULT_OPTIONS);
});
test('should accept correctly defined user options', async () => {
const userOptions = {
cacheTime: 300,
changefreq: 'yearly',
priority: 0.9,
};
const {value} = await PluginOptionSchema.validate(userOptions);
expect(value).toEqual(userOptions);
});
test('should reject cacheTime inputs with wrong type', () => {
expect(() => {
normalizePluginOptions({
cacheTime: '42',
});
}).toThrowErrorMatchingInlineSnapshot(`"\\"cacheTime\\" must be a number"`);
});
test('should reject out-of-range priority inputs', () => {
expect(() => {
normalizePluginOptions({
priority: 2,
});
}).toThrowErrorMatchingInlineSnapshot(
`"\\"priority\\" must be less than or equal to 1"`,
);
});
test('should reject bad changefreq inputs', () => {
expect(() => {
normalizePluginOptions({
changefreq: 'annually',
});
}).toThrowErrorMatchingInlineSnapshot(
`"\\"changefreq\\" must be one of [always, hourly, daily, weekly, monthly, yearly, never]"`,
);
});
});

View file

@ -9,20 +9,20 @@ import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import {PluginOptions} from './types'; import {PluginOptions} from './types';
import createSitemap from './createSitemap'; import createSitemap from './createSitemap';
import {LoadContext, Props, Plugin} from '@docusaurus/types'; import {
LoadContext,
const DEFAULT_OPTIONS: Required<PluginOptions> = { Props,
cacheTime: 600 * 1000, // 600 sec - cache purge period. OptionValidationContext,
changefreq: 'weekly', ValidationResult,
priority: 0.5, Plugin,
}; } from '@docusaurus/types';
import {PluginOptionSchema} from './pluginOptionSchema';
import {ValidationError} from '@hapi/joi';
export default function pluginSitemap( export default function pluginSitemap(
_context: LoadContext, _context: LoadContext,
opts: Partial<PluginOptions>, options: PluginOptions,
): Plugin<void> { ): Plugin<void> {
const options = {...DEFAULT_OPTIONS, ...opts};
return { return {
name: 'docusaurus-plugin-sitemap', name: 'docusaurus-plugin-sitemap',
@ -44,3 +44,14 @@ export default function pluginSitemap(
}, },
}; };
} }
export function validateOptions({
validate,
options,
}: OptionValidationContext<PluginOptions, ValidationError>): ValidationResult<
PluginOptions,
ValidationError
> {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}

View file

@ -0,0 +1,22 @@
/**
* 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 * as Joi from '@hapi/joi';
import {PluginOptions} from './types';
export const DEFAULT_OPTIONS: Required<PluginOptions> = {
cacheTime: 600 * 1000, // 600 sec - cache purge period.
changefreq: 'weekly',
priority: 0.5,
};
export const PluginOptionSchema = Joi.object({
cacheTime: Joi.number().positive().default(DEFAULT_OPTIONS.cacheTime),
changefreq: Joi.string()
.valid('always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never')
.default(DEFAULT_OPTIONS.changefreq),
priority: Joi.number().min(0).max(1).default(DEFAULT_OPTIONS.priority),
});

View file

@ -13,8 +13,7 @@
"clsx": "^1.1.1", "clsx": "^1.1.1",
"parse-numeric-range": "^0.0.2", "parse-numeric-range": "^0.0.2",
"prism-react-renderer": "^1.1.0", "prism-react-renderer": "^1.1.0",
"react-live": "^2.2.1", "react-live": "^2.2.1"
"yup": "^0.29.1"
}, },
"peerDependencies": { "peerDependencies": {
"@docusaurus/core": "^2.0.0", "@docusaurus/core": "^2.0.0",

View file

@ -211,7 +211,7 @@ export interface ValidationResult<T, E extends Error = Error> {
} }
export type Validate<T, E extends Error = Error> = ( export type Validate<T, E extends Error = Error> = (
validationSchrema: ValidationSchema<T>, validationSchema: ValidationSchema<T>,
options: Partial<T>, options: Partial<T>,
) => ValidationResult<T, E>; ) => ValidationResult<T, E>;

View file

@ -1172,13 +1172,6 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.9.6":
version "7.9.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f"
integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
version "7.8.6" version "7.8.6"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
@ -1392,17 +1385,7 @@
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.0.4.tgz#e80ad4e8e8d2adc6c77d985f698447e8628b6010" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.0.4.tgz#e80ad4e8e8d2adc6c77d985f698447e8628b6010"
integrity sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw== integrity sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw==
"@hapi/joi@^15.1.0": "@hapi/joi@17.1.1", "@hapi/joi@^17.1.1":
version "15.1.1"
resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7"
integrity sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==
dependencies:
"@hapi/address" "2.x.x"
"@hapi/bourne" "1.x.x"
"@hapi/hoek" "8.x.x"
"@hapi/topo" "3.x.x"
"@hapi/joi@^17.1.1":
version "17.1.1" version "17.1.1"
resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-17.1.1.tgz#9cc8d7e2c2213d1e46708c6260184b447c661350" resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-17.1.1.tgz#9cc8d7e2c2213d1e46708c6260184b447c661350"
integrity sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg== integrity sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg==
@ -1413,6 +1396,16 @@
"@hapi/pinpoint" "^2.0.0" "@hapi/pinpoint" "^2.0.0"
"@hapi/topo" "^5.0.0" "@hapi/topo" "^5.0.0"
"@hapi/joi@^15.1.0":
version "15.1.1"
resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7"
integrity sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==
dependencies:
"@hapi/address" "2.x.x"
"@hapi/bourne" "1.x.x"
"@hapi/hoek" "8.x.x"
"@hapi/topo" "3.x.x"
"@hapi/pinpoint@^2.0.0": "@hapi/pinpoint@^2.0.0":
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.0.tgz#805b40d4dbec04fc116a73089494e00f073de8df" resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.0.tgz#805b40d4dbec04fc116a73089494e00f073de8df"
@ -3272,11 +3265,6 @@
dependencies: dependencies:
"@types/yargs-parser" "*" "@types/yargs-parser" "*"
"@types/yup@^0.29.0":
version "0.29.0"
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.0.tgz#0918ec503dfacb19d0b3cca0195b9f3441f46685"
integrity sha512-E9RTXPD4x44qBOvY6TjUqdkR9FNV9cACWlnAsooUInDqtLZz9M9oYXKn/w1GHNxRvyYyHuG6Bfjbg3QlK+SgXw==
"@typescript-eslint/eslint-plugin@^3.3.0": "@typescript-eslint/eslint-plugin@^3.3.0":
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.3.0.tgz#89518e5c5209a349bde161c3489b0ec187ae5d37" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.3.0.tgz#89518e5c5209a349bde161c3489b0ec187ae5d37"
@ -8176,11 +8164,6 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3" inherits "^2.0.3"
readable-stream "^2.3.6" readable-stream "^2.3.6"
fn-name@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c"
integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==
follow-redirects@^1.0.0: follow-redirects@^1.0.0:
version "1.9.0" version "1.9.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f"
@ -11332,11 +11315,6 @@ locate-path@^5.0.0:
dependencies: dependencies:
p-locate "^4.1.0" p-locate "^4.1.0"
lodash-es@^4.17.11:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
lodash._reinterpolate@^3.0.0: lodash._reinterpolate@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@ -14633,11 +14611,6 @@ prop-types@^15.0.0, prop-types@^15.5.0, prop-types@^15.5.4, prop-types@^15.5.8,
object-assign "^4.1.1" object-assign "^4.1.1"
react-is "^16.8.1" react-is "^16.8.1"
property-expr@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.2.tgz#fff2a43919135553a3bc2fdd94bdb841965b2330"
integrity sha512-bc/5ggaYZxNkFKj374aLbEDqVADdYaLcFo8XBkishUWbaAdjlphaBFns9TvRA2pUseVL/wMFmui9X3IdNDU37g==
property-information@^5.0.0, property-information@^5.3.0: property-information@^5.0.0, property-information@^5.3.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.3.0.tgz#bc87ac82dc4e72a31bb62040544b1bf9653da039" resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.3.0.tgz#bc87ac82dc4e72a31bb62040544b1bf9653da039"
@ -17310,11 +17283,6 @@ symbol-tree@^3.2.2:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
synchronous-promise@^2.0.10:
version "2.0.13"
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.13.tgz#9d8c165ddee69c5a6542862b405bc50095926702"
integrity sha512-R9N6uDkVsghHePKh1TEqbnLddO2IY25OcsksyFp/qBe7XYd0PVbKEWxhcdMhpLzE1I6skj5l4aEZ3CRxcbArlA==
table@^5.2.3, table@^5.4.6: table@^5.2.3, table@^5.4.6:
version "5.4.6" version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
@ -17694,11 +17662,6 @@ toml@^2.3.2:
resolved "https://registry.yarnpkg.com/toml/-/toml-2.3.6.tgz#25b0866483a9722474895559088b436fd11f861b" resolved "https://registry.yarnpkg.com/toml/-/toml-2.3.6.tgz#25b0866483a9722474895559088b436fd11f861b"
integrity sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ== integrity sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ==
toposort@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
touch@^3.1.0: touch@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
@ -19132,32 +19095,6 @@ yauzl@^2.4.2:
buffer-crc32 "~0.2.3" buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0" fd-slicer "~1.1.0"
yup@^0.29.0:
version "0.29.0"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.0.tgz#c0670897b2ebcea42ebde12b3567f55ea3a7acaf"
integrity sha512-rXPkxhMIVPsQ6jZXPRcO+nc+AIT+BBo3012pmiEos2RSrPxAq1LyspZyK7l14ahcXuiKQnEHI0H5bptI47v5Tw==
dependencies:
"@babel/runtime" "^7.9.6"
fn-name "~3.0.0"
lodash "^4.17.15"
lodash-es "^4.17.11"
property-expr "^2.0.2"
synchronous-promise "^2.0.10"
toposort "^2.0.2"
yup@^0.29.1:
version "0.29.1"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.1.tgz#35d25aab470a0c3950f66040ba0ff4b1b6efe0d9"
integrity sha512-U7mPIbgfQWI6M3hZCJdGFrr+U0laG28FxMAKIgNvgl7OtyYuUoc4uy9qCWYHZjh49b8T7Ug8NNDdiMIEytcXrQ==
dependencies:
"@babel/runtime" "^7.9.6"
fn-name "~3.0.0"
lodash "^4.17.15"
lodash-es "^4.17.11"
property-expr "^2.0.2"
synchronous-promise "^2.0.10"
toposort "^2.0.2"
zepto@^1.2.0: zepto@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/zepto/-/zepto-1.2.0.tgz#e127bd9e66fd846be5eab48c1394882f7c0e4f98" resolved "https://registry.yarnpkg.com/zepto/-/zepto-1.2.0.tgz#e127bd9e66fd846be5eab48c1394882f7c0e4f98"