Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 7 commits into from
Jun 26, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/docusaurus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ module.exports = {
'@docusaurus/plugin-content-docs',
{
path: '../docs',
sidebarPath: require.resolve('./sidebars.js'),
},
],
'@docusaurus/plugin-content-pages',
Expand Down
Original file line number Diff line number Diff line change
@@ -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`] = `
"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\\"favicon\\" is required
"
`;

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",
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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
"
`;

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
"
`;
31 changes: 4 additions & 27 deletions packages/docusaurus/src/server/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,51 +13,28 @@ 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({});
});

test('website with incomplete siteConfig', () => {
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();
});
});
102 changes: 102 additions & 0 deletions packages/docusaurus/src/server/__tests__/configValidation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* 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';

const baseConfig = {
baseUrl: '/',
favicon: 'some.ico',
title: 'my site',
url: 'https://mysite.com',
};

const testConfig = (config) => validateConfig({...baseConfig, ...config});

describe('validateConfig', () => {
slorber marked this conversation as resolved.
Show resolved Hide resolved
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({})).toThrowErrorMatchingSnapshot();
});
});
76 changes: 3 additions & 73 deletions packages/docusaurus/src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
Loading