chore(v2): use joi for config validation (#2987)

* use joi for  validation

* fix theme validation

* add test for required fields

* format errors

* a little better format errors

* fix config file

* try to rerun action
This commit is contained in:
Anshul Goyal 2020-06-26 18:24:33 +05:30 committed by GitHub
parent ec3c281952
commit 3213955e72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 325 additions and 102 deletions

View file

@ -30,7 +30,8 @@
"url": "https://github.com/facebook/docusaurus/issues"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^2.0.0-alpha.58"
"@docusaurus/module-type-aliases": "^2.0.0-alpha.58",
"@types/hapi__joi": "^17.1.2"
},
"dependencies": {
"@babel/core": "^7.9.0",
@ -43,6 +44,7 @@
"@docusaurus/types": "^2.0.0-alpha.58",
"@docusaurus/utils": "^2.0.0-alpha.58",
"@endiliey/static-site-generator-webpack-plugin": "^4.0.0",
"@hapi/joi": "^17.1.1",
"@svgr/webpack": "^5.4.0",
"babel-loader": "^8.1.0",
"babel-plugin-dynamic-import-node": "^2.3.0",

View file

@ -18,7 +18,6 @@ module.exports = {
'@docusaurus/plugin-content-docs',
{
path: '../docs',
sidebarPath: require.resolve('./sidebars.js'),
},
],
'@docusaurus/plugin-content-pages',

View file

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loadConfig website with incomplete siteConfig 1`] = `
"\\"favicon\\" is required
\\"url\\" is required
"
`;
exports[`loadConfig website with no siteConfig 1`] = `"docusaurus.config.js not found"`;
exports[`loadConfig website with useless field (wrong field) in siteConfig 1`] = `
"\\"favicon\\" is required
These field(s) [\\"useLessField\\",] are not recognized in docusaurus.config.js.
If you still want these fields to be in your configuration, put them in the 'customFields' attribute.
See https://v2.docusaurus.io/docs/docusaurus.config.js/#customfields"
`;
exports[`loadConfig website with valid siteConfig 1`] = `
Object {
"baseUrl": "/",
"customFields": Object {},
"favicon": "img/docusaurus.ico",
"organizationName": "endiliey",
"plugins": Array [
Array [
"@docusaurus/plugin-content-docs",
Object {
"path": "../docs",
},
],
"@docusaurus/plugin-content-pages",
],
"projectName": "hello",
"tagline": "Hello World",
"themeConfig": Object {},
"themes": Array [],
"title": "Hello",
"url": "https://docusaurus.io",
}
`;

View file

@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`validateConfig throw error for baseUrl without trailing \`/\` 1`] = `
"\\"baseUrl\\" must be a string with a trailing \`/\`
"
`;
exports[`validateConfig throw error for required fields 1`] = `
"\\"baseUrl\\" is required
\\"favicon\\" is required
\\"title\\" is required
\\"url\\" is required
\\"themes\\" must be an array
\\"scripts\\" must be an array
\\"stylesheets\\" must be an array
These field(s) [\\"invalid\\",\\"preset\\",] are not recognized in docusaurus.config.js.
If you still want these fields to be in your configuration, put them in the 'customFields' attribute.
See https://v2.docusaurus.io/docs/docusaurus.config.js/#customfields"
`;
exports[`validateConfig throw error for unknown field 1`] = `
"These field(s) [\\"invalid\\",] are not recognized in docusaurus.config.js.
If you still want these fields to be in your configuration, put them in the 'customFields' attribute.
See https://v2.docusaurus.io/docs/docusaurus.config.js/#customfields"
`;
exports[`validateConfig throw error if css doesn't have href 1`] = `
"\\"stylesheets[1]\\" does not match any of the allowed types
"
`;
exports[`validateConfig throw error if plugins is not array 1`] = `
"\\"plugins\\" must be an array
"
`;
exports[`validateConfig throw error if presets is not array 1`] = `
"\\"presets\\" must be an array
"
`;
exports[`validateConfig throw error if scripts doesn't have src 1`] = `
"\\"scripts[1]\\" does not match any of the allowed types
"
`;
exports[`validateConfig throw error if themes is not array 1`] = `
"\\"themes\\" must be an array
"
`;

View file

@ -13,26 +13,7 @@ describe('loadConfig', () => {
const fixtures = path.join(__dirname, '__fixtures__');
const siteDir = path.join(fixtures, 'simple-site');
const config = loadConfig(siteDir);
expect(config).toMatchInlineSnapshot(
{
plugins: expect.any(Array),
},
`
Object {
"baseUrl": "/",
"customFields": Object {},
"favicon": "img/docusaurus.ico",
"organizationName": "endiliey",
"plugins": Any<Array>,
"projectName": "hello",
"tagline": "Hello World",
"themeConfig": Object {},
"themes": Array [],
"title": "Hello",
"url": "https://docusaurus.io",
}
`,
);
expect(config).toMatchSnapshot();
expect(config).not.toEqual({});
});
@ -40,24 +21,20 @@ describe('loadConfig', () => {
const siteDir = path.join(__dirname, '__fixtures__', 'bad-site');
expect(() => {
loadConfig(siteDir);
}).toThrowErrorMatchingInlineSnapshot(
`"The required field(s) 'favicon', 'url' are missing from docusaurus.config.js"`,
);
}).toThrowErrorMatchingSnapshot();
});
test('website with useless field (wrong field) in siteConfig', () => {
const siteDir = path.join(__dirname, '__fixtures__', 'wrong-site');
expect(() => {
loadConfig(siteDir);
}).toThrowErrorMatchingInlineSnapshot(
`"The required field(s) 'favicon' are missing from docusaurus.config.js"`,
);
}).toThrowErrorMatchingSnapshot();
});
test('website with no siteConfig', () => {
const siteDir = path.join(__dirname, '__fixtures__', 'nonExisting');
expect(() => {
loadConfig(siteDir);
}).toThrowErrorMatchingInlineSnapshot(`"docusaurus.config.js not found"`);
}).toThrowErrorMatchingSnapshot();
});
});

View file

@ -0,0 +1,112 @@
/**
* 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 {DEFAULT_CONFIG, validateConfig} from '../configValidation';
import {DocusaurusConfig} from '@docusaurus/types';
const baseConfig = {
baseUrl: '/',
favicon: 'some.ico',
title: 'my site',
url: 'https://mysite.com',
};
const testConfig = (config) => validateConfig({...baseConfig, ...config});
describe('validateConfig', () => {
test('normalize config', () => {
const value = testConfig({});
expect(value).toEqual({
...DEFAULT_CONFIG,
...baseConfig,
});
});
test('throw error for unknown field', () => {
expect(() => {
testConfig({
invalid: true,
});
}).toThrowErrorMatchingSnapshot();
});
test('throw error for baseUrl without trailing `/`', () => {
expect(() => {
testConfig({
baseUrl: 'noslash',
});
}).toThrowErrorMatchingSnapshot();
});
test('throw error if plugins is not array', () => {
expect(() => {
testConfig({
plugins: {},
});
}).toThrowErrorMatchingSnapshot();
});
test('throw error if themes is not array', () => {
expect(() => {
testConfig({
themes: {},
});
}).toThrowErrorMatchingSnapshot();
});
test('throw error if presets is not array', () => {
expect(() => {
testConfig({
presets: {},
});
}).toThrowErrorMatchingSnapshot();
});
test("throw error if scripts doesn't have src", () => {
expect(() => {
testConfig({
scripts: ['https://some.com', {}],
});
}).toThrowErrorMatchingSnapshot();
});
test("throw error if css doesn't have href", () => {
expect(() => {
testConfig({
stylesheets: ['https://somescript.com', {type: 'text/css'}],
});
}).toThrowErrorMatchingSnapshot();
});
test('custom field in config', () => {
const value = testConfig({
customFields: {
author: 'anshul',
},
});
expect(value).toEqual({
...DEFAULT_CONFIG,
...baseConfig,
customFields: {
author: 'anshul',
},
});
});
test('throw error for required fields', () => {
expect(
() =>
validateConfig(({
invalid: true,
preset: {},
stylesheets: {},
themes: {},
scripts: {},
} as unknown) as DocusaurusConfig), // to fields not in the type
).toThrowErrorMatchingSnapshot();
});
});

View file

@ -7,46 +7,10 @@
import fs from 'fs-extra';
import importFresh from 'import-fresh';
import has from 'lodash.has';
import path from 'path';
import {DocusaurusConfig} from '@docusaurus/types';
import {CONFIG_FILE_NAME} from '../constants';
import {DocusaurusConfig, PluginConfig} from '@docusaurus/types';
const REQUIRED_FIELDS = ['baseUrl', 'favicon', 'title', 'url'];
const OPTIONAL_FIELDS = [
'organizationName',
'projectName',
'customFields',
'githubHost',
'plugins',
'themes',
'presets',
'themeConfig',
'scripts',
'stylesheets',
'tagline',
];
const DEFAULT_CONFIG: {
plugins: PluginConfig[];
themes: PluginConfig[];
customFields: {
[key: string]: unknown;
};
themeConfig: {
[key: string]: unknown;
};
} = {
plugins: [],
themes: [],
customFields: {},
themeConfig: {},
};
function formatFields(fields: string[]): string {
return fields.map((field) => `'${field}'`).join(', ');
}
import {validateConfig} from './configValidation';
export default function loadConfig(siteDir: string): DocusaurusConfig {
const configPath = path.resolve(siteDir, CONFIG_FILE_NAME);
@ -56,39 +20,5 @@ export default function loadConfig(siteDir: string): DocusaurusConfig {
}
const loadedConfig = importFresh(configPath) as Partial<DocusaurusConfig>;
const missingFields = REQUIRED_FIELDS.filter(
(field) => !has(loadedConfig, field),
);
if (missingFields.length > 0) {
throw new Error(
`The required field(s) ${formatFields(
missingFields,
)} are missing from ${CONFIG_FILE_NAME}`,
);
}
// Merge default config with loaded config.
const config: DocusaurusConfig = {
...DEFAULT_CONFIG,
...loadedConfig,
} as DocusaurusConfig;
// Don't allow unrecognized fields.
const allowedFields = [...REQUIRED_FIELDS, ...OPTIONAL_FIELDS];
const unrecognizedFields = Object.keys(config).filter(
(field) => !allowedFields.includes(field),
);
if (unrecognizedFields && unrecognizedFields.length > 0) {
throw new Error(
`The field(s) ${formatFields(
unrecognizedFields,
)} are not recognized in ${CONFIG_FILE_NAME}.
If you still want these fields to be in your configuration, put them in the 'customFields' attribute.
See https://v2.docusaurus.io/docs/docusaurus.config.js/#customfields`,
);
}
return config;
return validateConfig(loadedConfig);
}

View file

@ -0,0 +1,113 @@
/**
* 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 {PluginConfig, DocusaurusConfig} from '@docusaurus/types';
import Joi from '@hapi/joi';
import {CONFIG_FILE_NAME} from '../constants';
export const DEFAULT_CONFIG: {
plugins: PluginConfig[];
themes: PluginConfig[];
customFields: {
[key: string]: unknown;
};
themeConfig: {
[key: string]: unknown;
};
} = {
plugins: [],
themes: [],
customFields: {},
themeConfig: {},
};
const ConfigSchema = Joi.object({
baseUrl: Joi.string()
.required()
.regex(new RegExp('/$', 'm'))
.message('{{#label}} must be a string with a trailing `/`'),
favicon: Joi.string().required(),
title: Joi.string().required(),
url: Joi.string().uri().required(),
organizationName: Joi.string(),
projectName: Joi.string(),
customFields: Joi.object().unknown().default(DEFAULT_CONFIG.customFields),
githubHost: Joi.string(),
plugins: Joi.array()
.items(
Joi.alternatives().try(
Joi.string(),
Joi.array()
.items(Joi.string().required(), Joi.object().required())
.length(2),
),
)
.default(DEFAULT_CONFIG.plugins),
themes: Joi.array()
.items(
Joi.alternatives().try(
Joi.string(),
Joi.array()
.items(Joi.string().required(), Joi.object().required())
.length(2),
),
)
.default(DEFAULT_CONFIG.themes),
presets: Joi.array().items(
Joi.alternatives().try(
Joi.string(),
Joi.array().items(Joi.string(), Joi.object()).length(2),
),
),
themeConfig: Joi.object().unknown().default(DEFAULT_CONFIG.themeConfig),
scripts: Joi.array().items(
Joi.string(),
Joi.object({
src: Joi.string().required(),
async: Joi.bool(),
defer: Joi.bool(),
}).oxor('async', 'defer'),
),
stylesheets: Joi.array().items(
Joi.string(),
Joi.object({
href: Joi.string().uri().required(),
type: Joi.string().required(),
}),
),
tagline: Joi.string(),
});
export function validateConfig(
config: Partial<DocusaurusConfig>,
): DocusaurusConfig {
const {error, value} = ConfigSchema.validate(config, {
abortEarly: false,
});
if (error) {
const unknownFields = error.details.reduce((formatedError, err) => {
if (err.type === 'object.unknown') {
return `${formatedError}"${err.path}",`;
}
return formatedError;
}, '');
let formatedError = error.details.reduce(
(accumalatedErr, err) =>
err.type !== 'object.unknown'
? `${accumalatedErr}${err.message}\n`
: accumalatedErr,
'',
);
formatedError = unknownFields
? `${formatedError}These field(s) [${unknownFields}] are not recognized in ${CONFIG_FILE_NAME}.\nIf you still want these fields to be in your configuration, put them in the 'customFields' attribute.\nSee https://v2.docusaurus.io/docs/docusaurus.config.js/#customfields`
: formatedError;
throw new Error(formatedError);
} else {
return value;
}
}