mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-19 12:07:00 +02:00
refactor(utils): reorganize functions; move authors file resolution to utils (#6229)
* refactor(utils): reorganize functions; move authors file resolution to utils * More refactor
This commit is contained in:
parent
7adc1c0cdb
commit
24d65d9bdd
39 changed files with 533 additions and 747 deletions
|
@ -27,7 +27,6 @@
|
|||
"feed": "^4.2.2",
|
||||
"fs-extra": "^10.0.0",
|
||||
"globby": "^11.0.2",
|
||||
"js-yaml": "^4.0.0",
|
||||
"loader-utils": "^2.0.0",
|
||||
"lodash": "^4.17.20",
|
||||
"reading-time": "^1.5.0",
|
||||
|
|
|
@ -6,12 +6,10 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
AuthorsMap,
|
||||
getAuthorsMapFilePath,
|
||||
validateAuthorsMapFile,
|
||||
readAuthorsMapFile,
|
||||
type AuthorsMap,
|
||||
getAuthorsMap,
|
||||
getBlogPostAuthors,
|
||||
validateAuthorsMap,
|
||||
} from '../authors';
|
||||
import path from 'path';
|
||||
|
||||
|
@ -282,80 +280,6 @@ describe('getBlogPostAuthors', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('readAuthorsMapFile', () => {
|
||||
const fixturesDir = path.join(__dirname, '__fixtures__/authorsMapFiles');
|
||||
|
||||
test('read valid yml author file', async () => {
|
||||
const filePath = path.join(fixturesDir, 'authors.yml');
|
||||
expect(await readAuthorsMapFile(filePath)).toBeDefined();
|
||||
});
|
||||
|
||||
test('read valid json author file', async () => {
|
||||
const filePath = path.join(fixturesDir, 'authors.json');
|
||||
expect(await readAuthorsMapFile(filePath)).toBeDefined();
|
||||
});
|
||||
|
||||
test('read yml and json should lead to the same result', async () => {
|
||||
const content1 = await readAuthorsMapFile(
|
||||
path.join(fixturesDir, 'authors.yml'),
|
||||
);
|
||||
const content2 = await readAuthorsMapFile(
|
||||
path.join(fixturesDir, 'authors.json'),
|
||||
);
|
||||
expect(content1).toEqual(content2);
|
||||
});
|
||||
|
||||
test('fail to read invalid yml 1', async () => {
|
||||
const filePath = path.join(fixturesDir, 'authorsBad1.yml');
|
||||
await expect(
|
||||
readAuthorsMapFile(filePath),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"slorber.name\\" is required"`,
|
||||
);
|
||||
});
|
||||
test('fail to read invalid json 1', async () => {
|
||||
const filePath = path.join(fixturesDir, 'authorsBad1.json');
|
||||
await expect(
|
||||
readAuthorsMapFile(filePath),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"slorber.name\\" is required"`,
|
||||
);
|
||||
});
|
||||
|
||||
test('fail to read invalid yml 2', async () => {
|
||||
const filePath = path.join(fixturesDir, 'authorsBad2.yml');
|
||||
await expect(
|
||||
readAuthorsMapFile(filePath),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"name\\" must be of type object"`,
|
||||
);
|
||||
});
|
||||
test('fail to read invalid json 2', async () => {
|
||||
const filePath = path.join(fixturesDir, 'authorsBad2.json');
|
||||
await expect(
|
||||
readAuthorsMapFile(filePath),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"name\\" must be of type object"`,
|
||||
);
|
||||
});
|
||||
|
||||
test('fail to read invalid yml 3', async () => {
|
||||
const filePath = path.join(fixturesDir, 'authorsBad3.yml');
|
||||
await expect(
|
||||
readAuthorsMapFile(filePath),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"value\\" must be of type object"`,
|
||||
);
|
||||
});
|
||||
test('fail to read invalid json 3', async () => {
|
||||
const filePath = path.join(fixturesDir, 'authorsBad3.json');
|
||||
await expect(
|
||||
readAuthorsMapFile(filePath),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"value\\" must be of type object"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('getAuthorsMap', () => {
|
||||
const fixturesDir = path.join(__dirname, '__fixtures__/authorsMapFiles');
|
||||
const contentPaths = {
|
||||
|
@ -391,7 +315,7 @@ describe('getAuthorsMap', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('validateAuthorsMapFile', () => {
|
||||
describe('validateAuthorsMap', () => {
|
||||
test('accept valid authors map', () => {
|
||||
const authorsMap: AuthorsMap = {
|
||||
slorber: {
|
||||
|
@ -411,7 +335,7 @@ describe('validateAuthorsMapFile', () => {
|
|||
hello: new Date(),
|
||||
},
|
||||
};
|
||||
expect(validateAuthorsMapFile(authorsMap)).toEqual(authorsMap);
|
||||
expect(validateAuthorsMap(authorsMap)).toEqual(authorsMap);
|
||||
});
|
||||
|
||||
test('rename snake case image_url to camelCase imageURL', () => {
|
||||
|
@ -421,7 +345,7 @@ describe('validateAuthorsMapFile', () => {
|
|||
image_url: 'https://github.com/slorber.png',
|
||||
},
|
||||
};
|
||||
expect(validateAuthorsMapFile(authorsMap)).toEqual({
|
||||
expect(validateAuthorsMap(authorsMap)).toEqual({
|
||||
slorber: {
|
||||
name: 'Sébastien Lorber',
|
||||
imageURL: 'https://github.com/slorber.png',
|
||||
|
@ -436,13 +360,13 @@ describe('validateAuthorsMapFile', () => {
|
|||
},
|
||||
};
|
||||
expect(() =>
|
||||
validateAuthorsMapFile(authorsMap),
|
||||
validateAuthorsMap(authorsMap),
|
||||
).toThrowErrorMatchingInlineSnapshot(`"\\"slorber.name\\" is required"`);
|
||||
});
|
||||
|
||||
test('reject undefined author', () => {
|
||||
expect(() =>
|
||||
validateAuthorsMapFile({
|
||||
validateAuthorsMap({
|
||||
slorber: undefined,
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`"\\"slorber\\" is required"`);
|
||||
|
@ -450,7 +374,7 @@ describe('validateAuthorsMapFile', () => {
|
|||
|
||||
test('reject null author', () => {
|
||||
expect(() =>
|
||||
validateAuthorsMapFile({
|
||||
validateAuthorsMap({
|
||||
slorber: null,
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
|
@ -460,14 +384,14 @@ describe('validateAuthorsMapFile', () => {
|
|||
|
||||
test('reject array author', () => {
|
||||
expect(() =>
|
||||
validateAuthorsMapFile({slorber: []}),
|
||||
validateAuthorsMap({slorber: []}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"slorber\\" must be of type object"`,
|
||||
);
|
||||
});
|
||||
|
||||
test('reject array content', () => {
|
||||
expect(() => validateAuthorsMapFile([])).toThrowErrorMatchingInlineSnapshot(
|
||||
expect(() => validateAuthorsMap([])).toThrowErrorMatchingInlineSnapshot(
|
||||
// TODO improve this error message
|
||||
`"\\"value\\" must be of type object"`,
|
||||
);
|
||||
|
@ -475,7 +399,7 @@ describe('validateAuthorsMapFile', () => {
|
|||
|
||||
test('reject flat author', () => {
|
||||
expect(() =>
|
||||
validateAuthorsMapFile({name: 'Sébastien'}),
|
||||
validateAuthorsMap({name: 'Sébastien'}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
// TODO improve this error message
|
||||
`"\\"name\\" must be of type object"`,
|
||||
|
@ -488,121 +412,9 @@ describe('validateAuthorsMapFile', () => {
|
|||
slorber: [],
|
||||
};
|
||||
expect(() =>
|
||||
validateAuthorsMapFile(authorsMap),
|
||||
validateAuthorsMap(authorsMap),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"slorber\\" must be of type object"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAuthorsMapFilePath', () => {
|
||||
const fixturesDir = path.join(
|
||||
__dirname,
|
||||
'__fixtures__/getAuthorsMapFilePath',
|
||||
);
|
||||
const contentPathYml1 = path.join(fixturesDir, 'contentPathYml1');
|
||||
const contentPathYml2 = path.join(fixturesDir, 'contentPathYml2');
|
||||
const contentPathJson1 = path.join(fixturesDir, 'contentPathJson1');
|
||||
const contentPathJson2 = path.join(fixturesDir, 'contentPathJson2');
|
||||
const contentPathEmpty = path.join(fixturesDir, 'contentPathEmpty');
|
||||
const contentPathNestedYml = path.join(fixturesDir, 'contentPathNestedYml');
|
||||
|
||||
test('getAuthorsMapFilePath returns localized Yml path in priority', async () => {
|
||||
expect(
|
||||
await getAuthorsMapFilePath({
|
||||
authorsMapPath: 'authors.yml',
|
||||
contentPaths: {
|
||||
contentPathLocalized: contentPathYml1,
|
||||
contentPath: contentPathYml2,
|
||||
},
|
||||
}),
|
||||
).toEqual(path.join(contentPathYml1, 'authors.yml'));
|
||||
expect(
|
||||
await getAuthorsMapFilePath({
|
||||
authorsMapPath: 'authors.yml',
|
||||
contentPaths: {
|
||||
contentPathLocalized: contentPathYml2,
|
||||
contentPath: contentPathYml1,
|
||||
},
|
||||
}),
|
||||
).toEqual(path.join(contentPathYml2, 'authors.yml'));
|
||||
});
|
||||
|
||||
test('getAuthorsMapFilePath returns localized Json path in priority', async () => {
|
||||
expect(
|
||||
await getAuthorsMapFilePath({
|
||||
authorsMapPath: 'authors.json',
|
||||
contentPaths: {
|
||||
contentPathLocalized: contentPathJson1,
|
||||
contentPath: contentPathJson2,
|
||||
},
|
||||
}),
|
||||
).toEqual(path.join(contentPathJson1, 'authors.json'));
|
||||
expect(
|
||||
await getAuthorsMapFilePath({
|
||||
authorsMapPath: 'authors.json',
|
||||
contentPaths: {
|
||||
contentPathLocalized: contentPathJson2,
|
||||
contentPath: contentPathJson1,
|
||||
},
|
||||
}),
|
||||
).toEqual(path.join(contentPathJson2, 'authors.json'));
|
||||
});
|
||||
|
||||
test('getAuthorsMapFilePath returns unlocalized Yml path as fallback', async () => {
|
||||
expect(
|
||||
await getAuthorsMapFilePath({
|
||||
authorsMapPath: 'authors.yml',
|
||||
contentPaths: {
|
||||
contentPathLocalized: contentPathEmpty,
|
||||
contentPath: contentPathYml2,
|
||||
},
|
||||
}),
|
||||
).toEqual(path.join(contentPathYml2, 'authors.yml'));
|
||||
});
|
||||
|
||||
test('getAuthorsMapFilePath returns unlocalized Json path as fallback', async () => {
|
||||
expect(
|
||||
await getAuthorsMapFilePath({
|
||||
authorsMapPath: 'authors.json',
|
||||
contentPaths: {
|
||||
contentPathLocalized: contentPathEmpty,
|
||||
contentPath: contentPathJson1,
|
||||
},
|
||||
}),
|
||||
).toEqual(path.join(contentPathJson1, 'authors.json'));
|
||||
});
|
||||
|
||||
test('getAuthorsMapFilePath can return undefined (file not found)', async () => {
|
||||
expect(
|
||||
await getAuthorsMapFilePath({
|
||||
authorsMapPath: 'authors.json',
|
||||
contentPaths: {
|
||||
contentPathLocalized: contentPathEmpty,
|
||||
contentPath: contentPathYml1,
|
||||
},
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
await getAuthorsMapFilePath({
|
||||
authorsMapPath: 'authors.yml',
|
||||
contentPaths: {
|
||||
contentPathLocalized: contentPathEmpty,
|
||||
contentPath: contentPathJson1,
|
||||
},
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getAuthorsMapFilePath can return nested path', async () => {
|
||||
expect(
|
||||
await getAuthorsMapFilePath({
|
||||
authorsMapPath: 'sub/folder/authors.yml',
|
||||
contentPaths: {
|
||||
contentPathLocalized: contentPathEmpty,
|
||||
contentPath: contentPathNestedYml,
|
||||
},
|
||||
}),
|
||||
).toEqual(path.join(contentPathNestedYml, 'sub/folder/authors.yml'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,19 +5,14 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import logger from '@docusaurus/logger';
|
||||
import path from 'path';
|
||||
import {Author, BlogContentPaths} from './types';
|
||||
import {findFolderContainingFile} from '@docusaurus/utils';
|
||||
import {getDataFileData} from '@docusaurus/utils';
|
||||
import {Joi, URISchema} from '@docusaurus/utils-validation';
|
||||
import {
|
||||
BlogPostFrontMatter,
|
||||
BlogPostFrontMatterAuthor,
|
||||
BlogPostFrontMatterAuthors,
|
||||
} from './blogFrontMatter';
|
||||
import {getContentPathList} from './blogUtils';
|
||||
import Yaml from 'js-yaml';
|
||||
|
||||
export type AuthorsMap = Record<string, Author>;
|
||||
|
||||
|
@ -34,63 +29,22 @@ const AuthorsMapSchema = Joi.object<AuthorsMap>().pattern(
|
|||
.required(),
|
||||
);
|
||||
|
||||
export function validateAuthorsMapFile(content: unknown): AuthorsMap {
|
||||
export function validateAuthorsMap(content: unknown): AuthorsMap {
|
||||
return Joi.attempt(content, AuthorsMapSchema);
|
||||
}
|
||||
|
||||
export async function readAuthorsMapFile(
|
||||
filePath: string,
|
||||
): Promise<AuthorsMap | undefined> {
|
||||
if (await fs.pathExists(filePath)) {
|
||||
const contentString = await fs.readFile(filePath, {encoding: 'utf8'});
|
||||
try {
|
||||
const unsafeContent = Yaml.load(contentString);
|
||||
return validateAuthorsMapFile(unsafeContent);
|
||||
} catch (e) {
|
||||
// TODO replace later by error cause: see https://v8.dev/features/error-cause
|
||||
logger.error('The author list file looks invalid!');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type AuthorsMapParams = {
|
||||
export async function getAuthorsMap(params: {
|
||||
authorsMapPath: string;
|
||||
contentPaths: BlogContentPaths;
|
||||
};
|
||||
|
||||
export async function getAuthorsMapFilePath({
|
||||
authorsMapPath,
|
||||
contentPaths,
|
||||
}: AuthorsMapParams): Promise<string | undefined> {
|
||||
// Useful to load an eventually localize authors map
|
||||
const contentPath = await findFolderContainingFile(
|
||||
getContentPathList(contentPaths),
|
||||
authorsMapPath,
|
||||
}): Promise<AuthorsMap | undefined> {
|
||||
return getDataFileData(
|
||||
{
|
||||
filePath: params.authorsMapPath,
|
||||
contentPaths: params.contentPaths,
|
||||
fileType: 'authors map',
|
||||
},
|
||||
validateAuthorsMap,
|
||||
);
|
||||
|
||||
if (contentPath) {
|
||||
return path.join(contentPath, authorsMapPath);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function getAuthorsMap(
|
||||
params: AuthorsMapParams,
|
||||
): Promise<AuthorsMap | undefined> {
|
||||
const filePath = await getAuthorsMapFilePath(params);
|
||||
if (!filePath) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return await readAuthorsMapFile(filePath);
|
||||
} catch (e) {
|
||||
// TODO replace later by error cause, see https://v8.dev/features/error-cause
|
||||
logger.error`Couldn't read blog authors map at path=${filePath}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
type AuthorsParam = {
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
Globby,
|
||||
normalizeFrontMatterTags,
|
||||
groupTaggedItems,
|
||||
getContentPathList,
|
||||
} from '@docusaurus/utils';
|
||||
import {LoadContext} from '@docusaurus/types';
|
||||
import {validateBlogPostFrontMatter} from './blogFrontMatter';
|
||||
|
@ -318,8 +319,3 @@ export function linkify({
|
|||
|
||||
return newContent;
|
||||
}
|
||||
|
||||
// Order matters: we look in priority in localized folder
|
||||
export function getContentPathList(contentPaths: BlogContentPaths): string[] {
|
||||
return [contentPaths.contentPathLocalized, contentPaths.contentPath];
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ import {
|
|||
posixPath,
|
||||
addTrailingPathSeparator,
|
||||
createAbsoluteFilePathMatcher,
|
||||
getContentPathList,
|
||||
getDataFilePath,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
} from '@docusaurus/utils';
|
||||
import {translateContent, getTranslationFiles} from './translations';
|
||||
|
@ -45,13 +47,11 @@ import {
|
|||
import {Configuration} from 'webpack';
|
||||
import {
|
||||
generateBlogPosts,
|
||||
getContentPathList,
|
||||
getSourceToPermalink,
|
||||
getBlogTags,
|
||||
} from './blogUtils';
|
||||
import {BlogPostFrontMatter} from './blogFrontMatter';
|
||||
import {createBlogFeedFiles} from './feed';
|
||||
import {getAuthorsMapFilePath} from './authors';
|
||||
|
||||
export default async function pluginContentBlog(
|
||||
context: LoadContext,
|
||||
|
@ -90,8 +90,8 @@ export default async function pluginContentBlog(
|
|||
const aliasedSource = (source: string) =>
|
||||
`~blog/${posixPath(path.relative(pluginDataDirRoot, source))}`;
|
||||
|
||||
const authorsMapFilePath = await getAuthorsMapFilePath({
|
||||
authorsMapPath: options.authorsMapPath,
|
||||
const authorsMapFilePath = await getDataFilePath({
|
||||
filePath: options.authorsMapPath,
|
||||
contentPaths,
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue