mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-04 01:09:20 +02:00
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:
parent
ec3c281952
commit
3213955e72
8 changed files with 325 additions and 102 deletions
|
@ -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",
|
||||
|
|
|
@ -18,7 +18,6 @@ module.exports = {
|
|||
'@docusaurus/plugin-content-docs',
|
||||
{
|
||||
path: '../docs',
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
},
|
||||
],
|
||||
'@docusaurus/plugin-content-pages',
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
`;
|
|
@ -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
|
||||
"
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
|
|
113
packages/docusaurus/src/server/configValidation.ts
Normal file
113
packages/docusaurus/src/server/configValidation.ts
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue