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:
Joshua Chen 2021-12-31 11:55:42 +08:00 committed by GitHub
parent 7adc1c0cdb
commit 24d65d9bdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 533 additions and 747 deletions

View file

@ -1,8 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getFolderContainingFile throw if no folder contain such file 1`] = `
"File \\"index.test.ts\\" does not exist in any of these folders:
- /abcdef
- /gehij
- /klmn]"
`;

View file

@ -0,0 +1,202 @@
/**
* 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 path from 'path';
import {
findFolderContainingFile,
getFolderContainingFile,
getDataFilePath,
getDataFileData,
} from '../dataFileUtils';
describe('getDataFilePath', () => {
const fixturesDir = path.join(__dirname, '__fixtures__/dataFiles');
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('getDataFilePath returns localized Yml path in priority', async () => {
expect(
await getDataFilePath({
filePath: 'authors.yml',
contentPaths: {
contentPathLocalized: contentPathYml1,
contentPath: contentPathYml2,
},
}),
).toEqual(path.join(contentPathYml1, 'authors.yml'));
expect(
await getDataFilePath({
filePath: 'authors.yml',
contentPaths: {
contentPathLocalized: contentPathYml2,
contentPath: contentPathYml1,
},
}),
).toEqual(path.join(contentPathYml2, 'authors.yml'));
});
test('getDataFilePath returns localized Json path in priority', async () => {
expect(
await getDataFilePath({
filePath: 'authors.json',
contentPaths: {
contentPathLocalized: contentPathJson1,
contentPath: contentPathJson2,
},
}),
).toEqual(path.join(contentPathJson1, 'authors.json'));
expect(
await getDataFilePath({
filePath: 'authors.json',
contentPaths: {
contentPathLocalized: contentPathJson2,
contentPath: contentPathJson1,
},
}),
).toEqual(path.join(contentPathJson2, 'authors.json'));
});
test('getDataFilePath returns unlocalized Yml path as fallback', async () => {
expect(
await getDataFilePath({
filePath: 'authors.yml',
contentPaths: {
contentPathLocalized: contentPathEmpty,
contentPath: contentPathYml2,
},
}),
).toEqual(path.join(contentPathYml2, 'authors.yml'));
});
test('getDataFilePath returns unlocalized Json path as fallback', async () => {
expect(
await getDataFilePath({
filePath: 'authors.json',
contentPaths: {
contentPathLocalized: contentPathEmpty,
contentPath: contentPathJson1,
},
}),
).toEqual(path.join(contentPathJson1, 'authors.json'));
});
test('getDataFilePath can return undefined (file not found)', async () => {
expect(
await getDataFilePath({
filePath: 'authors.json',
contentPaths: {
contentPathLocalized: contentPathEmpty,
contentPath: contentPathYml1,
},
}),
).toBeUndefined();
expect(
await getDataFilePath({
filePath: 'authors.yml',
contentPaths: {
contentPathLocalized: contentPathEmpty,
contentPath: contentPathJson1,
},
}),
).toBeUndefined();
});
test('getDataFilePath can return nested path', async () => {
expect(
await getDataFilePath({
filePath: 'sub/folder/authors.yml',
contentPaths: {
contentPathLocalized: contentPathEmpty,
contentPath: contentPathNestedYml,
},
}),
).toEqual(path.join(contentPathNestedYml, 'sub/folder/authors.yml'));
});
});
describe('getDataFileData', () => {
const fixturesDir = path.join(__dirname, '__fixtures__/dataFiles/actualData');
function readDataFile(filePath: string) {
return getDataFileData(
{
filePath,
contentPaths: {contentPath: fixturesDir, contentPathLocalized: ''},
fileType: 'test',
},
(content) => {
// @ts-expect-error: good enough
if (content.a !== 1) {
throw new Error('Nope');
}
return content;
},
);
}
test('read valid yml author file', async () => {
await expect(readDataFile('valid.yml')).resolves.toEqual({a: 1});
});
test('read valid json author file', async () => {
await expect(readDataFile('valid.json')).resolves.toEqual({a: 1});
});
test('fail to read invalid yml', async () => {
await expect(
readDataFile('bad.yml'),
).rejects.toThrowErrorMatchingInlineSnapshot(`"Nope"`);
});
test('fail to read invalid json', async () => {
await expect(
readDataFile('bad.json'),
).rejects.toThrowErrorMatchingInlineSnapshot(`"Nope"`);
});
});
describe('findFolderContainingFile', () => {
test('find appropriate folder', async () => {
await expect(
findFolderContainingFile(
['/abcdef', '/gehij', __dirname, '/klmn'],
'index.test.ts',
),
).resolves.toEqual(__dirname);
});
test('return undefined if no folder contain such file', async () => {
await expect(
findFolderContainingFile(['/abcdef', '/gehij', '/klmn'], 'index.test.ts'),
).resolves.toBeUndefined();
});
});
describe('getFolderContainingFile', () => {
test('get appropriate folder', async () => {
await expect(
getFolderContainingFile(
['/abcdef', '/gehij', __dirname, '/klmn'],
'index.test.ts',
),
).resolves.toEqual(__dirname);
});
test('throw if no folder contain such file', async () => {
await expect(
getFolderContainingFile(['/abcdef', '/gehij', '/klmn'], 'index.test.ts'),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"File \\"index.test.ts\\" does not exist in any of these folders:
- /abcdef
- /gehij
- /klmn]"
`);
});
});

View file

@ -1,25 +0,0 @@
/**
* 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 {escapePath} from '../escapePath';
describe('escapePath', () => {
test('escapePath works', () => {
const asserts: Record<string, string> = {
'c:/aaaa\\bbbb': 'c:/aaaa\\\\bbbb',
'c:\\aaaa\\bbbb\\★': 'c:\\\\aaaa\\\\bbbb\\\\★',
'\\\\?\\c:\\aaaa\\bbbb': '\\\\\\\\?\\\\c:\\\\aaaa\\\\bbbb',
'c:\\aaaa\\bbbb': 'c:\\\\aaaa\\\\bbbb',
'foo\\bar': 'foo\\\\bar',
'foo\\bar/lol': 'foo\\\\bar/lol',
'website\\docs/**/*.{md,mdx}': 'website\\\\docs/**/*.{md,mdx}',
};
Object.keys(asserts).forEach((file) => {
expect(escapePath(file)).toBe(asserts[file]);
});
});
});

View file

@ -5,16 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import path from 'path';
import {
fileToPath,
genComponentName,
genChunkName,
idx,
getSubFolder,
posixPath,
objectWithKeySorted,
aliasedSitePath,
isValidPathname,
addTrailingSlash,
removeTrailingSlash,
@ -23,47 +16,14 @@ import {
addLeadingSlash,
getElementsAround,
mergeTranslations,
mapAsyncSequencial,
mapAsyncSequential,
findAsyncSequential,
findFolderContainingFile,
getFolderContainingFile,
updateTranslationFileMessages,
parseMarkdownHeadingId,
} from '../index';
import {sum} from 'lodash';
describe('load utils', () => {
test('aliasedSitePath', () => {
const asserts: Record<string, string> = {
'user/website/docs/asd.md': '@site/docs/asd.md',
'user/website/versioned_docs/foo/bar.md':
'@site/versioned_docs/foo/bar.md',
'user/docs/test.md': '@site/../docs/test.md',
};
Object.keys(asserts).forEach((file) => {
expect(posixPath(aliasedSitePath(file, 'user/website'))).toBe(
asserts[file],
);
});
});
test('genComponentName', () => {
const asserts: Record<string, string> = {
'/': 'index',
'/foo-bar': 'FooBar096',
'/foo/bar': 'FooBar1Df',
'/blog/2017/12/14/introducing-docusaurus':
'Blog20171214IntroducingDocusaurus8D2',
'/blog/2017/12/14-introducing-docusaurus':
'Blog20171214IntroducingDocusaurus0Bc',
'/blog/201712/14-introducing-docusaurus':
'Blog20171214IntroducingDocusaurusA93',
};
Object.keys(asserts).forEach((file) => {
expect(genComponentName(file)).toBe(asserts[file]);
});
});
test('fileToPath', () => {
const asserts: Record<string, string> = {
'index.md': '/',
@ -80,41 +40,6 @@ describe('load utils', () => {
});
});
test('objectWithKeySorted', () => {
const obj = {
'/docs/adding-blog': '4',
'/docs/versioning': '5',
'/': '1',
'/blog/2018': '3',
'/youtube': '7',
'/users/en/': '6',
'/blog': '2',
};
expect(objectWithKeySorted(obj)).toMatchInlineSnapshot(`
Object {
"/": "1",
"/blog": "2",
"/blog/2018": "3",
"/docs/adding-blog": "4",
"/docs/versioning": "5",
"/users/en/": "6",
"/youtube": "7",
}
`);
const obj2 = {
b: 'foo',
c: 'bar',
a: 'baz',
};
expect(objectWithKeySorted(obj2)).toMatchInlineSnapshot(`
Object {
"a": "baz",
"b": "foo",
"c": "bar",
}
`);
});
test('genChunkName', () => {
const firstAssert: Record<string, string> = {
'/docs/adding-blog': 'docs-adding-blog-062',
@ -159,64 +84,6 @@ describe('load utils', () => {
expect(genChunkName('d', undefined, undefined, true)).toBe('8277e091');
});
test('idx', () => {
const a = {};
const b = {hello: 'world'};
const obj = {
translation: {
enabled: true,
enabledLanguages: [
{
enabled: true,
name: 'English',
tag: 'en',
},
{
enabled: true,
name: '日本語',
tag: 'ja',
},
],
},
versioning: {
enabled: false,
versions: [],
},
};
const test = {arr: [1, 2, 3]};
const variable = 'enabledLanguages';
expect(idx(a, ['b', 'c'])).toBeUndefined();
expect(idx(b, ['hello'])).toEqual('world');
expect(idx(b, 'hello')).toEqual('world');
expect(idx(obj, 'typo')).toBeUndefined();
expect(idx(obj, 'versioning')).toEqual({
enabled: false,
versions: [],
});
expect(idx(obj, ['translation', 'enabled'])).toEqual(true);
expect(
idx(obj, ['translation', variable]).map(
(lang: {tag: string}) => lang.tag,
),
).toEqual(['en', 'ja']);
expect(idx(test, ['arr', 0])).toEqual(1);
expect(idx(undefined)).toBeUndefined();
expect(idx(null)).toBeNull();
});
test('getSubFolder', () => {
const testA = path.join('folder', 'en', 'test.md');
const testB = path.join('folder', 'ja', 'test.md');
const testC = path.join('folder', 'ja', 'en', 'test.md');
const testD = path.join('docs', 'ro', 'test.md');
const testE = path.join('docs', 'test.md');
expect(getSubFolder(testA, 'folder')).toBe('en');
expect(getSubFolder(testB, 'folder')).toBe('ja');
expect(getSubFolder(testC, 'folder')).toBe('ja');
expect(getSubFolder(testD, 'docs')).toBe('ro');
expect(getSubFolder(testE, 'docs')).toBeNull();
});
test('isValidPathname', () => {
expect(isValidPathname('/')).toBe(true);
expect(isValidPathname('/hey')).toBe(true);
@ -349,7 +216,7 @@ describe('mergeTranslations', () => {
});
});
describe('mapAsyncSequencial', () => {
describe('mapAsyncSequential', () => {
function sleep(timeout: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
@ -369,7 +236,7 @@ describe('mapAsyncSequencial', () => {
const timeBefore = Date.now();
await expect(
mapAsyncSequencial(items, async (item) => {
mapAsyncSequential(items, async (item) => {
const itemTimeout = itemToTimeout[item];
itemMapStartsAt[item] = Date.now();
await sleep(itemTimeout);
@ -419,40 +286,6 @@ describe('findAsyncSequencial', () => {
});
});
describe('findFolderContainingFile', () => {
test('find appropriate folder', async () => {
await expect(
findFolderContainingFile(
['/abcdef', '/gehij', __dirname, '/klmn'],
'index.test.ts',
),
).resolves.toEqual(__dirname);
});
test('return undefined if no folder contain such file', async () => {
await expect(
findFolderContainingFile(['/abcdef', '/gehij', '/klmn'], 'index.test.ts'),
).resolves.toBeUndefined();
});
});
describe('getFolderContainingFile', () => {
test('get appropriate folder', async () => {
await expect(
getFolderContainingFile(
['/abcdef', '/gehij', __dirname, '/klmn'],
'index.test.ts',
),
).resolves.toEqual(__dirname);
});
test('throw if no folder contain such file', async () => {
await expect(
getFolderContainingFile(['/abcdef', '/gehij', '/klmn'], 'index.test.ts'),
).rejects.toThrowErrorMatchingSnapshot();
});
});
describe('updateTranslationFileMessages', () => {
test('should update messages', () => {
expect(

View file

@ -5,10 +5,16 @@
* LICENSE file in the root directory of this source tree.
*/
import {isNameTooLong, shortName} from '../pathUtils';
import {
isNameTooLong,
shortName,
escapePath,
posixPath,
aliasedSitePath,
} from '../pathUtils';
describe('pathUtils', () => {
test('isNameTooLong', () => {
describe('isNameTooLong', () => {
test('behaves correctly', () => {
const asserts: Record<string, boolean> = {
'': false,
'foo-bar-096': false,
@ -26,40 +32,90 @@ describe('pathUtils', () => {
expect(isNameTooLong(path)).toBe(asserts[path]);
});
});
});
describe('shortName', () => {
test('works', () => {
const asserts: Record<string, string> = {
'': '',
'foo-bar': 'foo-bar',
'endi-lie': 'endi-lie',
'yangshun-tay': 'yangshun-tay',
'foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar':
'foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-',
'foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-test-1-test-2':
'foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-test-1-test-',
};
Object.keys(asserts).forEach((file) => {
expect(shortName(file)).toBe(asserts[file]);
});
describe('shortName', () => {
test('works', () => {
const asserts: Record<string, string> = {
'': '',
'foo-bar': 'foo-bar',
'endi-lie': 'endi-lie',
'yangshun-tay': 'yangshun-tay',
'foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar':
'foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-',
'foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-test-1-test-2':
'foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-foo-bar-test-1-test-',
};
Object.keys(asserts).forEach((file) => {
expect(shortName(file)).toBe(asserts[file]);
});
});
// Based on https://github.com/gatsbyjs/gatsby/pull/21518/files
// Based on https://github.com/gatsbyjs/gatsby/pull/21518/files
const SHORT_PATH = `/short/path/without/trailing/slash`;
const VERY_LONG_PATH = `/${`x`.repeat(256)}/`;
const VERY_LONG_PATH_NON_LATIN = `/${``.repeat(255)}/`;
const SHORT_PATH = `/short/path/without/trailing/slash`;
const VERY_LONG_PATH = `/${`x`.repeat(256)}/`;
const VERY_LONG_PATH_NON_LATIN = `/${``.repeat(255)}/`;
it(`Truncates long paths correctly`, () => {
const truncatedPathLatin = shortName(VERY_LONG_PATH);
const truncatedPathNonLatin = shortName(VERY_LONG_PATH_NON_LATIN);
expect(truncatedPathLatin.length).toBeLessThanOrEqual(255);
expect(truncatedPathNonLatin.length).toBeLessThanOrEqual(255);
});
test('Truncates long paths correctly', () => {
const truncatedPathLatin = shortName(VERY_LONG_PATH);
const truncatedPathNonLatin = shortName(VERY_LONG_PATH_NON_LATIN);
expect(truncatedPathLatin.length).toBeLessThanOrEqual(255);
expect(truncatedPathNonLatin.length).toBeLessThanOrEqual(255);
});
it(`Does not truncate short paths`, () => {
const truncatedPath = shortName(SHORT_PATH);
expect(truncatedPath).toEqual(SHORT_PATH);
test('Does not truncate short paths', () => {
const truncatedPath = shortName(SHORT_PATH);
expect(truncatedPath).toEqual(SHORT_PATH);
});
});
describe('escapePath', () => {
test('escapePath works', () => {
const asserts: Record<string, string> = {
'c:/aaaa\\bbbb': 'c:/aaaa\\\\bbbb',
'c:\\aaaa\\bbbb\\★': 'c:\\\\aaaa\\\\bbbb\\\\★',
'\\\\?\\c:\\aaaa\\bbbb': '\\\\\\\\?\\\\c:\\\\aaaa\\\\bbbb',
'c:\\aaaa\\bbbb': 'c:\\\\aaaa\\\\bbbb',
'foo\\bar': 'foo\\\\bar',
'foo\\bar/lol': 'foo\\\\bar/lol',
'website\\docs/**/*.{md,mdx}': 'website\\\\docs/**/*.{md,mdx}',
};
Object.keys(asserts).forEach((file) => {
expect(escapePath(file)).toBe(asserts[file]);
});
});
});
describe('posixPath', () => {
test('posixPath works', () => {
const asserts: Record<string, string> = {
'c:/aaaa\\bbbb': 'c:/aaaa/bbbb',
'c:\\aaaa\\bbbb\\★': 'c:\\aaaa\\bbbb\\★',
'\\\\?\\c:\\aaaa\\bbbb': '\\\\?\\c:\\aaaa\\bbbb',
'c:\\aaaa\\bbbb': 'c:/aaaa/bbbb',
'foo\\bar': 'foo/bar',
'foo\\bar/lol': 'foo/bar/lol',
'website\\docs/**/*.{md,mdx}': 'website/docs/**/*.{md,mdx}',
};
Object.keys(asserts).forEach((file) => {
expect(posixPath(file)).toBe(asserts[file]);
});
});
});
describe('aliasedSitePath', () => {
test('behaves correctly', () => {
const asserts: Record<string, string> = {
'user/website/docs/asd.md': '@site/docs/asd.md',
'user/website/versioned_docs/foo/bar.md':
'@site/versioned_docs/foo/bar.md',
'user/docs/test.md': '@site/../docs/test.md',
};
Object.keys(asserts).forEach((file) => {
expect(posixPath(aliasedSitePath(file, 'user/website'))).toBe(
asserts[file],
);
});
});
});

View file

@ -1,25 +0,0 @@
/**
* 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 {posixPath} from '../posixPath';
describe('posixPath', () => {
test('posixPath works', () => {
const asserts: Record<string, string> = {
'c:/aaaa\\bbbb': 'c:/aaaa/bbbb',
'c:\\aaaa\\bbbb\\★': 'c:\\aaaa\\bbbb\\★',
'\\\\?\\c:\\aaaa\\bbbb': '\\\\?\\c:\\aaaa\\bbbb',
'c:\\aaaa\\bbbb': 'c:/aaaa/bbbb',
'foo\\bar': 'foo/bar',
'foo\\bar/lol': 'foo/bar/lol',
'website\\docs/**/*.{md,mdx}': 'website/docs/**/*.{md,mdx}',
};
Object.keys(asserts).forEach((file) => {
expect(posixPath(file)).toBe(asserts[file]);
});
});
});

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {normalizeUrl} from '../normalizeUrl';
import {normalizeUrl} from '../urlUtils';
describe('normalizeUrl', () => {
test('should normalize urls correctly', () => {

View file

@ -0,0 +1,93 @@
/**
* 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 fs from 'fs-extra';
import Yaml from 'js-yaml';
import path from 'path';
import {findAsyncSequential} from './index';
import type {ContentPaths} from './markdownLinks';
import logger from '@docusaurus/logger';
type DataFileParams = {
filePath: string;
contentPaths: ContentPaths;
};
export async function getDataFilePath({
filePath,
contentPaths,
}: DataFileParams): Promise<string | undefined> {
// Loads a localized data file in priority
const contentPath = await findFolderContainingFile(
getContentPathList(contentPaths),
filePath,
);
if (contentPath) {
return path.join(contentPath, filePath);
}
return undefined;
}
/**
* Looks up for a data file in the content paths, returns the normalized object.
* Throws when validation fails; returns undefined when file not found
*/
export async function getDataFileData<T>(
params: DataFileParams & {fileType: string},
validate: (content: unknown) => T,
): Promise<T | undefined> {
const filePath = await getDataFilePath(params);
if (!filePath) {
return undefined;
}
if (await fs.pathExists(filePath)) {
try {
const contentString = await fs.readFile(filePath, {encoding: 'utf8'});
const unsafeContent = Yaml.load(contentString);
return validate(unsafeContent);
} catch (e) {
// TODO replace later by error cause, see https://v8.dev/features/error-cause
logger.error`The ${params.fileType} file at path=${filePath} looks invalid.`;
throw e;
}
}
return undefined;
}
// Order matters: we look in priority in localized folder
export function getContentPathList(contentPaths: ContentPaths): string[] {
return [contentPaths.contentPathLocalized, contentPaths.contentPath];
}
// return the first folder path in which the file exists in
export async function findFolderContainingFile(
folderPaths: string[],
relativeFilePath: string,
): Promise<string | undefined> {
return findAsyncSequential(folderPaths, (folderPath) =>
fs.pathExists(path.join(folderPath, relativeFilePath)),
);
}
export async function getFolderContainingFile(
folderPaths: string[],
relativeFilePath: string,
): Promise<string> {
const maybeFolderPath = await findFolderContainingFile(
folderPaths,
relativeFilePath,
);
// should never happen, as the source was read from the FS anyway...
if (!maybeFolderPath) {
throw new Error(
`File "${relativeFilePath}" does not exist in any of these folders:\n- ${folderPaths.join(
'\n- ',
)}]`,
);
}
return maybeFolderPath;
}

View file

@ -1,23 +0,0 @@
/**
* 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.
*/
/**
* When you have a path like C:\X\Y
* It is not safe to use directly when generating code
* For example, this would fail due to unescaped \: `<img src={require('${filePath}')} />`
* But this would work: `<img src={require('${escapePath(filePath)}')} />`
*
* posixPath can't be used in all cases, because forward slashes are only valid
* Windows paths when they don't contain non-ascii characters, and posixPath
* doesn't escape those that fail to be converted.
*/
export function escapePath(str: string): string {
const escaped = JSON.stringify(str);
// Remove the " around the json string;
return escaped.substring(1, escaped.length - 1);
}

View file

@ -8,8 +8,7 @@
import logger from '@docusaurus/logger';
import path from 'path';
import {createHash} from 'crypto';
import {camelCase, mapValues} from 'lodash';
import escapeStringRegexp from 'escape-string-regexp';
import {mapValues} from 'lodash';
import fs from 'fs-extra';
import {URL} from 'url';
import {
@ -20,30 +19,21 @@ import {
import resolvePathnameUnsafe from 'resolve-pathname';
import {posixPath as posixPathImport} from './posixPath';
import {simpleHash, docuHash} from './hashUtils';
import {normalizeUrl} from './normalizeUrl';
import {DEFAULT_PLUGIN_ID} from './constants';
export * from './constants';
export * from './mdxUtils';
export * from './normalizeUrl';
export * from './urlUtils';
export * from './tags';
export const posixPath = posixPathImport;
export * from './markdownParser';
export * from './markdownLinks';
export * from './escapePath';
export * from './slugger';
export {md5Hash, simpleHash, docuHash} from './hashUtils';
export {
Globby,
GlobExcludeDefault,
createMatcher,
createAbsoluteFilePathMatcher,
} from './globUtils';
export * from './pathUtils';
export * from './hashUtils';
export * from './globUtils';
export * from './webpackUtils';
export * from './dataFileUtils';
const fileHash = new Map();
export async function generate(
@ -80,18 +70,6 @@ export async function generate(
}
}
export function objectWithKeySorted<T>(
obj: Record<string, T>,
): Record<string, T> {
// https://github.com/lodash/lodash/issues/1459#issuecomment-460941233
return Object.keys(obj)
.sort()
.reduce((acc: Record<string, T>, key: string) => {
acc[key] = obj[key];
return acc;
}, {});
}
const indexRE = /(^|.*\/)index\.(md|mdx|js|jsx|ts|tsx)$/i;
const extRE = /\.(md|mdx|js|jsx|ts|tsx)$/;
@ -113,37 +91,6 @@ export function encodePath(userpath: string): string {
.join('/');
}
/**
* Convert first string character to the upper case.
* E.g: docusaurus -> Docusaurus
*/
export function upperFirst(str: string): string {
return str ? str.charAt(0).toUpperCase() + str.slice(1) : '';
}
/**
* Generate unique React Component Name.
* E.g: /foo-bar -> FooBar096
*/
export function genComponentName(pagePath: string): string {
if (pagePath === '/') {
return 'index';
}
const pageHash = docuHash(pagePath);
return upperFirst(camelCase(pageHash));
}
// When you want to display a path in a message/warning/error,
// it's more convenient to:
// - make it relative to cwd()
// - convert to posix (ie not using windows \ path separator)
// This way, Jest tests can run more reliably on any computer/CI
// on both Unix/Windows
// For Windows users this is not perfect (as they see / instead of \) but it's probably good enough
export function toMessageRelativeFilePath(filePath: string): string {
return posixPath(path.relative(process.cwd(), filePath));
}
const chunkNameCache = new Map();
/**
* Generate unique chunk name given a module path.
@ -172,52 +119,6 @@ export function genChunkName(
return chunkName;
}
// Too dynamic
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
export function idx(target: any, keyPaths?: string | (string | number)[]): any {
return (
target &&
keyPaths &&
(Array.isArray(keyPaths)
? keyPaths.reduce((obj, key) => obj && obj[key], target)
: target[keyPaths])
);
}
/**
* Given a filepath and dirpath, get the first directory.
*/
export function getSubFolder(file: string, refDir: string): string | null {
const separator = escapeStringRegexp(path.sep);
const baseDir = escapeStringRegexp(path.basename(refDir));
const regexSubFolder = new RegExp(
`${baseDir}${separator}(.*?)${separator}.*`,
);
const match = regexSubFolder.exec(file);
return match && match[1];
}
/**
* Alias filepath relative to site directory, very useful so that we
* don't expose user's site structure.
* Example: some/path/to/website/docs/foo.md -> @site/docs/foo.md
*/
export function aliasedSitePath(filePath: string, siteDir: string): string {
const relativePath = posixPath(path.relative(siteDir, filePath));
// Cannot use path.join() as it resolves '../' and removes
// the '@site'. Let webpack loader resolve it.
return `@site/${relativePath}`;
}
export function getEditUrl(
fileRelativePath: string,
editUrl?: string,
): string | undefined {
return editUrl
? normalizeUrl([editUrl, posixPath(fileRelativePath)])
: undefined;
}
export function isValidPathname(str: string): boolean {
if (!str.startsWith('/')) {
return false;
@ -306,7 +207,7 @@ export function getPluginI18nPath({
);
}
export async function mapAsyncSequencial<T, R>(
export async function mapAsyncSequential<T, R>(
array: T[],
action: (t: T) => Promise<R>,
): Promise<R[]> {
@ -332,35 +233,6 @@ export async function findAsyncSequential<T>(
return undefined;
}
// return the first folder path in which the file exists in
export async function findFolderContainingFile(
folderPaths: string[],
relativeFilePath: string,
): Promise<string | undefined> {
return findAsyncSequential(folderPaths, (folderPath) =>
fs.pathExists(path.join(folderPath, relativeFilePath)),
);
}
export async function getFolderContainingFile(
folderPaths: string[],
relativeFilePath: string,
): Promise<string> {
const maybeFolderPath = await findFolderContainingFile(
folderPaths,
relativeFilePath,
);
// should never happen, as the source was read from the FS anyway...
if (!maybeFolderPath) {
throw new Error(
`File "${relativeFilePath}" does not exist in any of these folders:\n- ${folderPaths.join(
'\n- ',
)}]`,
);
}
return maybeFolderPath;
}
export function reportMessage(
message: string,
reportingSeverity: ReportingSeverity,
@ -420,21 +292,3 @@ export function updateTranslationFileMessages(
})),
};
}
// Input: ## Some heading {#some-heading}
// Output: {text: "## Some heading", id: "some-heading"}
export function parseMarkdownHeadingId(heading: string): {
text: string;
id?: string;
} {
const customHeadingIdRegex = /^(.*?)\s*\{#([\w-]+)\}$/;
const matches = customHeadingIdRegex.exec(heading);
if (matches) {
return {
text: matches[1],
id: matches[2],
};
} else {
return {text: heading, id: undefined};
}
}

View file

@ -6,7 +6,7 @@
*/
import path from 'path';
import {aliasedSitePath} from './index';
import {aliasedSitePath} from './pathUtils';
export type ContentPaths = {
contentPath: string;

View file

@ -9,6 +9,24 @@ import logger from '@docusaurus/logger';
import fs from 'fs-extra';
import matter from 'gray-matter';
// Input: ## Some heading {#some-heading}
// Output: {text: "## Some heading", id: "some-heading"}
export function parseMarkdownHeadingId(heading: string): {
text: string;
id?: string;
} {
const customHeadingIdRegex = /^(.*?)\s*\{#([\w-]+)\}$/;
const matches = customHeadingIdRegex.exec(heading);
if (matches) {
return {
text: matches[1],
id: matches[2],
};
} else {
return {text: heading, id: undefined};
}
}
// Hacky way of stripping out import statements from the excerpt
// TODO: Find a better way to do so, possibly by compiling the Markdown content,
// stripping out HTML tags and obtaining the first line.

View file

@ -7,6 +7,8 @@
// Based on https://github.com/gatsbyjs/gatsby/pull/21518/files
import path from 'path';
// MacOS (APFS) and Windows (NTFS) filename length limit = 255 chars, Others = 255 bytes
const MAX_PATH_SEGMENT_CHARS = 255;
const MAX_PATH_SEGMENT_BYTES = 255;
@ -39,3 +41,66 @@ export const shortName = (str: string): string => {
)
.toString();
};
/**
* Convert Windows backslash paths to posix style paths.
* E.g: endi\lie -> endi/lie
*
* Returns original path if the posix counterpart is not valid Windows path.
* This makes the legacy code that uses posixPath safe; but also makes it less
* useful when you actually want a path with forward slashes (e.g. for URL)
*
* Adopted from https://github.com/sindresorhus/slash/blob/main/index.js
*/
export function posixPath(str: string): string {
const isExtendedLengthPath = /^\\\\\?\\/.test(str);
// Forward slashes are only valid Windows paths when they don't contain non-ascii characters.
// eslint-disable-next-line no-control-regex
const hasNonAscii = /[^\u0000-\u0080]+/.test(str);
if (isExtendedLengthPath || hasNonAscii) {
return str;
}
return str.replace(/\\/g, '/');
}
// When you want to display a path in a message/warning/error,
// it's more convenient to:
// - make it relative to cwd()
// - convert to posix (ie not using windows \ path separator)
// This way, Jest tests can run more reliably on any computer/CI
// on both Unix/Windows
// For Windows users this is not perfect (as they see / instead of \) but it's probably good enough
export function toMessageRelativeFilePath(filePath: string): string {
return posixPath(path.relative(process.cwd(), filePath));
}
/**
* Alias filepath relative to site directory, very useful so that we
* don't expose user's site structure.
* Example: some/path/to/website/docs/foo.md -> @site/docs/foo.md
*/
export function aliasedSitePath(filePath: string, siteDir: string): string {
const relativePath = posixPath(path.relative(siteDir, filePath));
// Cannot use path.join() as it resolves '../' and removes
// the '@site'. Let webpack loader resolve it.
return `@site/${relativePath}`;
}
/**
* When you have a path like C:\X\Y
* It is not safe to use directly when generating code
* For example, this would fail due to unescaped \: `<img src={require('${filePath}')} />`
* But this would work: `<img src={require('${escapePath(filePath)}')} />`
*
* posixPath can't be used in all cases, because forward slashes are only valid
* Windows paths when they don't contain non-ascii characters, and posixPath
* doesn't escape those that fail to be converted.
*/
export function escapePath(str: string): string {
const escaped = JSON.stringify(str);
// Remove the " around the json string;
return escaped.substring(1, escaped.length - 1);
}

View file

@ -1,29 +0,0 @@
/**
* 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.
*/
/**
* Convert Windows backslash paths to posix style paths.
* E.g: endi\lie -> endi/lie
*
* Returns original path if the posix counterpart is not valid Windows path.
* This makes the legacy code that uses posixPath safe; but also makes it less
* useful when you actually want a path with forward slashes (e.g. for URL)
*
* Adopted from https://github.com/sindresorhus/slash/blob/main/index.js
*/
export function posixPath(str: string): string {
const isExtendedLengthPath = /^\\\\\?\\/.test(str);
// Forward slashes are only valid Windows paths when they don't contain non-ascii characters.
// eslint-disable-next-line no-control-regex
const hasNonAscii = /[^\u0000-\u0080]+/.test(str);
if (isExtendedLengthPath || hasNonAscii) {
return str;
}
return str.replace(/\\/g, '/');
}

View file

@ -6,7 +6,7 @@
*/
import {kebabCase, uniq, uniqBy} from 'lodash';
import {normalizeUrl} from './normalizeUrl';
import {normalizeUrl} from './urlUtils';
export type Tag = {
label: string;

View file

@ -78,3 +78,13 @@ export function normalizeUrl(rawUrls: string[]): string {
return str;
}
export function getEditUrl(
fileRelativePath: string,
editUrl?: string,
): string | undefined {
return editUrl
? // Don't use posixPath for this: we need to force a forward slash path
normalizeUrl([editUrl, fileRelativePath.replace(/\\/g, '/')])
: undefined;
}

View file

@ -7,7 +7,7 @@
import type {RuleSetRule} from 'webpack';
import path from 'path';
import {escapePath} from './escapePath';
import {escapePath} from './pathUtils';
import {
WEBPACK_URL_LOADER_LIMIT,
OUTPUT_STATIC_ASSETS_DIR_NAME,