mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
test: improve test coverage; multiple internal refactors (#6912)
This commit is contained in:
parent
12a7305238
commit
ad88f5cc87
78 changed files with 1613 additions and 1149 deletions
|
@ -37,10 +37,7 @@ async function findPackageManagerFromLockFile(): Promise<
|
||||||
SupportedPackageManager | undefined
|
SupportedPackageManager | undefined
|
||||||
> {
|
> {
|
||||||
for (const packageManager of PackageManagersList) {
|
for (const packageManager of PackageManagersList) {
|
||||||
const lockFilePath = path.resolve(
|
const lockFilePath = path.resolve(SupportedPackageManagers[packageManager]);
|
||||||
process.cwd(),
|
|
||||||
SupportedPackageManagers[packageManager],
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(lockFilePath)) {
|
if (await fs.pathExists(lockFilePath)) {
|
||||||
return packageManager;
|
return packageManager;
|
||||||
}
|
}
|
||||||
|
@ -152,7 +149,7 @@ async function copyTemplate(
|
||||||
template: string,
|
template: string,
|
||||||
dest: string,
|
dest: string,
|
||||||
) {
|
) {
|
||||||
await fs.copy(path.resolve(templatesDir, 'shared'), dest);
|
await fs.copy(path.join(templatesDir, 'shared'), dest);
|
||||||
|
|
||||||
// TypeScript variants will copy duplicate resources like CSS & config from
|
// TypeScript variants will copy duplicate resources like CSS & config from
|
||||||
// base template
|
// base template
|
||||||
|
@ -211,7 +208,7 @@ export default async function init(
|
||||||
const templates = await readTemplates(templatesDir);
|
const templates = await readTemplates(templatesDir);
|
||||||
const hasTS = (templateName: string) =>
|
const hasTS = (templateName: string) =>
|
||||||
fs.pathExists(
|
fs.pathExists(
|
||||||
path.resolve(templatesDir, `${templateName}${TypeScriptTemplateSuffix}`),
|
path.join(templatesDir, `${templateName}${TypeScriptTemplateSuffix}`),
|
||||||
);
|
);
|
||||||
let name = siteName;
|
let name = siteName;
|
||||||
|
|
||||||
|
@ -297,7 +294,7 @@ export default async function init(
|
||||||
name: 'templateDir',
|
name: 'templateDir',
|
||||||
validate: async (dir?: string) => {
|
validate: async (dir?: string) => {
|
||||||
if (dir) {
|
if (dir) {
|
||||||
const fullDir = path.resolve(process.cwd(), dir);
|
const fullDir = path.resolve(dir);
|
||||||
if (await fs.pathExists(fullDir)) {
|
if (await fs.pathExists(fullDir)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -351,8 +348,8 @@ export default async function init(
|
||||||
logger.error`Copying Docusaurus template name=${template} failed!`;
|
logger.error`Copying Docusaurus template name=${template} failed!`;
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
} else if (await fs.pathExists(path.resolve(process.cwd(), template))) {
|
} else if (await fs.pathExists(path.resolve(template))) {
|
||||||
const templateDir = path.resolve(process.cwd(), template);
|
const templateDir = path.resolve(template);
|
||||||
try {
|
try {
|
||||||
await fs.copy(templateDir, dest);
|
await fs.copy(templateDir, dest);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
1
packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/outside/doc1.md
generated
Normal file
1
packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/outside/doc1.md
generated
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[link](../docs/doc1.md)
|
|
@ -193,4 +193,14 @@ describe('linkify', () => {
|
||||||
expect(transformedContent).not.toContain('](../doc2.md)');
|
expect(transformedContent).not.toContain('](../doc2.md)');
|
||||||
expect(content).not.toEqual(transformedContent);
|
expect(content).not.toEqual(transformedContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// See comment in linkify.ts
|
||||||
|
it('throws for file outside version', async () => {
|
||||||
|
const doc1 = path.join(__dirname, '__fixtures__/outside/doc1.md');
|
||||||
|
await expect(() =>
|
||||||
|
transform(doc1),
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Unexpected error: Markdown file at \\"<PROJECT_ROOT>/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/outside/doc1.md\\" does not belong to any docs version!"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,6 +15,10 @@ function getVersion(filePath: string, options: DocsMarkdownOption) {
|
||||||
filePath.startsWith(docsDirPath),
|
filePath.startsWith(docsDirPath),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
// At this point, this should never happen, because the MDX loaders' paths are
|
||||||
|
// literally using the version content paths; but if we allow sourcing content
|
||||||
|
// from outside the docs directory (through the `include` option, for example;
|
||||||
|
// is there a compelling use-case?), this would actually be testable
|
||||||
if (!versionFound) {
|
if (!versionFound) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unexpected error: Markdown file at "${filePath}" does not belong to any docs version!`,
|
`Unexpected error: Markdown file at "${filePath}" does not belong to any docs version!`,
|
||||||
|
|
|
@ -58,22 +58,17 @@ function postProcessSidebarItem(
|
||||||
`Sidebar category ${item.label} has neither any subitem nor a link. This makes this item not able to link to anything.`,
|
`Sidebar category ${item.label} has neither any subitem nor a link. This makes this item not able to link to anything.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
switch (category.link.type) {
|
return category.link.type === 'doc'
|
||||||
case 'doc':
|
? {
|
||||||
return {
|
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
label: category.label,
|
label: category.label,
|
||||||
id: category.link.id,
|
id: category.link.id,
|
||||||
};
|
}
|
||||||
case 'generated-index':
|
: {
|
||||||
return {
|
|
||||||
type: 'link',
|
type: 'link',
|
||||||
label: category.label,
|
label: category.label,
|
||||||
href: category.link.permalink,
|
href: category.link.permalink,
|
||||||
};
|
};
|
||||||
default:
|
|
||||||
throw new Error('Unexpected sidebar category link type');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// A non-collapsible category can't be collapsed!
|
// A non-collapsible category can't be collapsed!
|
||||||
if (category.collapsible === false) {
|
if (category.collapsible === false) {
|
||||||
|
|
|
@ -376,18 +376,13 @@ export function toNavigationLink(
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (navigationItem.type === 'doc') {
|
if (navigationItem.type === 'category') {
|
||||||
return toDocNavigationLink(getDocById(navigationItem.id));
|
return navigationItem.link.type === 'doc'
|
||||||
} else if (navigationItem.type === 'category') {
|
? toDocNavigationLink(getDocById(navigationItem.link.id))
|
||||||
if (navigationItem.link.type === 'doc') {
|
: {
|
||||||
return toDocNavigationLink(getDocById(navigationItem.link.id));
|
|
||||||
} else if (navigationItem.link.type === 'generated-index') {
|
|
||||||
return {
|
|
||||||
title: navigationItem.label,
|
title: navigationItem.label,
|
||||||
permalink: navigationItem.link.permalink,
|
permalink: navigationItem.link.permalink,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw new Error('unexpected category link type');
|
return toDocNavigationLink(getDocById(navigationItem.id));
|
||||||
}
|
|
||||||
throw new Error('unexpected navigation item');
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {jest} from '@jest/globals';
|
import {jest} from '@jest/globals';
|
||||||
import {extractThemeCodeMessages} from '../update';
|
import {extractThemeCodeMessages} from '../../src/utils';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
@ -16,20 +16,17 @@ jest.setTimeout(15000);
|
||||||
|
|
||||||
describe('theme translations', () => {
|
describe('theme translations', () => {
|
||||||
it('has base messages files contain EXACTLY all the translations extracted from the theme. Please run "yarn workspace @docusaurus/theme-translations update" to keep base messages files up-to-date', async () => {
|
it('has base messages files contain EXACTLY all the translations extracted from the theme. Please run "yarn workspace @docusaurus/theme-translations update" to keep base messages files up-to-date', async () => {
|
||||||
const baseMessagesDirPath = path.join(__dirname, '../locales/base');
|
const baseMessagesDirPath = path.join(__dirname, '../base');
|
||||||
const baseMessages = Object.fromEntries(
|
const baseMessages = Object.fromEntries(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
(
|
(
|
||||||
await fs.readdir(baseMessagesDirPath)
|
await fs.readdir(baseMessagesDirPath)
|
||||||
).map(async (baseMessagesFile) =>
|
).map(async (baseMessagesFile) =>
|
||||||
Object.entries(
|
Object.entries(
|
||||||
JSON.parse(
|
(await fs.readJSON(
|
||||||
(
|
|
||||||
await fs.readFile(
|
|
||||||
path.join(baseMessagesDirPath, baseMessagesFile),
|
path.join(baseMessagesDirPath, baseMessagesFile),
|
||||||
)
|
'utf-8',
|
||||||
).toString(),
|
)) as Record<string, string>,
|
||||||
) as Record<string, string>,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((translations) =>
|
).then((translations) =>
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "أحدث مشاركات المدونة",
|
"theme.blog.sidebar.navAriaLabel": "أحدث مشاركات المدونة",
|
||||||
"theme.blog.tagTitle": "{nPosts} موسومة ب \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} موسومة ب \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "تعديل هذه الصفحة",
|
"theme.common.editThisPage": "تعديل هذه الصفحة",
|
||||||
"theme.common.headingLinkTitle": "ارتباط مباشر بالعنوان",
|
"theme.common.headingLinkTitle": "ارتباط مباشر بالعنوان",
|
||||||
"theme.common.skipToMainContent": "انتقل إلى المحتوى الرئيسي",
|
"theme.common.skipToMainContent": "انتقل إلى المحتوى الرئيسي",
|
||||||
|
|
|
@ -53,10 +53,10 @@
|
||||||
"theme.blog.tagTitle___DESCRIPTION": "The title of the page for a blog tag",
|
"theme.blog.tagTitle___DESCRIPTION": "The title of the page for a blog tag",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel___DESCRIPTION": "The ARIA label for the navbar color mode toggle",
|
"theme.colorToggle.ariaLabel___DESCRIPTION": "The ARIA label for the navbar color mode toggle",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.light___DESCRIPTION": "The name for the light color mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
"theme.colorToggle.ariaLabel.mode.dark___DESCRIPTION": "The name for the dark color mode",
|
"theme.colorToggle.ariaLabel.mode.dark___DESCRIPTION": "The name for the dark color mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light___DESCRIPTION": "The name for the light color mode",
|
||||||
"theme.common.editThisPage": "Edit this page",
|
"theme.common.editThisPage": "Edit this page",
|
||||||
"theme.common.editThisPage___DESCRIPTION": "The link label to edit the current page",
|
"theme.common.editThisPage___DESCRIPTION": "The link label to edit the current page",
|
||||||
"theme.common.headingLinkTitle": "Direct link to heading",
|
"theme.common.headingLinkTitle": "Direct link to heading",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "সাম্প্রতিক ব্লগ পোস্ট নেভিগেশন",
|
"theme.blog.sidebar.navAriaLabel": "সাম্প্রতিক ব্লগ পোস্ট নেভিগেশন",
|
||||||
"theme.blog.tagTitle": "{nPosts} সঙ্গে ট্যাগ্গেড \"{tagName}\" ",
|
"theme.blog.tagTitle": "{nPosts} সঙ্গে ট্যাগ্গেড \"{tagName}\" ",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "এই পেজটি এডিট করুন",
|
"theme.common.editThisPage": "এই পেজটি এডিট করুন",
|
||||||
"theme.common.headingLinkTitle": "হেডিং এর সঙ্গে সরাসরি লিংকড",
|
"theme.common.headingLinkTitle": "হেডিং এর সঙ্গে সরাসরি লিংকড",
|
||||||
"theme.common.skipToMainContent": "স্কিপ করে মূল কন্টেন্ট এ যান",
|
"theme.common.skipToMainContent": "স্কিপ করে মূল কন্টেন্ট এ যান",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Navigace s aktuálními články na blogu",
|
"theme.blog.sidebar.navAriaLabel": "Navigace s aktuálními články na blogu",
|
||||||
"theme.blog.tagTitle": "{nPosts} s tagem \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} s tagem \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "Upravit tuto stránku",
|
"theme.common.editThisPage": "Upravit tuto stránku",
|
||||||
"theme.common.headingLinkTitle": "Přímý odkaz na nadpis",
|
"theme.common.headingLinkTitle": "Přímý odkaz na nadpis",
|
||||||
"theme.common.skipToMainContent": "Přeskočit na hlavní obsah",
|
"theme.common.skipToMainContent": "Přeskočit na hlavní obsah",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
||||||
"theme.blog.tagTitle": "{nPosts} med følgende tag \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} med følgende tag \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "Rediger denne side",
|
"theme.common.editThisPage": "Rediger denne side",
|
||||||
"theme.common.headingLinkTitle": "Direkte link til overskrift",
|
"theme.common.headingLinkTitle": "Direkte link til overskrift",
|
||||||
"theme.common.skipToMainContent": "Hop til hovedindhold",
|
"theme.common.skipToMainContent": "Hop til hovedindhold",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
||||||
"theme.blog.tagTitle": "{nPosts} getaggt mit \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} getaggt mit \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "Diese Seite bearbeiten",
|
"theme.common.editThisPage": "Diese Seite bearbeiten",
|
||||||
"theme.common.headingLinkTitle": "Direkter Link zur Überschrift",
|
"theme.common.headingLinkTitle": "Direkter Link zur Überschrift",
|
||||||
"theme.common.skipToMainContent": "Zum Hauptinhalt springen",
|
"theme.common.skipToMainContent": "Zum Hauptinhalt springen",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Navegación de publicaciones recientes",
|
"theme.blog.sidebar.navAriaLabel": "Navegación de publicaciones recientes",
|
||||||
"theme.blog.tagTitle": "{nPosts} etiquetados con \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} etiquetados con \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "Editar esta página",
|
"theme.common.editThisPage": "Editar esta página",
|
||||||
"theme.common.headingLinkTitle": "Enlace directo al encabezado",
|
"theme.common.headingLinkTitle": "Enlace directo al encabezado",
|
||||||
"theme.common.skipToMainContent": "Saltar al contenido principal",
|
"theme.common.skipToMainContent": "Saltar al contenido principal",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "کنترل پست های اخیر وبلاگ",
|
"theme.blog.sidebar.navAriaLabel": "کنترل پست های اخیر وبلاگ",
|
||||||
"theme.blog.tagTitle": "{nPosts} با برچسب \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} با برچسب \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "ویرایش مطالب این صفحه",
|
"theme.common.editThisPage": "ویرایش مطالب این صفحه",
|
||||||
"theme.common.headingLinkTitle": "لینک مستقیم به عنوان",
|
"theme.common.headingLinkTitle": "لینک مستقیم به عنوان",
|
||||||
"theme.common.skipToMainContent": "پرش به مطلب اصلی",
|
"theme.common.skipToMainContent": "پرش به مطلب اصلی",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
||||||
"theme.blog.tagTitle": "{nPosts} na may tag na \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} na may tag na \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "I-edit ang page",
|
"theme.common.editThisPage": "I-edit ang page",
|
||||||
"theme.common.headingLinkTitle": "Direktang link patungo sa heading",
|
"theme.common.headingLinkTitle": "Direktang link patungo sa heading",
|
||||||
"theme.common.skipToMainContent": "Lumaktaw patungo sa pangunahing content",
|
"theme.common.skipToMainContent": "Lumaktaw patungo sa pangunahing content",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Navigation article de blog récent",
|
"theme.blog.sidebar.navAriaLabel": "Navigation article de blog récent",
|
||||||
"theme.blog.tagTitle": "{nPosts} tagués avec « {tagName} »",
|
"theme.blog.tagTitle": "{nPosts} tagués avec « {tagName} »",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "Éditer cette page",
|
"theme.common.editThisPage": "Éditer cette page",
|
||||||
"theme.common.headingLinkTitle": "Lien direct vers le titre",
|
"theme.common.headingLinkTitle": "Lien direct vers le titre",
|
||||||
"theme.common.skipToMainContent": "Aller au contenu principal",
|
"theme.common.skipToMainContent": "Aller au contenu principal",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "מעבר לרשומות אחרונות בבלוג",
|
"theme.blog.sidebar.navAriaLabel": "מעבר לרשומות אחרונות בבלוג",
|
||||||
"theme.blog.tagTitle": "{nPosts} עם התגית \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} עם התגית \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "ערוך דף זה",
|
"theme.common.editThisPage": "ערוך דף זה",
|
||||||
"theme.common.headingLinkTitle": "קישור ישיר לכותרת",
|
"theme.common.headingLinkTitle": "קישור ישיר לכותרת",
|
||||||
"theme.common.skipToMainContent": "דלג לתוכן הראשי",
|
"theme.common.skipToMainContent": "דלג לתוכן הראשי",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "नया ब्लॉग पोस्ट नेविगेशन",
|
"theme.blog.sidebar.navAriaLabel": "नया ब्लॉग पोस्ट नेविगेशन",
|
||||||
"theme.blog.tagTitle": "{nPosts} पोस्ट \"{tagName}\" टैग के साथ",
|
"theme.blog.tagTitle": "{nPosts} पोस्ट \"{tagName}\" टैग के साथ",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "इस पेज को बदलें",
|
"theme.common.editThisPage": "इस पेज को बदलें",
|
||||||
"theme.common.headingLinkTitle": "शीर्षक का सीधा लिंक",
|
"theme.common.headingLinkTitle": "शीर्षक का सीधा लिंक",
|
||||||
"theme.common.skipToMainContent": "मुख्य कंटेंट तक स्किप करें",
|
"theme.common.skipToMainContent": "मुख्य कंटेंट तक स्किप करें",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Navigazione dei post recenti del blog",
|
"theme.blog.sidebar.navAriaLabel": "Navigazione dei post recenti del blog",
|
||||||
"theme.blog.tagTitle": "{nPosts} etichettati con \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} etichettati con \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "Modifica questa pagina",
|
"theme.common.editThisPage": "Modifica questa pagina",
|
||||||
"theme.common.headingLinkTitle": "Link diretto all'intestazione",
|
"theme.common.headingLinkTitle": "Link diretto all'intestazione",
|
||||||
"theme.common.skipToMainContent": "Passa al contenuto principale",
|
"theme.common.skipToMainContent": "Passa al contenuto principale",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
||||||
"theme.blog.tagTitle": "「{tagName}」タグの記事が{nPosts}あります",
|
"theme.blog.tagTitle": "「{tagName}」タグの記事が{nPosts}あります",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "このページを編集",
|
"theme.common.editThisPage": "このページを編集",
|
||||||
"theme.common.headingLinkTitle": "見出しへの直接リンク",
|
"theme.common.headingLinkTitle": "見出しへの直接リンク",
|
||||||
"theme.common.skipToMainContent": "メインコンテンツまでスキップ",
|
"theme.common.skipToMainContent": "メインコンテンツまでスキップ",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "최근 블로그 문서 둘러보기",
|
"theme.blog.sidebar.navAriaLabel": "최근 블로그 문서 둘러보기",
|
||||||
"theme.blog.tagTitle": "\"{tagName}\" 태그로 연결된 {nPosts}개의 게시물이 있습니다.",
|
"theme.blog.tagTitle": "\"{tagName}\" 태그로 연결된 {nPosts}개의 게시물이 있습니다.",
|
||||||
"theme.colorToggle.ariaLabel": "어두운 모드와 밝은 모드 전환하기 (현재 {mode})",
|
"theme.colorToggle.ariaLabel": "어두운 모드와 밝은 모드 전환하기 (현재 {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "밝은 모드",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "어두운 모드",
|
"theme.colorToggle.ariaLabel.mode.dark": "어두운 모드",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "밝은 모드",
|
||||||
"theme.common.editThisPage": "페이지 편집",
|
"theme.common.editThisPage": "페이지 편집",
|
||||||
"theme.common.headingLinkTitle": "제목으로 바로 가기",
|
"theme.common.headingLinkTitle": "제목으로 바로 가기",
|
||||||
"theme.common.skipToMainContent": "본문으로 건너뛰기",
|
"theme.common.skipToMainContent": "본문으로 건너뛰기",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
||||||
"theme.blog.tagTitle": "{nPosts} z tagiem \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} z tagiem \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "Edytuj tą stronę",
|
"theme.common.editThisPage": "Edytuj tą stronę",
|
||||||
"theme.common.headingLinkTitle": "Bezpośredni link do nagłówka",
|
"theme.common.headingLinkTitle": "Bezpośredni link do nagłówka",
|
||||||
"theme.common.skipToMainContent": "Przejdź do głównej zawartości",
|
"theme.common.skipToMainContent": "Przejdź do głównej zawartości",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
||||||
"theme.blog.tagTitle": "{nPosts} marcadas com \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} marcadas com \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "Editar essa página",
|
"theme.common.editThisPage": "Editar essa página",
|
||||||
"theme.common.headingLinkTitle": "Link direto para o título",
|
"theme.common.headingLinkTitle": "Link direto para o título",
|
||||||
"theme.common.skipToMainContent": "Pular para o conteúdo principal",
|
"theme.common.skipToMainContent": "Pular para o conteúdo principal",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
"theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation",
|
||||||
"theme.blog.tagTitle": "{nPosts} marcadas com \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} marcadas com \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "Editar esta página",
|
"theme.common.editThisPage": "Editar esta página",
|
||||||
"theme.common.headingLinkTitle": "Link direto para o título",
|
"theme.common.headingLinkTitle": "Link direto para o título",
|
||||||
"theme.common.skipToMainContent": "Saltar para o conteúdo principal",
|
"theme.common.skipToMainContent": "Saltar para o conteúdo principal",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Навигация по последним постам в блоге",
|
"theme.blog.sidebar.navAriaLabel": "Навигация по последним постам в блоге",
|
||||||
"theme.blog.tagTitle": "{nPosts} с тегом \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} с тегом \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Переключение между темным и светлым режимом (сейчас используется {mode})",
|
"theme.colorToggle.ariaLabel": "Переключение между темным и светлым режимом (сейчас используется {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "Светлый режим",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "Тёмный режим",
|
"theme.colorToggle.ariaLabel.mode.dark": "Тёмный режим",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "Светлый режим",
|
||||||
"theme.common.editThisPage": "Отредактировать эту страницу",
|
"theme.common.editThisPage": "Отредактировать эту страницу",
|
||||||
"theme.common.headingLinkTitle": "Прямая ссылка на этот заголовок",
|
"theme.common.headingLinkTitle": "Прямая ссылка на этот заголовок",
|
||||||
"theme.common.skipToMainContent": "Перейти к основному содержимому",
|
"theme.common.skipToMainContent": "Перейти к основному содержимому",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Недавни постови на блогу",
|
"theme.blog.sidebar.navAriaLabel": "Недавни постови на блогу",
|
||||||
"theme.blog.tagTitle": "{nPosts} означени са \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} означени са \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "Уреди ову страницу",
|
"theme.common.editThisPage": "Уреди ову страницу",
|
||||||
"theme.common.headingLinkTitle": "Веза до наслова",
|
"theme.common.headingLinkTitle": "Веза до наслова",
|
||||||
"theme.common.skipToMainContent": "Пређи на главни садржај",
|
"theme.common.skipToMainContent": "Пређи на главни садржај",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Blog son gönderiler navigasyonu",
|
"theme.blog.sidebar.navAriaLabel": "Blog son gönderiler navigasyonu",
|
||||||
"theme.blog.tagTitle": "\"{tagName}\" ile etiketlenmiş {nPosts}",
|
"theme.blog.tagTitle": "\"{tagName}\" ile etiketlenmiş {nPosts}",
|
||||||
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "light mode",
|
||||||
"theme.common.editThisPage": "Bu sayfayı düzenle",
|
"theme.common.editThisPage": "Bu sayfayı düzenle",
|
||||||
"theme.common.headingLinkTitle": "Başlığa doğrudan bağlantı",
|
"theme.common.headingLinkTitle": "Başlığa doğrudan bağlantı",
|
||||||
"theme.common.skipToMainContent": "Ana içeriğe geç",
|
"theme.common.skipToMainContent": "Ana içeriğe geç",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "Điều hướng các bài viết gần đây trên blog",
|
"theme.blog.sidebar.navAriaLabel": "Điều hướng các bài viết gần đây trên blog",
|
||||||
"theme.blog.tagTitle": "{nPosts} được gắn thẻ \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} được gắn thẻ \"{tagName}\"",
|
||||||
"theme.colorToggle.ariaLabel": "Chuyển đổi chế độ sáng và tối (hiện tại {mode})",
|
"theme.colorToggle.ariaLabel": "Chuyển đổi chế độ sáng và tối (hiện tại {mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "chế độ sáng",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "chế độ tối",
|
"theme.colorToggle.ariaLabel.mode.dark": "chế độ tối",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "chế độ sáng",
|
||||||
"theme.common.editThisPage": "Sửa trang này",
|
"theme.common.editThisPage": "Sửa trang này",
|
||||||
"theme.common.headingLinkTitle": "Đường dẫn trực tiếp tới đề mục này",
|
"theme.common.headingLinkTitle": "Đường dẫn trực tiếp tới đề mục này",
|
||||||
"theme.common.skipToMainContent": "Nhảy tới nội dung",
|
"theme.common.skipToMainContent": "Nhảy tới nội dung",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "最近博文导航",
|
"theme.blog.sidebar.navAriaLabel": "最近博文导航",
|
||||||
"theme.blog.tagTitle": "{nPosts} 含有标签「{tagName}」",
|
"theme.blog.tagTitle": "{nPosts} 含有标签「{tagName}」",
|
||||||
"theme.colorToggle.ariaLabel": "切换浅色/暗黑模式(当前为{mode})",
|
"theme.colorToggle.ariaLabel": "切换浅色/暗黑模式(当前为{mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "浅色模式",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "暗黑模式",
|
"theme.colorToggle.ariaLabel.mode.dark": "暗黑模式",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "浅色模式",
|
||||||
"theme.common.editThisPage": "编辑此页",
|
"theme.common.editThisPage": "编辑此页",
|
||||||
"theme.common.headingLinkTitle": "标题的直接链接",
|
"theme.common.headingLinkTitle": "标题的直接链接",
|
||||||
"theme.common.skipToMainContent": "跳到主要内容",
|
"theme.common.skipToMainContent": "跳到主要内容",
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
"theme.blog.sidebar.navAriaLabel": "最近部落格文章導覽",
|
"theme.blog.sidebar.navAriaLabel": "最近部落格文章導覽",
|
||||||
"theme.blog.tagTitle": "{nPosts} 含有標籤「{tagName}」",
|
"theme.blog.tagTitle": "{nPosts} 含有標籤「{tagName}」",
|
||||||
"theme.colorToggle.ariaLabel": "切換淺色/暗黑模式(當前為{mode})",
|
"theme.colorToggle.ariaLabel": "切換淺色/暗黑模式(當前為{mode})",
|
||||||
"theme.colorToggle.ariaLabel.mode.light": "淺色模式",
|
|
||||||
"theme.colorToggle.ariaLabel.mode.dark": "暗黑模式",
|
"theme.colorToggle.ariaLabel.mode.dark": "暗黑模式",
|
||||||
|
"theme.colorToggle.ariaLabel.mode.light": "淺色模式",
|
||||||
"theme.common.editThisPage": "編輯此頁",
|
"theme.common.editThisPage": "編輯此頁",
|
||||||
"theme.common.headingLinkTitle": "標題的直接連結",
|
"theme.common.headingLinkTitle": "標題的直接連結",
|
||||||
"theme.common.skipToMainContent": "跳至主要内容",
|
"theme.common.skipToMainContent": "跳至主要内容",
|
||||||
|
|
|
@ -14,9 +14,9 @@
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc -p tsconfig.build.json",
|
||||||
"watch": "tsc --watch",
|
"watch": "tsc -p tsconfig.build.json --watch",
|
||||||
"update": "node ./update.js"
|
"update": "node ./update.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^10.0.1",
|
||||||
|
|
5
packages/docusaurus-theme-translations/src/__tests__/__fixtures__/theme/index.js
generated
Normal file
5
packages/docusaurus-theme-translations/src/__tests__/__fixtures__/theme/index.js
generated
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import Translate from '@docusaurus/Translate';
|
||||||
|
|
||||||
|
export default function Foo() {
|
||||||
|
return <Translate>{index}</Translate>;
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* 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 {extractThemeCodeMessages} from '../utils';
|
||||||
|
|
||||||
|
describe('extractThemeCodeMessages', () => {
|
||||||
|
it('throws with invalid syntax', async () => {
|
||||||
|
await expect(() =>
|
||||||
|
extractThemeCodeMessages([path.join(__dirname, '__fixtures__/theme')]),
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"
|
||||||
|
Please make sure all theme translations are static!
|
||||||
|
Some warnings were found!
|
||||||
|
|
||||||
|
Translate content could not be extracted. It has to be a static string and use optional but static props, like <Translate id=\\"my-id\\" description=\\"my-description\\">text</Translate>.
|
||||||
|
File: packages/docusaurus-theme-translations/src/__tests__/__fixtures__/theme/index.js at line 4
|
||||||
|
Full code: <Translate>{index}</Translate>
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
100
packages/docusaurus-theme-translations/src/utils.ts
Normal file
100
packages/docusaurus-theme-translations/src/utils.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This file isn't used by index.ts. It's used by update.mjs and tests. It's
|
||||||
|
// only here so that (a) we get a partially typed infrastructure (although the
|
||||||
|
// update script has ts-check anyways) (b) the test coverage isn't destroyed by
|
||||||
|
// the untested update.mjs file (c) we can ergonomically import the util
|
||||||
|
// functions in the Jest test without using `await import`
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
// Unsafe import, should we create a package for the translationsExtractor ?;
|
||||||
|
import {
|
||||||
|
globSourceCodeFilePaths,
|
||||||
|
extractAllSourceCodeFileTranslations,
|
||||||
|
} from '@docusaurus/core/lib/server/translations/translationsExtractor';
|
||||||
|
import type {TranslationFileContent} from '@docusaurus/types';
|
||||||
|
|
||||||
|
async function getPackageCodePath(packageName: string) {
|
||||||
|
const packagePath = path.join(__dirname, '../..', packageName);
|
||||||
|
const packageJsonPath = path.join(packagePath, 'package.json');
|
||||||
|
const {main} = await fs.readJSON(packageJsonPath);
|
||||||
|
const packageSrcPath = path.join(packagePath, path.dirname(main));
|
||||||
|
const packageLibNextPath = packageSrcPath.replace('lib', 'lib-next');
|
||||||
|
return (await fs.pathExists(packageLibNextPath))
|
||||||
|
? packageLibNextPath
|
||||||
|
: packageSrcPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getThemes(): Promise<{name: string; src: string[]}[]> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'theme-common',
|
||||||
|
src: [
|
||||||
|
await getPackageCodePath('docusaurus-theme-classic'),
|
||||||
|
await getPackageCodePath('docusaurus-theme-common'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'theme-search-algolia',
|
||||||
|
src: [await getPackageCodePath('docusaurus-theme-search-algolia')],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'theme-live-codeblock',
|
||||||
|
src: [await getPackageCodePath('docusaurus-theme-live-codeblock')],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin-pwa',
|
||||||
|
src: [await getPackageCodePath('docusaurus-plugin-pwa')],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'plugin-ideal-image',
|
||||||
|
src: [await getPackageCodePath('docusaurus-plugin-ideal-image')],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function extractThemeCodeMessages(
|
||||||
|
targetDirs?: string[],
|
||||||
|
): Promise<TranslationFileContent> {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
targetDirs ??= (await getThemes()).flatMap((theme) => theme.src);
|
||||||
|
|
||||||
|
const filePaths = (await globSourceCodeFilePaths(targetDirs)).filter(
|
||||||
|
(filePath) => ['.js', '.jsx'].includes(path.extname(filePath)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const filesExtractedTranslations = await extractAllSourceCodeFileTranslations(
|
||||||
|
filePaths,
|
||||||
|
{
|
||||||
|
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
filesExtractedTranslations.forEach((fileExtractedTranslations) => {
|
||||||
|
if (fileExtractedTranslations.warnings.length > 0) {
|
||||||
|
throw new Error(`
|
||||||
|
Please make sure all theme translations are static!
|
||||||
|
Some warnings were found!
|
||||||
|
|
||||||
|
${fileExtractedTranslations.warnings.join('\n\n')}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const translations = filesExtractedTranslations.reduce(
|
||||||
|
(acc, extractedTranslations) => ({
|
||||||
|
...acc,
|
||||||
|
...extractedTranslations.translations,
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
return translations;
|
||||||
|
}
|
11
packages/docusaurus-theme-translations/tsconfig.build.json
Normal file
11
packages/docusaurus-theme-translations/tsconfig.build.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"incremental": true,
|
||||||
|
"tsBuildInfoFile": "./lib/.tsbuildinfo",
|
||||||
|
"sourceMap": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "lib"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,10 @@
|
||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"incremental": true,
|
"module": "esnext",
|
||||||
"tsBuildInfoFile": "./lib/.tsbuildinfo",
|
"noEmit": true,
|
||||||
"sourceMap": true,
|
"checkJs": true,
|
||||||
"declarationMap": true,
|
"allowJs": true
|
||||||
"rootDir": "src",
|
},
|
||||||
"outDir": "lib"
|
"include": ["update.mjs", "src"]
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +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 type {TranslationFileContent} from '@docusaurus/types';
|
|
||||||
|
|
||||||
export function extractThemeCodeMessages(
|
|
||||||
targetDirs?: string[],
|
|
||||||
): Promise<TranslationFileContent>;
|
|
|
@ -1,353 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// @ts-check
|
|
||||||
// TODO convert this to ESM, which would also allow TLA
|
|
||||||
/* eslint-disable import/no-extraneous-dependencies, no-restricted-properties */
|
|
||||||
|
|
||||||
const logger = require('@docusaurus/logger').default;
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const LocalesDirPath = path.join(__dirname, 'locales');
|
|
||||||
const Themes = [
|
|
||||||
{
|
|
||||||
name: 'theme-common',
|
|
||||||
src: [
|
|
||||||
getPackageCodePath('docusaurus-theme-classic'),
|
|
||||||
getPackageCodePath('docusaurus-theme-common'),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'theme-search-algolia',
|
|
||||||
src: [getPackageCodePath('docusaurus-theme-search-algolia')],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'theme-live-codeblock',
|
|
||||||
src: [getPackageCodePath('docusaurus-theme-live-codeblock')],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin-pwa',
|
|
||||||
src: [getPackageCodePath('docusaurus-plugin-pwa')],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin-ideal-image',
|
|
||||||
src: [getPackageCodePath('docusaurus-plugin-ideal-image')],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const AllThemesSrcDirs = Themes.flatMap((theme) => theme.src);
|
|
||||||
|
|
||||||
logger.info`Will scan folders for code translations:path=${AllThemesSrcDirs}`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} packageName
|
|
||||||
*/
|
|
||||||
function getPackageCodePath(packageName) {
|
|
||||||
const packagePath = path.join(__dirname, '..', packageName);
|
|
||||||
const packageJsonPath = path.join(packagePath, 'package.json');
|
|
||||||
const {main} = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
||||||
const packageSrcPath = path.join(packagePath, path.dirname(main));
|
|
||||||
const packageLibNextPath = packageSrcPath.replace('lib', 'lib-next');
|
|
||||||
return fs.existsSync(packageLibNextPath)
|
|
||||||
? packageLibNextPath
|
|
||||||
: packageSrcPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} locale
|
|
||||||
* @param {string} themeName
|
|
||||||
*/
|
|
||||||
function getThemeLocalePath(locale, themeName) {
|
|
||||||
return path.join(LocalesDirPath, locale, `${themeName}.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} key
|
|
||||||
*/
|
|
||||||
function removeDescriptionSuffix(key) {
|
|
||||||
if (key.replace('___DESCRIPTION', '')) {
|
|
||||||
return key.replace('___DESCRIPTION', '');
|
|
||||||
}
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Record<string, string>} obj
|
|
||||||
*/
|
|
||||||
function sortObjectKeys(obj) {
|
|
||||||
let keys = Object.keys(obj);
|
|
||||||
keys = _.orderBy(keys, [(k) => removeDescriptionSuffix(k)]);
|
|
||||||
return keys.reduce((acc, key) => {
|
|
||||||
acc[key] = obj[key];
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string[]} targetDirs
|
|
||||||
* @returns {Promise<import('@docusaurus/types').TranslationFileContent>}
|
|
||||||
*/
|
|
||||||
async function extractThemeCodeMessages(targetDirs = AllThemesSrcDirs) {
|
|
||||||
// Unsafe import, should we create a package for the translationsExtractor ?
|
|
||||||
const {
|
|
||||||
globSourceCodeFilePaths,
|
|
||||||
extractAllSourceCodeFileTranslations,
|
|
||||||
// eslint-disable-next-line global-require
|
|
||||||
} = require('@docusaurus/core/lib/server/translations/translationsExtractor');
|
|
||||||
|
|
||||||
const filePaths = (await globSourceCodeFilePaths(targetDirs)).filter(
|
|
||||||
(filePath) => ['.js', '.jsx'].includes(path.extname(filePath)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const filesExtractedTranslations = await extractAllSourceCodeFileTranslations(
|
|
||||||
filePaths,
|
|
||||||
{
|
|
||||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
filesExtractedTranslations.forEach((fileExtractedTranslations) => {
|
|
||||||
fileExtractedTranslations.warnings.forEach((warning) => {
|
|
||||||
throw new Error(`
|
|
||||||
Please make sure all theme translations are static!
|
|
||||||
Some warnings were found!
|
|
||||||
|
|
||||||
${warning}
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const translations = filesExtractedTranslations.reduce(
|
|
||||||
(acc, extractedTranslations) => ({
|
|
||||||
...acc,
|
|
||||||
...extractedTranslations.translations,
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
return translations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} filePath
|
|
||||||
* @returns {Promise<Record<string, string>>}
|
|
||||||
*/
|
|
||||||
async function readMessagesFile(filePath) {
|
|
||||||
if (!(await fs.pathExists(filePath))) {
|
|
||||||
logger.info`File path=${filePath} not found. Creating new translation base file.`;
|
|
||||||
await fs.outputFile(filePath, '{}\n');
|
|
||||||
}
|
|
||||||
return JSON.parse((await fs.readFile(filePath)).toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} filePath
|
|
||||||
* @param {Record<string, string>} messages
|
|
||||||
*/
|
|
||||||
async function writeMessagesFile(filePath, messages) {
|
|
||||||
const sortedMessages = sortObjectKeys(messages);
|
|
||||||
|
|
||||||
const content = `${JSON.stringify(sortedMessages, null, 2)}\n`; // \n makes prettier happy
|
|
||||||
await fs.outputFile(filePath, content);
|
|
||||||
logger.info`path=${path.basename(
|
|
||||||
filePath,
|
|
||||||
)} updated subdue=${logger.interpolate`(number=${
|
|
||||||
Object.keys(sortedMessages).length
|
|
||||||
} messages)`}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} themeName
|
|
||||||
*/
|
|
||||||
async function getCodeTranslationFiles(themeName) {
|
|
||||||
const baseFile = getThemeLocalePath('base', themeName);
|
|
||||||
const localesFiles = (await fs.readdir(LocalesDirPath))
|
|
||||||
.filter((dirName) => dirName !== 'base')
|
|
||||||
.map((locale) => getThemeLocalePath(locale, themeName));
|
|
||||||
return {baseFile, localesFiles};
|
|
||||||
}
|
|
||||||
|
|
||||||
const DescriptionSuffix = '___DESCRIPTION';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} baseFile
|
|
||||||
* @param {string[]} targetDirs
|
|
||||||
*/
|
|
||||||
async function updateBaseFile(baseFile, targetDirs) {
|
|
||||||
const baseMessagesWithDescriptions = await readMessagesFile(baseFile);
|
|
||||||
const baseMessages = _.pickBy(
|
|
||||||
baseMessagesWithDescriptions,
|
|
||||||
(v, key) => !key.endsWith(DescriptionSuffix),
|
|
||||||
);
|
|
||||||
|
|
||||||
const codeExtractedTranslations = await extractThemeCodeMessages(targetDirs);
|
|
||||||
const codeMessages = _.mapValues(
|
|
||||||
codeExtractedTranslations,
|
|
||||||
(translation) => translation.message,
|
|
||||||
);
|
|
||||||
|
|
||||||
const unknownMessages = _.difference(
|
|
||||||
Object.keys(baseMessages),
|
|
||||||
Object.keys(codeMessages),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (unknownMessages.length) {
|
|
||||||
logger.error`Some messages exist in base locale but were not found by the code extractor!
|
|
||||||
They won't be removed automatically, so do the cleanup manually if necessary! code=${unknownMessages}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newBaseMessages = {
|
|
||||||
...baseMessages, // Ensure we don't automatically remove unknown messages
|
|
||||||
...codeMessages,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {Record<string, string>} */
|
|
||||||
const newBaseMessagesDescriptions = Object.entries(newBaseMessages).reduce(
|
|
||||||
(acc, [key]) => {
|
|
||||||
const codeTranslation = codeExtractedTranslations[key];
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[`${key}${DescriptionSuffix}`]: codeTranslation
|
|
||||||
? codeTranslation.description
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const newBaseMessagesWitDescription = {
|
|
||||||
...newBaseMessages,
|
|
||||||
...newBaseMessagesDescriptions,
|
|
||||||
};
|
|
||||||
|
|
||||||
await writeMessagesFile(baseFile, newBaseMessagesWitDescription);
|
|
||||||
|
|
||||||
return newBaseMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} localeFile
|
|
||||||
* @param {Record<string, string>} baseFileMessages
|
|
||||||
*/
|
|
||||||
async function updateLocaleCodeTranslations(localeFile, baseFileMessages) {
|
|
||||||
const localeFileMessages = await readMessagesFile(localeFile);
|
|
||||||
|
|
||||||
const unknownMessages = _.difference(
|
|
||||||
Object.keys(localeFileMessages),
|
|
||||||
Object.keys(baseFileMessages),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (unknownMessages.length) {
|
|
||||||
logger.error`Some localized messages do not exist in base.json!
|
|
||||||
You may want to delete these! code=${unknownMessages}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newLocaleFileMessages = {
|
|
||||||
...baseFileMessages,
|
|
||||||
...localeFileMessages,
|
|
||||||
};
|
|
||||||
|
|
||||||
const untranslatedKeys = Object.entries(newLocaleFileMessages)
|
|
||||||
.filter(([key, value]) => value === baseFileMessages[key])
|
|
||||||
.map(([key]) => key);
|
|
||||||
|
|
||||||
if (untranslatedKeys.length) {
|
|
||||||
logger.warn`Some messages do not seem to be translated! code=${untranslatedKeys}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeMessagesFile(localeFile, newLocaleFileMessages);
|
|
||||||
return {untranslated: untranslatedKeys.length};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateCodeTranslations() {
|
|
||||||
/** @type {Record<string, {untranslated: number}>} */
|
|
||||||
const stats = {};
|
|
||||||
let messageCount = 0;
|
|
||||||
const {2: newLocale} = process.argv;
|
|
||||||
for (const theme of Themes) {
|
|
||||||
const {baseFile, localesFiles} = await getCodeTranslationFiles(theme.name);
|
|
||||||
logger.info`Will update base file for name=${theme.name}\n`;
|
|
||||||
const baseFileMessages = await updateBaseFile(baseFile, theme.src);
|
|
||||||
|
|
||||||
if (newLocale) {
|
|
||||||
const newLocalePath = getThemeLocalePath(newLocale, theme.name);
|
|
||||||
|
|
||||||
if (!fs.existsSync(newLocalePath)) {
|
|
||||||
await writeMessagesFile(newLocalePath, baseFileMessages);
|
|
||||||
logger.success`Locale file path=${path.basename(
|
|
||||||
newLocalePath,
|
|
||||||
)} have been created.`;
|
|
||||||
} else {
|
|
||||||
logger.warn`Locale file path=${path.basename(
|
|
||||||
newLocalePath,
|
|
||||||
)} was already created!`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const localeFile of localesFiles) {
|
|
||||||
const localeName = path.basename(path.dirname(localeFile));
|
|
||||||
const pluginName = path.basename(localeFile, path.extname(localeFile));
|
|
||||||
logger.info`Will update name=${localeName} locale in name=${pluginName}`;
|
|
||||||
const stat = await updateLocaleCodeTranslations(
|
|
||||||
localeFile,
|
|
||||||
baseFileMessages,
|
|
||||||
);
|
|
||||||
|
|
||||||
stats[localeName] ??= {untranslated: 0};
|
|
||||||
stats[localeName].untranslated += stat.untranslated;
|
|
||||||
}
|
|
||||||
messageCount += Object.keys(baseFileMessages).length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (newLocale) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {stats, messageCount};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
updateCodeTranslations().then(
|
|
||||||
(result) => {
|
|
||||||
logger.success('updateCodeTranslations end\n');
|
|
||||||
if (result) {
|
|
||||||
const {stats, messageCount} = result;
|
|
||||||
const locales = Object.entries(stats).sort(
|
|
||||||
(a, b) => a[1].untranslated - b[1].untranslated,
|
|
||||||
);
|
|
||||||
const messages = locales.map(([name, stat]) => {
|
|
||||||
const percentage = (messageCount - stat.untranslated) / messageCount;
|
|
||||||
const filled = Math.floor(percentage * 30);
|
|
||||||
const color =
|
|
||||||
// eslint-disable-next-line no-nested-ternary
|
|
||||||
percentage > 0.99
|
|
||||||
? logger.green
|
|
||||||
: percentage > 0.7
|
|
||||||
? logger.yellow
|
|
||||||
: logger.red;
|
|
||||||
const progress = color(
|
|
||||||
`[${''.padStart(filled, '=')}${''.padStart(30 - filled, ' ')}]`,
|
|
||||||
);
|
|
||||||
return logger.interpolate`name=${name.padStart(8)} ${progress} ${(
|
|
||||||
percentage * 100
|
|
||||||
).toFixed(1)} subdue=${`(${
|
|
||||||
messageCount - stat.untranslated
|
|
||||||
}/${messageCount})`}`;
|
|
||||||
});
|
|
||||||
logger.info`Translation coverage:
|
|
||||||
${messages.join('\n')}`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(e) => {
|
|
||||||
logger.error(
|
|
||||||
`\nupdateCodeTranslations failure: ${e.message}\n${e.stack}\n`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.extractThemeCodeMessages = extractThemeCodeMessages;
|
|
247
packages/docusaurus-theme-translations/update.mjs
Normal file
247
packages/docusaurus-theme-translations/update.mjs
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
|
||||||
|
import logger from '@docusaurus/logger';
|
||||||
|
import path from 'path';
|
||||||
|
import {fileURLToPath} from 'url';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import {getThemes, extractThemeCodeMessages} from './lib/utils.js';
|
||||||
|
|
||||||
|
const LocalesDirPath = fileURLToPath(new URL('locales', import.meta.url));
|
||||||
|
const Themes = await getThemes();
|
||||||
|
const AllThemesSrcDirs = Themes.flatMap((theme) => theme.src);
|
||||||
|
|
||||||
|
logger.info`Will scan folders for code translations:path=${AllThemesSrcDirs}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} locale
|
||||||
|
* @param {string} themeName
|
||||||
|
*/
|
||||||
|
function getThemeLocalePath(locale, themeName) {
|
||||||
|
return path.join(LocalesDirPath, locale, `${themeName}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} key
|
||||||
|
*/
|
||||||
|
function removeDescriptionSuffix(key) {
|
||||||
|
if (key.replace('___DESCRIPTION', '')) {
|
||||||
|
return key.replace('___DESCRIPTION', '');
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, string>} obj
|
||||||
|
*/
|
||||||
|
function sortObjectKeys(obj) {
|
||||||
|
const keys = _.orderBy(Object.keys(obj), (k) => removeDescriptionSuffix(k));
|
||||||
|
return Object.fromEntries(keys.map((k) => [k, obj[k]]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} filePath
|
||||||
|
* @returns {Promise<Record<string, string>>}
|
||||||
|
*/
|
||||||
|
async function readMessagesFile(filePath) {
|
||||||
|
if (!(await fs.pathExists(filePath))) {
|
||||||
|
logger.info`File path=${filePath} not found. Creating new translation base file.`;
|
||||||
|
await fs.outputFile(filePath, '{}\n');
|
||||||
|
}
|
||||||
|
return JSON.parse((await fs.readFile(filePath)).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} filePath
|
||||||
|
* @param {Record<string, string>} messages
|
||||||
|
*/
|
||||||
|
async function writeMessagesFile(filePath, messages) {
|
||||||
|
const sortedMessages = sortObjectKeys(messages);
|
||||||
|
|
||||||
|
const content = `${JSON.stringify(sortedMessages, null, 2)}\n`; // \n makes prettier happy
|
||||||
|
await fs.outputFile(filePath, content);
|
||||||
|
logger.info`path=${path.basename(
|
||||||
|
filePath,
|
||||||
|
)} updated subdue=${logger.interpolate`(number=${
|
||||||
|
Object.keys(sortedMessages).length
|
||||||
|
} messages)`}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} themeName
|
||||||
|
*/
|
||||||
|
async function getCodeTranslationFiles(themeName) {
|
||||||
|
const baseFile = getThemeLocalePath('base', themeName);
|
||||||
|
const localesFiles = (await fs.readdir(LocalesDirPath))
|
||||||
|
.filter((dirName) => dirName !== 'base' && !dirName.startsWith('__'))
|
||||||
|
.map((locale) => getThemeLocalePath(locale, themeName));
|
||||||
|
return {baseFile, localesFiles};
|
||||||
|
}
|
||||||
|
|
||||||
|
const DescriptionSuffix = '___DESCRIPTION';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} baseFile
|
||||||
|
* @param {string[]} targetDirs
|
||||||
|
*/
|
||||||
|
async function updateBaseFile(baseFile, targetDirs) {
|
||||||
|
const baseMessagesWithDescriptions = await readMessagesFile(baseFile);
|
||||||
|
const baseMessages = _.pickBy(
|
||||||
|
baseMessagesWithDescriptions,
|
||||||
|
(v, key) => !key.endsWith(DescriptionSuffix),
|
||||||
|
);
|
||||||
|
|
||||||
|
const codeExtractedTranslations = await extractThemeCodeMessages(targetDirs);
|
||||||
|
const codeMessages = _.mapValues(
|
||||||
|
codeExtractedTranslations,
|
||||||
|
(translation) => translation.message,
|
||||||
|
);
|
||||||
|
|
||||||
|
const unknownMessages = _.difference(
|
||||||
|
Object.keys(baseMessages),
|
||||||
|
Object.keys(codeMessages),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (unknownMessages.length) {
|
||||||
|
logger.error`Some messages exist in base locale but were not found by the code extractor!
|
||||||
|
They won't be removed automatically, so do the cleanup manually if necessary! code=${unknownMessages}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBaseMessages = {
|
||||||
|
...baseMessages, // Ensure we don't automatically remove unknown messages
|
||||||
|
...codeMessages,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {Record<string, string>} */
|
||||||
|
const newBaseMessagesDescriptions = Object.entries(newBaseMessages).reduce(
|
||||||
|
(acc, [key]) => {
|
||||||
|
const codeTranslation = codeExtractedTranslations[key];
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[`${key}${DescriptionSuffix}`]: codeTranslation
|
||||||
|
? codeTranslation.description
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const newBaseMessagesWitDescription = {
|
||||||
|
...newBaseMessages,
|
||||||
|
...newBaseMessagesDescriptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
await writeMessagesFile(baseFile, newBaseMessagesWitDescription);
|
||||||
|
|
||||||
|
return newBaseMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} localeFile
|
||||||
|
* @param {Record<string, string>} baseFileMessages
|
||||||
|
*/
|
||||||
|
async function updateLocaleCodeTranslations(localeFile, baseFileMessages) {
|
||||||
|
const localeFileMessages = await readMessagesFile(localeFile);
|
||||||
|
|
||||||
|
const unknownMessages = _.difference(
|
||||||
|
Object.keys(localeFileMessages),
|
||||||
|
Object.keys(baseFileMessages),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (unknownMessages.length) {
|
||||||
|
logger.error`Some localized messages do not exist in base.json!
|
||||||
|
You may want to delete these! code=${unknownMessages}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLocaleFileMessages = {
|
||||||
|
...baseFileMessages,
|
||||||
|
...localeFileMessages,
|
||||||
|
};
|
||||||
|
|
||||||
|
const untranslatedKeys = Object.entries(newLocaleFileMessages)
|
||||||
|
.filter(([key, value]) => value === baseFileMessages[key])
|
||||||
|
.map(([key]) => key);
|
||||||
|
|
||||||
|
if (untranslatedKeys.length) {
|
||||||
|
logger.warn`Some messages do not seem to be translated! code=${untranslatedKeys}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeMessagesFile(localeFile, newLocaleFileMessages);
|
||||||
|
return {untranslated: untranslatedKeys.length};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Record<string, {untranslated: number}>} */
|
||||||
|
const stats = {};
|
||||||
|
let messageCount = 0;
|
||||||
|
const {2: newLocale} = process.argv;
|
||||||
|
for (const theme of Themes) {
|
||||||
|
const {baseFile, localesFiles} = await getCodeTranslationFiles(theme.name);
|
||||||
|
logger.info`Will update base file for name=${theme.name}\n`;
|
||||||
|
const baseFileMessages = await updateBaseFile(baseFile, theme.src);
|
||||||
|
|
||||||
|
if (newLocale) {
|
||||||
|
const newLocalePath = getThemeLocalePath(newLocale, theme.name);
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(newLocalePath))) {
|
||||||
|
await writeMessagesFile(newLocalePath, baseFileMessages);
|
||||||
|
logger.success`Locale file path=${path.basename(
|
||||||
|
newLocalePath,
|
||||||
|
)} have been created.`;
|
||||||
|
} else {
|
||||||
|
logger.warn`Locale file path=${path.basename(
|
||||||
|
newLocalePath,
|
||||||
|
)} was already created!`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const localeFile of localesFiles) {
|
||||||
|
const localeName = path.basename(path.dirname(localeFile));
|
||||||
|
const pluginName = path.basename(localeFile, path.extname(localeFile));
|
||||||
|
logger.info`Will update name=${localeName} locale in name=${pluginName}`;
|
||||||
|
const stat = await updateLocaleCodeTranslations(
|
||||||
|
localeFile,
|
||||||
|
baseFileMessages,
|
||||||
|
);
|
||||||
|
|
||||||
|
(stats[localeName] ??= {untranslated: 0}).untranslated +=
|
||||||
|
stat.untranslated;
|
||||||
|
}
|
||||||
|
messageCount += Object.keys(baseFileMessages).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success('updateCodeTranslations end\n');
|
||||||
|
if (newLocale) {
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
const locales = Object.entries(stats).sort(
|
||||||
|
(a, b) => a[1].untranslated - b[1].untranslated,
|
||||||
|
);
|
||||||
|
const messages = locales.map(([name, stat]) => {
|
||||||
|
const percentage = (messageCount - stat.untranslated) / messageCount;
|
||||||
|
const filled = Math.floor(percentage * 30);
|
||||||
|
const color =
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
percentage > 0.99
|
||||||
|
? logger.green
|
||||||
|
: percentage > 0.7
|
||||||
|
? logger.yellow
|
||||||
|
: logger.red;
|
||||||
|
const progress = color(
|
||||||
|
`[${''.padStart(filled, '=')}${''.padStart(30 - filled, ' ')}]`,
|
||||||
|
);
|
||||||
|
return logger.interpolate`name=${name.padStart(8)} ${progress} ${(
|
||||||
|
percentage * 100
|
||||||
|
).toFixed(1)} subdue=${`(${
|
||||||
|
messageCount - stat.untranslated
|
||||||
|
}/${messageCount})`}`;
|
||||||
|
});
|
||||||
|
logger.info`Translation coverage:
|
||||||
|
${messages.join('\n')}`;
|
2
packages/docusaurus-types/src/index.d.ts
vendored
2
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -288,7 +288,7 @@ export interface Plugin<Content = unknown> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InitializedPlugin<Content = unknown> = Plugin<Content> & {
|
export type InitializedPlugin<Content = unknown> = Plugin<Content> & {
|
||||||
readonly options: PluginOptions;
|
readonly options: Required<PluginOptions>;
|
||||||
readonly version: DocusaurusPluginVersionInformation;
|
readonly version: DocusaurusPluginVersionInformation;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,107 @@
|
||||||
import {jest} from '@jest/globals';
|
import {jest} from '@jest/globals';
|
||||||
import Joi from '../Joi';
|
import Joi from '../Joi';
|
||||||
import {JoiFrontMatter} from '../JoiFrontMatter';
|
import {JoiFrontMatter} from '../JoiFrontMatter';
|
||||||
import {validateFrontMatter} from '../validationUtils';
|
import {
|
||||||
|
normalizePluginOptions,
|
||||||
|
normalizeThemeConfig,
|
||||||
|
validateFrontMatter,
|
||||||
|
} from '../validationUtils';
|
||||||
|
|
||||||
|
describe('normalizePluginOptions', () => {
|
||||||
|
it('always adds an "id" field', () => {
|
||||||
|
const options = {id: 'a'};
|
||||||
|
expect(
|
||||||
|
normalizePluginOptions(
|
||||||
|
// "Malicious" schema that tries to forbid "id"
|
||||||
|
Joi.object({id: Joi.any().forbidden()}),
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
).toEqual(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes plugin options', () => {
|
||||||
|
const options = {};
|
||||||
|
expect(
|
||||||
|
normalizePluginOptions(
|
||||||
|
Joi.object({foo: Joi.string().default('a')}),
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
).toEqual({foo: 'a', id: 'default'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for invalid options', () => {
|
||||||
|
const options = {foo: 1};
|
||||||
|
expect(() =>
|
||||||
|
normalizePluginOptions(Joi.object({foo: Joi.string()}), options),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`"\\"foo\\" must be a string"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('warns', () => {
|
||||||
|
const options = {foo: 'a'};
|
||||||
|
const consoleMock = jest
|
||||||
|
.spyOn(console, 'warn')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
expect(
|
||||||
|
normalizePluginOptions(
|
||||||
|
Joi.object({foo: Joi.string().warning('deprecated', {})}).messages({
|
||||||
|
deprecated: '{#label} deprecated',
|
||||||
|
}),
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
).toEqual({foo: 'a', id: 'default'});
|
||||||
|
expect(consoleMock).toBeCalledWith(
|
||||||
|
expect.stringMatching(/"foo" deprecated/),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('normalizeThemeConfig', () => {
|
||||||
|
it('always allows unknown attributes', () => {
|
||||||
|
const themeConfig = {foo: 'a', bar: 1};
|
||||||
|
expect(
|
||||||
|
normalizeThemeConfig(
|
||||||
|
// "Malicious" schema that tries to forbid extra properties
|
||||||
|
Joi.object({foo: Joi.string()}).unknown(false),
|
||||||
|
themeConfig,
|
||||||
|
),
|
||||||
|
).toEqual(themeConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes theme config', () => {
|
||||||
|
const themeConfig = {bar: 1};
|
||||||
|
expect(
|
||||||
|
normalizeThemeConfig(
|
||||||
|
Joi.object({foo: Joi.string().default('a')}),
|
||||||
|
themeConfig,
|
||||||
|
),
|
||||||
|
).toEqual({bar: 1, foo: 'a'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for invalid options', () => {
|
||||||
|
const themeConfig = {foo: 1, bar: 1};
|
||||||
|
expect(() =>
|
||||||
|
normalizeThemeConfig(Joi.object({foo: Joi.string()}), themeConfig),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`"\\"foo\\" must be a string"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('warns', () => {
|
||||||
|
const themeConfig = {foo: 'a', bar: 1};
|
||||||
|
const consoleMock = jest
|
||||||
|
.spyOn(console, 'warn')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
expect(
|
||||||
|
normalizeThemeConfig(
|
||||||
|
Joi.object({foo: Joi.string().warning('deprecated', {})}).messages({
|
||||||
|
deprecated: '{#label} deprecated',
|
||||||
|
}),
|
||||||
|
themeConfig,
|
||||||
|
),
|
||||||
|
).toEqual(themeConfig);
|
||||||
|
expect(consoleMock).toBeCalledWith(
|
||||||
|
expect.stringMatching(/"foo" deprecated/),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('validateFrontMatter', () => {
|
describe('validateFrontMatter', () => {
|
||||||
it('accepts good values', () => {
|
it('accepts good values', () => {
|
||||||
|
|
|
@ -44,7 +44,7 @@ export function normalizeThemeConfig<T>(
|
||||||
schema: Joi.ObjectSchema<T>,
|
schema: Joi.ObjectSchema<T>,
|
||||||
themeConfig: Partial<T>,
|
themeConfig: Partial<T>,
|
||||||
): T {
|
): T {
|
||||||
// A theme should only validate his "slice" of the full themeConfig,
|
// A theme should only validate its "slice" of the full themeConfig,
|
||||||
// not the whole object, so we allow unknown attributes
|
// not the whole object, so we allow unknown attributes
|
||||||
// otherwise one theme would fail validating the data of another theme
|
// otherwise one theme would fail validating the data of another theme
|
||||||
const finalSchema = schema.unknown();
|
const finalSchema = schema.unknown();
|
||||||
|
|
|
@ -10,7 +10,8 @@ import {
|
||||||
parseMarkdownContentTitle,
|
parseMarkdownContentTitle,
|
||||||
parseMarkdownString,
|
parseMarkdownString,
|
||||||
parseMarkdownHeadingId,
|
parseMarkdownHeadingId,
|
||||||
} from '../markdownParser';
|
writeMarkdownHeadingId,
|
||||||
|
} from '../markdownUtils';
|
||||||
import dedent from 'dedent';
|
import dedent from 'dedent';
|
||||||
|
|
||||||
describe('createExcerpt', () => {
|
describe('createExcerpt', () => {
|
||||||
|
@ -801,3 +802,108 @@ describe('parseMarkdownHeadingId', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('writeMarkdownHeadingId', () => {
|
||||||
|
it('works for simple level-2 heading', () => {
|
||||||
|
expect(writeMarkdownHeadingId('## ABC')).toBe('## ABC {#abc}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works for simple level-3 heading', () => {
|
||||||
|
expect(writeMarkdownHeadingId('### ABC')).toBe('### ABC {#abc}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works for simple level-4 heading', () => {
|
||||||
|
expect(writeMarkdownHeadingId('#### ABC')).toBe('#### ABC {#abc}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unwraps markdown links', () => {
|
||||||
|
const input = `## hello [facebook](https://facebook.com) [crowdin](https://crowdin.com/translate/docusaurus-v2/126/en-fr?filter=basic&value=0)`;
|
||||||
|
expect(writeMarkdownHeadingId(input)).toBe(
|
||||||
|
`${input} {#hello-facebook-crowdin}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can slugify complex headings', () => {
|
||||||
|
const input = '## abc [Hello] How are you %Sébastien_-_$)( ## -56756';
|
||||||
|
expect(writeMarkdownHeadingId(input)).toBe(
|
||||||
|
// cSpell:ignore ébastien
|
||||||
|
`${input} {#abc-hello-how-are-you-sébastien_-_---56756}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not duplicate duplicate id', () => {
|
||||||
|
expect(writeMarkdownHeadingId('## hello world {#hello-world}')).toBe(
|
||||||
|
'## hello world {#hello-world}',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects existing heading', () => {
|
||||||
|
expect(writeMarkdownHeadingId('## New heading {#old-heading}')).toBe(
|
||||||
|
'## New heading {#old-heading}',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('overwrites heading ID when asked to', () => {
|
||||||
|
expect(
|
||||||
|
writeMarkdownHeadingId('## New heading {#old-heading}', {
|
||||||
|
overwrite: true,
|
||||||
|
}),
|
||||||
|
).toBe('## New heading {#new-heading}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maintains casing when asked to', () => {
|
||||||
|
expect(
|
||||||
|
writeMarkdownHeadingId('## getDataFromAPI()', {
|
||||||
|
maintainCase: true,
|
||||||
|
}),
|
||||||
|
).toBe('## getDataFromAPI() {#getDataFromAPI}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('transform the headings', () => {
|
||||||
|
const input = `
|
||||||
|
|
||||||
|
# Ignored title
|
||||||
|
|
||||||
|
## abc
|
||||||
|
|
||||||
|
### Hello world
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
# Heading in code block
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Hello world
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
# Heading in escaped code block
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### abc {#abc}
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
const expected = `
|
||||||
|
|
||||||
|
# Ignored title
|
||||||
|
|
||||||
|
## abc {#abc-1}
|
||||||
|
|
||||||
|
### Hello world {#hello-world}
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
# Heading in code block
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Hello world {#hello-world-1}
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
# Heading in escaped code block
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### abc {#abc}
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(writeMarkdownHeadingId(input)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
|
@ -15,11 +15,18 @@ import {
|
||||||
removeTrailingSlash,
|
removeTrailingSlash,
|
||||||
resolvePathname,
|
resolvePathname,
|
||||||
encodePath,
|
encodePath,
|
||||||
|
buildSshUrl,
|
||||||
|
buildHttpsUrl,
|
||||||
|
hasSSHProtocol,
|
||||||
} from '../urlUtils';
|
} from '../urlUtils';
|
||||||
|
|
||||||
describe('normalizeUrl', () => {
|
describe('normalizeUrl', () => {
|
||||||
it('normalizes urls correctly', () => {
|
it('normalizes urls correctly', () => {
|
||||||
const asserts = [
|
const asserts = [
|
||||||
|
{
|
||||||
|
input: [],
|
||||||
|
output: '',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: ['/', ''],
|
input: ['/', ''],
|
||||||
output: '/',
|
output: '/',
|
||||||
|
@ -248,3 +255,60 @@ describe('encodePath', () => {
|
||||||
expect(encodePath('a/你好/')).toBe('a/%E4%BD%A0%E5%A5%BD/');
|
expect(encodePath('a/你好/')).toBe('a/%E4%BD%A0%E5%A5%BD/');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('buildSshUrl', () => {
|
||||||
|
it('builds a normal ssh url', () => {
|
||||||
|
const url = buildSshUrl('github.com', 'facebook', 'docusaurus');
|
||||||
|
expect(url).toBe('git@github.com:facebook/docusaurus.git');
|
||||||
|
});
|
||||||
|
it('builds a ssh url with port', () => {
|
||||||
|
const url = buildSshUrl('github.com', 'facebook', 'docusaurus', '422');
|
||||||
|
expect(url).toBe('ssh://git@github.com:422/facebook/docusaurus.git');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildHttpsUrl', () => {
|
||||||
|
it('builds a normal http url', () => {
|
||||||
|
const url = buildHttpsUrl(
|
||||||
|
'user:pass',
|
||||||
|
'github.com',
|
||||||
|
'facebook',
|
||||||
|
'docusaurus',
|
||||||
|
);
|
||||||
|
expect(url).toBe('https://user:pass@github.com/facebook/docusaurus.git');
|
||||||
|
});
|
||||||
|
it('builds a normal http url with port', () => {
|
||||||
|
const url = buildHttpsUrl(
|
||||||
|
'user:pass',
|
||||||
|
'github.com',
|
||||||
|
'facebook',
|
||||||
|
'docusaurus',
|
||||||
|
'5433',
|
||||||
|
);
|
||||||
|
expect(url).toBe(
|
||||||
|
'https://user:pass@github.com:5433/facebook/docusaurus.git',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasSSHProtocol', () => {
|
||||||
|
it('recognizes explicit SSH protocol', () => {
|
||||||
|
const url = 'ssh://git@github.com:422/facebook/docusaurus.git';
|
||||||
|
expect(hasSSHProtocol(url)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recognizes implied SSH protocol', () => {
|
||||||
|
const url = 'git@github.com:facebook/docusaurus.git';
|
||||||
|
expect(hasSSHProtocol(url)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not recognize HTTPS with credentials', () => {
|
||||||
|
const url = 'https://user:pass@github.com/facebook/docusaurus.git';
|
||||||
|
expect(hasSSHProtocol(url)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not recognize plain HTTPS URL', () => {
|
||||||
|
const url = 'https://github.com:5433/facebook/docusaurus.git';
|
||||||
|
expect(hasSSHProtocol(url)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -45,6 +45,9 @@ export {
|
||||||
addLeadingSlash,
|
addLeadingSlash,
|
||||||
addTrailingSlash,
|
addTrailingSlash,
|
||||||
removeTrailingSlash,
|
removeTrailingSlash,
|
||||||
|
hasSSHProtocol,
|
||||||
|
buildHttpsUrl,
|
||||||
|
buildSshUrl,
|
||||||
} from './urlUtils';
|
} from './urlUtils';
|
||||||
export {
|
export {
|
||||||
type Tag,
|
type Tag,
|
||||||
|
@ -60,7 +63,9 @@ export {
|
||||||
parseFrontMatter,
|
parseFrontMatter,
|
||||||
parseMarkdownContentTitle,
|
parseMarkdownContentTitle,
|
||||||
parseMarkdownString,
|
parseMarkdownString,
|
||||||
} from './markdownParser';
|
writeMarkdownHeadingId,
|
||||||
|
type WriteHeadingIDOptions,
|
||||||
|
} from './markdownUtils';
|
||||||
export {
|
export {
|
||||||
type ContentPaths,
|
type ContentPaths,
|
||||||
type BrokenMarkdownLink,
|
type BrokenMarkdownLink,
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import matter from 'gray-matter';
|
import matter from 'gray-matter';
|
||||||
|
import {createSlugger, type Slugger} from './slugger';
|
||||||
|
|
||||||
// Input: ## Some heading {#some-heading}
|
// Input: ## Some heading {#some-heading}
|
||||||
// Output: {text: "## Some heading", id: "some-heading"}
|
// Output: {text: "## Some heading", id: "some-heading"}
|
||||||
|
@ -205,3 +206,72 @@ This can happen if you use special characters in front matter values (try using
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unwrapMarkdownLinks(line: string): string {
|
||||||
|
return line.replace(/\[(?<alt>[^\]]+)\]\([^)]+\)/g, (match, p1) => p1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addHeadingId(
|
||||||
|
line: string,
|
||||||
|
slugger: Slugger,
|
||||||
|
maintainCase: boolean,
|
||||||
|
): string {
|
||||||
|
let headingLevel = 0;
|
||||||
|
while (line.charAt(headingLevel) === '#') {
|
||||||
|
headingLevel += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headingText = line.slice(headingLevel).trimEnd();
|
||||||
|
const headingHashes = line.slice(0, headingLevel);
|
||||||
|
const slug = slugger.slug(unwrapMarkdownLinks(headingText).trim(), {
|
||||||
|
maintainCase,
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${headingHashes}${headingText} {#${slug}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WriteHeadingIDOptions = {
|
||||||
|
maintainCase?: boolean;
|
||||||
|
overwrite?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function writeMarkdownHeadingId(
|
||||||
|
content: string,
|
||||||
|
options: WriteHeadingIDOptions = {maintainCase: false, overwrite: false},
|
||||||
|
): string {
|
||||||
|
const {maintainCase = false, overwrite = false} = options;
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const slugger = createSlugger();
|
||||||
|
|
||||||
|
// If we can't overwrite existing slugs, make sure other headings don't
|
||||||
|
// generate colliding slugs by first marking these slugs as occupied
|
||||||
|
if (!overwrite) {
|
||||||
|
lines.forEach((line) => {
|
||||||
|
const parsedHeading = parseMarkdownHeadingId(line);
|
||||||
|
if (parsedHeading.id) {
|
||||||
|
slugger.slug(parsedHeading.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let inCode = false;
|
||||||
|
return lines
|
||||||
|
.map((line) => {
|
||||||
|
if (line.startsWith('```')) {
|
||||||
|
inCode = !inCode;
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
// Ignore h1 headings, as we don't create anchor links for those
|
||||||
|
if (inCode || !line.startsWith('##')) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
const parsedHeading = parseMarkdownHeadingId(line);
|
||||||
|
|
||||||
|
// Do not process if id is already there
|
||||||
|
if (parsedHeading.id && !overwrite) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
return addHeadingId(parsedHeading.text, slugger, maintainCase);
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
}
|
|
@ -154,3 +154,40 @@ export function addTrailingSlash(str: string): string {
|
||||||
export function removeTrailingSlash(str: string): string {
|
export function removeTrailingSlash(str: string): string {
|
||||||
return removeSuffix(str, '/');
|
return removeSuffix(str, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildSshUrl(
|
||||||
|
githubHost: string,
|
||||||
|
organizationName: string,
|
||||||
|
projectName: string,
|
||||||
|
githubPort?: string,
|
||||||
|
): string {
|
||||||
|
if (githubPort) {
|
||||||
|
return `ssh://git@${githubHost}:${githubPort}/${organizationName}/${projectName}.git`;
|
||||||
|
}
|
||||||
|
return `git@${githubHost}:${organizationName}/${projectName}.git`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildHttpsUrl(
|
||||||
|
gitCredentials: string,
|
||||||
|
githubHost: string,
|
||||||
|
organizationName: string,
|
||||||
|
projectName: string,
|
||||||
|
githubPort?: string,
|
||||||
|
): string {
|
||||||
|
if (githubPort) {
|
||||||
|
return `https://${gitCredentials}@${githubHost}:${githubPort}/${organizationName}/${projectName}.git`;
|
||||||
|
}
|
||||||
|
return `https://${gitCredentials}@${githubHost}/${organizationName}/${projectName}.git`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasSSHProtocol(sourceRepoUrl: string): boolean {
|
||||||
|
try {
|
||||||
|
if (new URL(sourceRepoUrl).protocol === 'ssh:') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
// Fails when there isn't a protocol
|
||||||
|
return /^(?:[\w-]+@)?[\w.-]+:[\w./-]+/.test(sourceRepoUrl); // git@github.com:facebook/docusaurus.git
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -98,9 +98,7 @@ export default async function beforeCli() {
|
||||||
.filter((p) => p.startsWith('@docusaurus'))
|
.filter((p) => p.startsWith('@docusaurus'))
|
||||||
.map((p) => p.concat('@latest'))
|
.map((p) => p.concat('@latest'))
|
||||||
.join(' ');
|
.join(' ');
|
||||||
const isYarnUsed = await fs.pathExists(
|
const isYarnUsed = await fs.pathExists(path.resolve('yarn.lock'));
|
||||||
path.resolve(process.cwd(), 'yarn.lock'),
|
|
||||||
);
|
|
||||||
const upgradeCommand = isYarnUsed
|
const upgradeCommand = isYarnUsed
|
||||||
? `yarn upgrade ${siteDocusaurusPackagesForUpdate}`
|
? `yarn upgrade ${siteDocusaurusPackagesForUpdate}`
|
||||||
: `npm i ${siteDocusaurusPackagesForUpdate}`;
|
: `npm i ${siteDocusaurusPackagesForUpdate}`;
|
||||||
|
|
|
@ -1,62 +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 {buildSshUrl, buildHttpsUrl, hasSSHProtocol} from '../deploy';
|
|
||||||
|
|
||||||
describe('remoteBranchUrl', () => {
|
|
||||||
it('builds a normal ssh url', () => {
|
|
||||||
const url = buildSshUrl('github.com', 'facebook', 'docusaurus');
|
|
||||||
expect(url).toBe('git@github.com:facebook/docusaurus.git');
|
|
||||||
});
|
|
||||||
it('builds a ssh url with port', () => {
|
|
||||||
const url = buildSshUrl('github.com', 'facebook', 'docusaurus', '422');
|
|
||||||
expect(url).toBe('ssh://git@github.com:422/facebook/docusaurus.git');
|
|
||||||
});
|
|
||||||
it('builds a normal http url', () => {
|
|
||||||
const url = buildHttpsUrl(
|
|
||||||
'user:pass',
|
|
||||||
'github.com',
|
|
||||||
'facebook',
|
|
||||||
'docusaurus',
|
|
||||||
);
|
|
||||||
expect(url).toBe('https://user:pass@github.com/facebook/docusaurus.git');
|
|
||||||
});
|
|
||||||
it('builds a normal http url with port', () => {
|
|
||||||
const url = buildHttpsUrl(
|
|
||||||
'user:pass',
|
|
||||||
'github.com',
|
|
||||||
'facebook',
|
|
||||||
'docusaurus',
|
|
||||||
'5433',
|
|
||||||
);
|
|
||||||
expect(url).toBe(
|
|
||||||
'https://user:pass@github.com:5433/facebook/docusaurus.git',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('hasSSHProtocol', () => {
|
|
||||||
it('recognizes explicit SSH protocol', () => {
|
|
||||||
const url = 'ssh://git@github.com:422/facebook/docusaurus.git';
|
|
||||||
expect(hasSSHProtocol(url)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('recognizes implied SSH protocol', () => {
|
|
||||||
const url = 'git@github.com:facebook/docusaurus.git';
|
|
||||||
expect(hasSSHProtocol(url)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not recognize HTTPS with credentials', () => {
|
|
||||||
const url = 'https://user:pass@github.com/facebook/docusaurus.git';
|
|
||||||
expect(hasSSHProtocol(url)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not recognize plain HTTPS URL', () => {
|
|
||||||
const url = 'https://github.com:5433/facebook/docusaurus.git';
|
|
||||||
expect(hasSSHProtocol(url)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,113 +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 {transformMarkdownContent} from '../writeHeadingIds';
|
|
||||||
|
|
||||||
describe('transformMarkdownContent', () => {
|
|
||||||
it('works for simple level-2 heading', () => {
|
|
||||||
expect(transformMarkdownContent('## ABC')).toBe('## ABC {#abc}');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('works for simple level-3 heading', () => {
|
|
||||||
expect(transformMarkdownContent('### ABC')).toBe('### ABC {#abc}');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('works for simple level-4 heading', () => {
|
|
||||||
expect(transformMarkdownContent('#### ABC')).toBe('#### ABC {#abc}');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('unwraps markdown links', () => {
|
|
||||||
const input = `## hello [facebook](https://facebook.com) [crowdin](https://crowdin.com/translate/docusaurus-v2/126/en-fr?filter=basic&value=0)`;
|
|
||||||
expect(transformMarkdownContent(input)).toBe(
|
|
||||||
`${input} {#hello-facebook-crowdin}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can slugify complex headings', () => {
|
|
||||||
const input = '## abc [Hello] How are you %Sébastien_-_$)( ## -56756';
|
|
||||||
expect(transformMarkdownContent(input)).toBe(
|
|
||||||
// cSpell:ignore ébastien
|
|
||||||
`${input} {#abc-hello-how-are-you-sébastien_-_---56756}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not duplicate duplicate id', () => {
|
|
||||||
expect(transformMarkdownContent('## hello world {#hello-world}')).toBe(
|
|
||||||
'## hello world {#hello-world}',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('respects existing heading', () => {
|
|
||||||
expect(transformMarkdownContent('## New heading {#old-heading}')).toBe(
|
|
||||||
'## New heading {#old-heading}',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('overwrites heading ID when asked to', () => {
|
|
||||||
expect(
|
|
||||||
transformMarkdownContent('## New heading {#old-heading}', {
|
|
||||||
overwrite: true,
|
|
||||||
}),
|
|
||||||
).toBe('## New heading {#new-heading}');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('maintains casing when asked to', () => {
|
|
||||||
expect(
|
|
||||||
transformMarkdownContent('## getDataFromAPI()', {
|
|
||||||
maintainCase: true,
|
|
||||||
}),
|
|
||||||
).toBe('## getDataFromAPI() {#getDataFromAPI}');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('transform the headings', () => {
|
|
||||||
const input = `
|
|
||||||
|
|
||||||
# Ignored title
|
|
||||||
|
|
||||||
## abc
|
|
||||||
|
|
||||||
### Hello world
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
# Heading in code block
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## Hello world
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
# Heading in escaped code block
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### abc {#abc}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const expected = `
|
|
||||||
|
|
||||||
# Ignored title
|
|
||||||
|
|
||||||
## abc {#abc-1}
|
|
||||||
|
|
||||||
### Hello world {#hello-world}
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
# Heading in code block
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## Hello world {#hello-world-1}
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
# Heading in escaped code block
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### abc {#abc}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
expect(transformMarkdownContent(input)).toEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -8,6 +8,7 @@
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import shell from 'shelljs';
|
import shell from 'shelljs';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
|
import {hasSSHProtocol, buildSshUrl, buildHttpsUrl} from '@docusaurus/utils';
|
||||||
import {loadContext} from '../server';
|
import {loadContext} from '../server';
|
||||||
import build from './build';
|
import build from './build';
|
||||||
import type {BuildCLIOptions} from '@docusaurus/types';
|
import type {BuildCLIOptions} from '@docusaurus/types';
|
||||||
|
@ -33,43 +34,6 @@ function shellExecLog(cmd: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSshUrl(
|
|
||||||
githubHost: string,
|
|
||||||
organizationName: string,
|
|
||||||
projectName: string,
|
|
||||||
githubPort?: string,
|
|
||||||
): string {
|
|
||||||
if (githubPort) {
|
|
||||||
return `ssh://git@${githubHost}:${githubPort}/${organizationName}/${projectName}.git`;
|
|
||||||
}
|
|
||||||
return `git@${githubHost}:${organizationName}/${projectName}.git`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildHttpsUrl(
|
|
||||||
gitCredentials: string,
|
|
||||||
githubHost: string,
|
|
||||||
organizationName: string,
|
|
||||||
projectName: string,
|
|
||||||
githubPort?: string,
|
|
||||||
): string {
|
|
||||||
if (githubPort) {
|
|
||||||
return `https://${gitCredentials}@${githubHost}:${githubPort}/${organizationName}/${projectName}.git`;
|
|
||||||
}
|
|
||||||
return `https://${gitCredentials}@${githubHost}/${organizationName}/${projectName}.git`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasSSHProtocol(sourceRepoUrl: string): boolean {
|
|
||||||
try {
|
|
||||||
if (new URL(sourceRepoUrl).protocol === 'ssh:') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch {
|
|
||||||
// Fails when there isn't a protocol
|
|
||||||
return /^(?:[\w-]+@)?[\w.-]+:[\w./-]+/.test(sourceRepoUrl); // git@github.com:facebook/docusaurus.git
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function deploy(
|
export default async function deploy(
|
||||||
siteDir: string,
|
siteDir: string,
|
||||||
cliOptions: Partial<BuildCLIOptions> = {},
|
cliOptions: Partial<BuildCLIOptions> = {},
|
||||||
|
|
|
@ -123,7 +123,7 @@ export default async function start(
|
||||||
plugins: [
|
plugins: [
|
||||||
// Generates an `index.html` file with the <script> injected.
|
// Generates an `index.html` file with the <script> injected.
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: path.resolve(
|
template: path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../webpack/templates/index.html.template.ejs',
|
'../webpack/templates/index.html.template.ejs',
|
||||||
),
|
),
|
||||||
|
|
|
@ -7,91 +7,20 @@
|
||||||
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
|
import {
|
||||||
|
writeMarkdownHeadingId,
|
||||||
|
type WriteHeadingIDOptions,
|
||||||
|
} from '@docusaurus/utils';
|
||||||
import {loadContext, loadPluginConfigs} from '../server';
|
import {loadContext, loadPluginConfigs} from '../server';
|
||||||
import initPlugins from '../server/plugins/init';
|
import initPlugins from '../server/plugins/init';
|
||||||
|
|
||||||
import {
|
|
||||||
parseMarkdownHeadingId,
|
|
||||||
createSlugger,
|
|
||||||
type Slugger,
|
|
||||||
} from '@docusaurus/utils';
|
|
||||||
import {safeGlobby} from '../server/utils';
|
import {safeGlobby} from '../server/utils';
|
||||||
|
|
||||||
type Options = {
|
|
||||||
maintainCase?: boolean;
|
|
||||||
overwrite?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function unwrapMarkdownLinks(line: string): string {
|
|
||||||
return line.replace(/\[(?<alt>[^\]]+)\]\([^)]+\)/g, (match, p1) => p1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addHeadingId(
|
|
||||||
line: string,
|
|
||||||
slugger: Slugger,
|
|
||||||
maintainCase: boolean,
|
|
||||||
): string {
|
|
||||||
let headingLevel = 0;
|
|
||||||
while (line.charAt(headingLevel) === '#') {
|
|
||||||
headingLevel += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headingText = line.slice(headingLevel).trimEnd();
|
|
||||||
const headingHashes = line.slice(0, headingLevel);
|
|
||||||
const slug = slugger.slug(unwrapMarkdownLinks(headingText).trim(), {
|
|
||||||
maintainCase,
|
|
||||||
});
|
|
||||||
|
|
||||||
return `${headingHashes}${headingText} {#${slug}}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transformMarkdownContent(
|
|
||||||
content: string,
|
|
||||||
options: Options = {maintainCase: false, overwrite: false},
|
|
||||||
): string {
|
|
||||||
const {maintainCase = false, overwrite = false} = options;
|
|
||||||
const lines = content.split('\n');
|
|
||||||
const slugger = createSlugger();
|
|
||||||
|
|
||||||
// If we can't overwrite existing slugs, make sure other headings don't
|
|
||||||
// generate colliding slugs by first marking these slugs as occupied
|
|
||||||
if (!overwrite) {
|
|
||||||
lines.forEach((line) => {
|
|
||||||
const parsedHeading = parseMarkdownHeadingId(line);
|
|
||||||
if (parsedHeading.id) {
|
|
||||||
slugger.slug(parsedHeading.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let inCode = false;
|
|
||||||
return lines
|
|
||||||
.map((line) => {
|
|
||||||
if (line.startsWith('```')) {
|
|
||||||
inCode = !inCode;
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
// Ignore h1 headings, as we don't create anchor links for those
|
|
||||||
if (inCode || !line.startsWith('##')) {
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
const parsedHeading = parseMarkdownHeadingId(line);
|
|
||||||
|
|
||||||
// Do not process if id is already there
|
|
||||||
if (parsedHeading.id && !overwrite) {
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
return addHeadingId(parsedHeading.text, slugger, maintainCase);
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function transformMarkdownFile(
|
async function transformMarkdownFile(
|
||||||
filepath: string,
|
filepath: string,
|
||||||
options?: Options,
|
options?: WriteHeadingIDOptions,
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
const content = await fs.readFile(filepath, 'utf8');
|
const content = await fs.readFile(filepath, 'utf8');
|
||||||
const updatedContent = transformMarkdownContent(content, options);
|
const updatedContent = writeMarkdownHeadingId(content, options);
|
||||||
if (content !== updatedContent) {
|
if (content !== updatedContent) {
|
||||||
await fs.writeFile(filepath, updatedContent);
|
await fs.writeFile(filepath, updatedContent);
|
||||||
return filepath;
|
return filepath;
|
||||||
|
@ -118,7 +47,7 @@ async function getPathsToWatch(siteDir: string): Promise<string[]> {
|
||||||
export default async function writeHeadingIds(
|
export default async function writeHeadingIds(
|
||||||
siteDir: string,
|
siteDir: string,
|
||||||
files?: string[],
|
files?: string[],
|
||||||
options?: Options,
|
options?: WriteHeadingIDOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const markdownFiles = await safeGlobby(
|
const markdownFiles = await safeGlobby(
|
||||||
files ?? (await getPathsToWatch(siteDir)),
|
files ?? (await getPathsToWatch(siteDir)),
|
||||||
|
|
|
@ -21,7 +21,7 @@ exports[`loadRoutes loads flat route config 1`] = `
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"routesChunkNames": {
|
"routesChunkNames": {
|
||||||
"/blog-94e": {
|
"/blog-1e7": {
|
||||||
"component": "component---theme-blog-list-pagea-6-a-7ba",
|
"component": "component---theme-blog-list-pagea-6-a-7ba",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
@ -32,6 +32,10 @@ exports[`loadRoutes loads flat route config 1`] = `
|
||||||
"content": "content---blog-7-b-8-fd9",
|
"content": "content---blog-7-b-8-fd9",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"content": "content---blog-7-b-8-fd9",
|
||||||
|
"metadata": null,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -42,7 +46,7 @@ import ComponentCreator from '@docusaurus/ComponentCreator';
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
path: '/blog',
|
path: '/blog',
|
||||||
component: ComponentCreator('/blog','94e'),
|
component: ComponentCreator('/blog','1e7'),
|
||||||
exact: true
|
exact: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -96,11 +100,11 @@ exports[`loadRoutes loads nested route config 1`] = `
|
||||||
"content": "content---docs-helloaff-811",
|
"content": "content---docs-helloaff-811",
|
||||||
"metadata": "metadata---docs-hello-956-741",
|
"metadata": "metadata---docs-hello-956-741",
|
||||||
},
|
},
|
||||||
"/docs:route-63b": {
|
"/docs:route-52d": {
|
||||||
"component": "component---theme-doc-page-1-be-9be",
|
"component": "component---theme-doc-page-1-be-9be",
|
||||||
"docsMetadata": "docsMetadata---docs-routef-34-881",
|
"docsMetadata": "docsMetadata---docs-routef-34-881",
|
||||||
},
|
},
|
||||||
"docs/foo/baz-ac2": {
|
"docs/foo/baz-070": {
|
||||||
"component": "component---theme-doc-item-178-a40",
|
"component": "component---theme-doc-item-178-a40",
|
||||||
"content": "content---docs-foo-baz-8-ce-61e",
|
"content": "content---docs-foo-baz-8-ce-61e",
|
||||||
"metadata": "metadata---docs-foo-baz-2-cf-fa7",
|
"metadata": "metadata---docs-foo-baz-2-cf-fa7",
|
||||||
|
@ -113,18 +117,23 @@ import ComponentCreator from '@docusaurus/ComponentCreator';
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
path: '/docs:route',
|
path: '/docs:route',
|
||||||
component: ComponentCreator('/docs:route','63b'),
|
component: ComponentCreator('/docs:route','52d'),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/docs/hello',
|
path: '/docs/hello',
|
||||||
component: ComponentCreator('/docs/hello','44b'),
|
component: ComponentCreator('/docs/hello','44b'),
|
||||||
exact: true,
|
exact: true,
|
||||||
'sidebar': \\"main\\"
|
sidebar: \\"main\\"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'docs/foo/baz',
|
path: 'docs/foo/baz',
|
||||||
component: ComponentCreator('docs/foo/baz','ac2'),
|
component: ComponentCreator('docs/foo/baz','070'),
|
||||||
'sidebar': \\"secondary\\"
|
sidebar: \\"secondary\\",
|
||||||
|
\\"key:a\\": \\"containing colon\\",
|
||||||
|
\\"key'b\\": \\"containing quote\\",
|
||||||
|
\\"key\\\\\\"c\\": \\"containing double quote\\",
|
||||||
|
\\"key,d\\": \\"containing comma\\",
|
||||||
|
字段: \\"containing unicode\\"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -35,6 +35,11 @@ describe('loadRoutes', () => {
|
||||||
metadata: 'docs-foo-baz-dd9.json',
|
metadata: 'docs-foo-baz-dd9.json',
|
||||||
},
|
},
|
||||||
sidebar: 'secondary',
|
sidebar: 'secondary',
|
||||||
|
'key:a': 'containing colon',
|
||||||
|
"key'b": 'containing quote',
|
||||||
|
'key"c': 'containing double quote',
|
||||||
|
'key,d': 'containing comma',
|
||||||
|
字段: 'containing unicode',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -64,6 +69,13 @@ describe('loadRoutes', () => {
|
||||||
content: 'blog/2018-12-14-Happy-First-Birthday-Slash.md',
|
content: 'blog/2018-12-14-Happy-First-Birthday-Slash.md',
|
||||||
metadata: null,
|
metadata: null,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
content: {
|
||||||
|
__import: true,
|
||||||
|
path: 'blog/2018-12-14-Happy-First-Birthday-Slash.md',
|
||||||
|
},
|
||||||
|
metadata: null,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -56,7 +56,7 @@ export const DEFAULT_CONFIG: Pick<
|
||||||
staticDirectories: [STATIC_DIR_NAME],
|
staticDirectories: [STATIC_DIR_NAME],
|
||||||
};
|
};
|
||||||
|
|
||||||
function createPluginSchema(theme: boolean = false) {
|
function createPluginSchema(theme: boolean) {
|
||||||
return (
|
return (
|
||||||
Joi.alternatives()
|
Joi.alternatives()
|
||||||
.try(
|
.try(
|
||||||
|
|
|
@ -30,7 +30,7 @@ export function getDefaultLocaleConfig(locale: string): I18nLocaleConfig {
|
||||||
|
|
||||||
export async function loadI18n(
|
export async function loadI18n(
|
||||||
config: DocusaurusConfig,
|
config: DocusaurusConfig,
|
||||||
options: {locale?: string} = {},
|
options: {locale?: string},
|
||||||
): Promise<I18n> {
|
): Promise<I18n> {
|
||||||
const {i18n: i18nConfig} = config;
|
const {i18n: i18nConfig} = config;
|
||||||
|
|
||||||
|
@ -88,9 +88,5 @@ export function localizePath({
|
||||||
return path.join(originalPath, i18n.currentLocale);
|
return path.join(originalPath, i18n.currentLocale);
|
||||||
}
|
}
|
||||||
// Url paths; add a trailing slash so it's a valid base URL
|
// Url paths; add a trailing slash so it's a valid base URL
|
||||||
if (pathType === 'url') {
|
|
||||||
return normalizeUrl([originalPath, i18n.currentLocale, '/']);
|
return normalizeUrl([originalPath, i18n.currentLocale, '/']);
|
||||||
}
|
}
|
||||||
// should never happen
|
|
||||||
throw new Error(`Unhandled path type "${pathType}".`);
|
|
||||||
}
|
|
||||||
|
|
|
@ -57,12 +57,10 @@ export async function loadSiteConfig({
|
||||||
siteDir: string;
|
siteDir: string;
|
||||||
customConfigFilePath?: string;
|
customConfigFilePath?: string;
|
||||||
}): Promise<{siteConfig: DocusaurusConfig; siteConfigPath: string}> {
|
}): Promise<{siteConfig: DocusaurusConfig; siteConfigPath: string}> {
|
||||||
const siteConfigPathUnresolved =
|
const siteConfigPath = path.resolve(
|
||||||
customConfigFilePath ?? DEFAULT_CONFIG_FILE_NAME;
|
siteDir,
|
||||||
|
customConfigFilePath ?? DEFAULT_CONFIG_FILE_NAME,
|
||||||
const siteConfigPath = path.isAbsolute(siteConfigPathUnresolved)
|
);
|
||||||
? siteConfigPathUnresolved
|
|
||||||
: path.resolve(siteDir, siteConfigPathUnresolved);
|
|
||||||
|
|
||||||
const siteConfig = await loadConfig(siteConfigPath);
|
const siteConfig = await loadConfig(siteConfigPath);
|
||||||
return {siteConfig, siteConfigPath};
|
return {siteConfig, siteConfigPath};
|
||||||
|
@ -73,9 +71,7 @@ export async function loadContext(
|
||||||
options: LoadContextOptions = {},
|
options: LoadContextOptions = {},
|
||||||
): Promise<LoadContext> {
|
): Promise<LoadContext> {
|
||||||
const {customOutDir, locale, customConfigFilePath} = options;
|
const {customOutDir, locale, customConfigFilePath} = options;
|
||||||
const generatedFilesDir = path.isAbsolute(GENERATED_FILES_DIR_NAME)
|
const generatedFilesDir = path.resolve(siteDir, GENERATED_FILES_DIR_NAME);
|
||||||
? GENERATED_FILES_DIR_NAME
|
|
||||||
: path.resolve(siteDir, GENERATED_FILES_DIR_NAME);
|
|
||||||
|
|
||||||
const {siteConfig: initialSiteConfig, siteConfigPath} = await loadSiteConfig({
|
const {siteConfig: initialSiteConfig, siteConfigPath} = await loadSiteConfig({
|
||||||
siteDir,
|
siteDir,
|
||||||
|
@ -83,9 +79,10 @@ export async function loadContext(
|
||||||
});
|
});
|
||||||
const {ssrTemplate} = initialSiteConfig;
|
const {ssrTemplate} = initialSiteConfig;
|
||||||
|
|
||||||
const baseOutDir = customOutDir
|
const baseOutDir = path.resolve(
|
||||||
? path.resolve(customOutDir)
|
siteDir,
|
||||||
: path.resolve(siteDir, DEFAULT_BUILD_DIR_NAME);
|
customOutDir ?? DEFAULT_BUILD_DIR_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
const i18n = await loadI18n(initialSiteConfig, {locale});
|
const i18n = await loadI18n(initialSiteConfig, {locale});
|
||||||
|
|
||||||
|
@ -191,7 +188,9 @@ function createBootstrapPlugin({
|
||||||
return {
|
return {
|
||||||
name: 'docusaurus-bootstrap-plugin',
|
name: 'docusaurus-bootstrap-plugin',
|
||||||
content: null,
|
content: null,
|
||||||
options: {},
|
options: {
|
||||||
|
id: 'default',
|
||||||
|
},
|
||||||
version: {type: 'synthetic'},
|
version: {type: 'synthetic'},
|
||||||
getClientModules() {
|
getClientModules() {
|
||||||
return siteConfigClientModules;
|
return siteConfigClientModules;
|
||||||
|
@ -241,7 +240,9 @@ function createMDXFallbackPlugin({
|
||||||
return {
|
return {
|
||||||
name: 'docusaurus-mdx-fallback-plugin',
|
name: 'docusaurus-mdx-fallback-plugin',
|
||||||
content: null,
|
content: null,
|
||||||
options: {},
|
options: {
|
||||||
|
id: 'default',
|
||||||
|
},
|
||||||
version: {type: 'synthetic'},
|
version: {type: 'synthetic'},
|
||||||
configureWebpack(config, isServer, {getJSLoader}) {
|
configureWebpack(config, isServer, {getJSLoader}) {
|
||||||
// We need the mdx fallback loader to exclude files that were already
|
// We need the mdx fallback loader to exclude files that were already
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`loadPlugins loads plugins 1`] = `
|
||||||
|
{
|
||||||
|
"globalData": {
|
||||||
|
"test1": {
|
||||||
|
"default": {
|
||||||
|
"content": "a",
|
||||||
|
"prop": "a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"content": "a",
|
||||||
|
"contentLoaded": [Function],
|
||||||
|
"loadContent": [Function],
|
||||||
|
"name": "test1",
|
||||||
|
"options": {
|
||||||
|
"id": "default",
|
||||||
|
},
|
||||||
|
"prop": "a",
|
||||||
|
"version": {
|
||||||
|
"type": "local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"configureWebpack": [Function],
|
||||||
|
"content": undefined,
|
||||||
|
"name": "test2",
|
||||||
|
"options": {
|
||||||
|
"id": "default",
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"pluginsRouteConfigs": [],
|
||||||
|
"themeConfigTranslated": {},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`sortConfig sorts route config correctly 1`] = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/community",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/some-page",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/docs",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/docs/someDoc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/docs/someOtherDoc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/someDoc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/someOtherDoc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/subroute",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`sortConfig sorts route config given a baseURL 1`] = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/latest/community",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/latest/example",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/latest/some-page",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/latest/docs",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/latest/docs/someDoc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/latest/docs/someOtherDoc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/latest/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/latest/",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/latest/someDoc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "",
|
||||||
|
"path": "/latest/someOtherDoc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
|
@ -27,103 +27,3 @@ Example valid plugin config:
|
||||||
|
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`sortConfig sorts route config correctly 1`] = `
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/community",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/some-page",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/docs",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/docs/someDoc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/docs/someOtherDoc",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/someDoc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/someOtherDoc",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/subroute",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`sortConfig sorts route config given a baseURL 1`] = `
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/latest/community",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/latest/example",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/latest/some-page",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/latest/docs",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/latest/docs/someDoc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/latest/docs/someOtherDoc",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/latest",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/latest/someDoc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "",
|
|
||||||
"path": "/latest/someOtherDoc",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
139
packages/docusaurus/src/server/plugins/__tests__/index.test.ts
Normal file
139
packages/docusaurus/src/server/plugins/__tests__/index.test.ts
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
/**
|
||||||
|
* 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 {loadPlugins, sortConfig} from '..';
|
||||||
|
import type {RouteConfig} from '@docusaurus/types';
|
||||||
|
|
||||||
|
describe('loadPlugins', () => {
|
||||||
|
it('loads plugins', async () => {
|
||||||
|
const siteDir = path.join(__dirname, '__fixtures__/site-with-plugin');
|
||||||
|
await expect(
|
||||||
|
loadPlugins({
|
||||||
|
pluginConfigs: [
|
||||||
|
() => ({
|
||||||
|
name: 'test1',
|
||||||
|
prop: 'a',
|
||||||
|
async loadContent() {
|
||||||
|
// Testing that plugin lifecycle is bound to the plugin instance
|
||||||
|
return this.prop;
|
||||||
|
},
|
||||||
|
async contentLoaded({content, actions}) {
|
||||||
|
actions.setGlobalData({content, prop: this.prop});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
() => ({
|
||||||
|
name: 'test2',
|
||||||
|
configureWebpack() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
context: {
|
||||||
|
siteDir,
|
||||||
|
generatedFilesDir: path.join(siteDir, '.docusaurus'),
|
||||||
|
outDir: path.join(siteDir, 'build'),
|
||||||
|
// @ts-expect-error: good enough
|
||||||
|
siteConfig: {
|
||||||
|
baseUrl: '/',
|
||||||
|
trailingSlash: true,
|
||||||
|
themeConfig: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
siteConfigPath: path.join(siteDir, 'docusaurus.config.js'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sortConfig', () => {
|
||||||
|
it('sorts route config correctly', () => {
|
||||||
|
const routes: RouteConfig[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: '',
|
||||||
|
routes: [
|
||||||
|
{path: '/someDoc', component: ''},
|
||||||
|
{path: '/someOtherDoc', component: ''},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: '',
|
||||||
|
routes: [{path: '/subroute', component: ''}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/docs',
|
||||||
|
component: '',
|
||||||
|
routes: [
|
||||||
|
{path: '/docs/someDoc', component: ''},
|
||||||
|
{path: '/docs/someOtherDoc', component: ''},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/community',
|
||||||
|
component: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/some-page',
|
||||||
|
component: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
sortConfig(routes);
|
||||||
|
|
||||||
|
expect(routes).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts route config given a baseURL', () => {
|
||||||
|
const baseURL = '/latest/';
|
||||||
|
const routes: RouteConfig[] = [
|
||||||
|
{
|
||||||
|
path: baseURL,
|
||||||
|
component: '',
|
||||||
|
routes: [
|
||||||
|
{path: `${baseURL}someDoc`, component: ''},
|
||||||
|
{path: `${baseURL}someOtherDoc`, component: ''},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${baseURL}example`,
|
||||||
|
component: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${baseURL}docs`,
|
||||||
|
component: '',
|
||||||
|
routes: [
|
||||||
|
{path: `${baseURL}docs/someDoc`, component: ''},
|
||||||
|
{path: `${baseURL}docs/someOtherDoc`, component: ''},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${baseURL}community`,
|
||||||
|
component: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${baseURL}some-page`,
|
||||||
|
component: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${baseURL}`,
|
||||||
|
component: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
sortConfig(routes, baseURL);
|
||||||
|
|
||||||
|
expect(routes).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -13,8 +13,6 @@ import {
|
||||||
type LoadContextOptions,
|
type LoadContextOptions,
|
||||||
} from '../../index';
|
} from '../../index';
|
||||||
import initPlugins from '../init';
|
import initPlugins from '../init';
|
||||||
import {sortConfig} from '../index';
|
|
||||||
import type {RouteConfig} from '@docusaurus/types';
|
|
||||||
|
|
||||||
describe('initPlugins', () => {
|
describe('initPlugins', () => {
|
||||||
async function loadSite(options: LoadContextOptions = {}) {
|
async function loadSite(options: LoadContextOptions = {}) {
|
||||||
|
@ -53,85 +51,3 @@ describe('initPlugins', () => {
|
||||||
).rejects.toThrowErrorMatchingSnapshot();
|
).rejects.toThrowErrorMatchingSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sortConfig', () => {
|
|
||||||
it('sorts route config correctly', () => {
|
|
||||||
const routes: RouteConfig[] = [
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
component: '',
|
|
||||||
routes: [
|
|
||||||
{path: '/someDoc', component: ''},
|
|
||||||
{path: '/someOtherDoc', component: ''},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
component: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
component: '',
|
|
||||||
routes: [{path: '/subroute', component: ''}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/docs',
|
|
||||||
component: '',
|
|
||||||
routes: [
|
|
||||||
{path: '/docs/someDoc', component: ''},
|
|
||||||
{path: '/docs/someOtherDoc', component: ''},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/community',
|
|
||||||
component: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/some-page',
|
|
||||||
component: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
sortConfig(routes);
|
|
||||||
|
|
||||||
expect(routes).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sorts route config given a baseURL', () => {
|
|
||||||
const baseURL = '/latest';
|
|
||||||
const routes: RouteConfig[] = [
|
|
||||||
{
|
|
||||||
path: baseURL,
|
|
||||||
component: '',
|
|
||||||
routes: [
|
|
||||||
{path: `${baseURL}/someDoc`, component: ''},
|
|
||||||
{path: `${baseURL}/someOtherDoc`, component: ''},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `${baseURL}/example`,
|
|
||||||
component: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `${baseURL}/docs`,
|
|
||||||
component: '',
|
|
||||||
routes: [
|
|
||||||
{path: `${baseURL}/docs/someDoc`, component: ''},
|
|
||||||
{path: `${baseURL}/docs/someOtherDoc`, component: ''},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `${baseURL}/community`,
|
|
||||||
component: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `${baseURL}/some-page`,
|
|
||||||
component: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
sortConfig(routes, baseURL);
|
|
||||||
|
|
||||||
expect(routes).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {generate, DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
import {generate} from '@docusaurus/utils';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type {
|
import type {
|
||||||
|
@ -125,7 +125,7 @@ export async function loadPlugins({
|
||||||
.groupBy((item) => item.name)
|
.groupBy((item) => item.name)
|
||||||
.mapValues((nameItems) =>
|
.mapValues((nameItems) =>
|
||||||
_.chain(nameItems)
|
_.chain(nameItems)
|
||||||
.groupBy((item) => item.options.id ?? DEFAULT_PLUGIN_ID)
|
.groupBy((item) => item.options.id)
|
||||||
.mapValues((idItems) => idItems[0]!.content)
|
.mapValues((idItems) => idItems[0]!.content)
|
||||||
.value(),
|
.value(),
|
||||||
)
|
)
|
||||||
|
@ -143,7 +143,7 @@ export async function loadPlugins({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginId = plugin.options.id ?? DEFAULT_PLUGIN_ID;
|
const pluginId = plugin.options.id;
|
||||||
|
|
||||||
// plugins data files are namespaced by pluginName/pluginId
|
// plugins data files are namespaced by pluginName/pluginId
|
||||||
const dataDirRoot = path.join(context.generatedFilesDir, plugin.name);
|
const dataDirRoot = path.join(context.generatedFilesDir, plugin.name);
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
|
|
||||||
module.exports = function preset(context, opts = {}) {
|
module.exports = function preset(context, opts = {}) {
|
||||||
return {
|
return {
|
||||||
plugins: [
|
themes: [
|
||||||
['@docusaurus/plugin-content-pages', opts.pages],
|
['@docusaurus/theme-live-codeblock', opts.codeblock],
|
||||||
['@docusaurus/plugin-sitemap', opts.sitemap],
|
['@docusaurus/theme-algolia', opts.algolia],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -29,18 +29,19 @@ exports[`loadPresets array form composite 1`] = `
|
||||||
"@docusaurus/plugin-content-blog",
|
"@docusaurus/plugin-content-blog",
|
||||||
undefined,
|
undefined,
|
||||||
],
|
],
|
||||||
[
|
|
||||||
"@docusaurus/plugin-content-pages",
|
|
||||||
{
|
|
||||||
"path": "../",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
"themes": [
|
||||||
[
|
[
|
||||||
"@docusaurus/plugin-sitemap",
|
"@docusaurus/theme-live-codeblock",
|
||||||
undefined,
|
undefined,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"@docusaurus/theme-algolia",
|
||||||
|
{
|
||||||
|
"trackingID": "foo",
|
||||||
|
},
|
||||||
|
],
|
||||||
],
|
],
|
||||||
"themes": [],
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -75,16 +76,17 @@ exports[`loadPresets mixed form 1`] = `
|
||||||
"@docusaurus/plugin-content-blog",
|
"@docusaurus/plugin-content-blog",
|
||||||
undefined,
|
undefined,
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
"themes": [
|
||||||
[
|
[
|
||||||
"@docusaurus/plugin-content-pages",
|
"@docusaurus/theme-live-codeblock",
|
||||||
undefined,
|
undefined,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"@docusaurus/plugin-sitemap",
|
"@docusaurus/theme-algolia",
|
||||||
undefined,
|
undefined,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"themes": [],
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -101,20 +103,20 @@ exports[`loadPresets mixed form with themes 1`] = `
|
||||||
"@docusaurus/plugin-content-blog",
|
"@docusaurus/plugin-content-blog",
|
||||||
undefined,
|
undefined,
|
||||||
],
|
],
|
||||||
[
|
|
||||||
"@docusaurus/plugin-content-pages",
|
|
||||||
undefined,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@docusaurus/plugin-sitemap",
|
|
||||||
undefined,
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"@docusaurus/plugin-test",
|
"@docusaurus/plugin-test",
|
||||||
undefined,
|
undefined,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"themes": [
|
"themes": [
|
||||||
|
[
|
||||||
|
"@docusaurus/theme-live-codeblock",
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@docusaurus/theme-algolia",
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"@docusaurus/theme-classic",
|
"@docusaurus/theme-classic",
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -150,15 +152,16 @@ exports[`loadPresets string form composite 1`] = `
|
||||||
"@docusaurus/plugin-content-blog",
|
"@docusaurus/plugin-content-blog",
|
||||||
undefined,
|
undefined,
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
"themes": [
|
||||||
[
|
[
|
||||||
"@docusaurus/plugin-content-pages",
|
"@docusaurus/theme-live-codeblock",
|
||||||
undefined,
|
undefined,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"@docusaurus/plugin-sitemap",
|
"@docusaurus/theme-algolia",
|
||||||
undefined,
|
undefined,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"themes": [],
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -31,7 +31,7 @@ describe('loadPresets', () => {
|
||||||
const context = {
|
const context = {
|
||||||
siteConfigPath: __dirname,
|
siteConfigPath: __dirname,
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
presets: [path.join(__dirname, '__fixtures__/preset-bar.js')],
|
presets: [path.join(__dirname, '__fixtures__/preset-plugins.js')],
|
||||||
},
|
},
|
||||||
} as LoadContext;
|
} as LoadContext;
|
||||||
const presets = await loadPresets(context);
|
const presets = await loadPresets(context);
|
||||||
|
@ -43,8 +43,8 @@ describe('loadPresets', () => {
|
||||||
siteConfigPath: __dirname,
|
siteConfigPath: __dirname,
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
presets: [
|
presets: [
|
||||||
path.join(__dirname, '__fixtures__/preset-bar.js'),
|
path.join(__dirname, '__fixtures__/preset-plugins.js'),
|
||||||
path.join(__dirname, '__fixtures__/preset-foo.js'),
|
path.join(__dirname, '__fixtures__/preset-themes.js'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
} as LoadContext;
|
} as LoadContext;
|
||||||
|
@ -56,7 +56,7 @@ describe('loadPresets', () => {
|
||||||
const context = {
|
const context = {
|
||||||
siteConfigPath: __dirname,
|
siteConfigPath: __dirname,
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
presets: [[path.join(__dirname, '__fixtures__/preset-bar.js')]],
|
presets: [[path.join(__dirname, '__fixtures__/preset-plugins.js')]],
|
||||||
},
|
},
|
||||||
} as Partial<LoadContext>;
|
} as Partial<LoadContext>;
|
||||||
const presets = await loadPresets(context);
|
const presets = await loadPresets(context);
|
||||||
|
@ -69,7 +69,7 @@ describe('loadPresets', () => {
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
presets: [
|
presets: [
|
||||||
[
|
[
|
||||||
path.join(__dirname, '__fixtures__/preset-bar.js'),
|
path.join(__dirname, '__fixtures__/preset-plugins.js'),
|
||||||
{docs: {path: '../'}},
|
{docs: {path: '../'}},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -85,12 +85,12 @@ describe('loadPresets', () => {
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
presets: [
|
presets: [
|
||||||
[
|
[
|
||||||
path.join(__dirname, '__fixtures__/preset-bar.js'),
|
path.join(__dirname, '__fixtures__/preset-plugins.js'),
|
||||||
{docs: {path: '../'}},
|
{docs: {path: '../'}},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
path.join(__dirname, '__fixtures__/preset-foo.js'),
|
path.join(__dirname, '__fixtures__/preset-themes.js'),
|
||||||
{pages: {path: '../'}},
|
{algolia: {trackingID: 'foo'}},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -105,10 +105,10 @@ describe('loadPresets', () => {
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
presets: [
|
presets: [
|
||||||
[
|
[
|
||||||
path.join(__dirname, '__fixtures__/preset-bar.js'),
|
path.join(__dirname, '__fixtures__/preset-plugins.js'),
|
||||||
{docs: {path: '../'}},
|
{docs: {path: '../'}},
|
||||||
],
|
],
|
||||||
path.join(__dirname, '__fixtures__/preset-foo.js'),
|
path.join(__dirname, '__fixtures__/preset-themes.js'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
} as LoadContext;
|
} as LoadContext;
|
||||||
|
@ -122,11 +122,11 @@ describe('loadPresets', () => {
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
presets: [
|
presets: [
|
||||||
[
|
[
|
||||||
path.join(__dirname, '__fixtures__/preset-bar.js'),
|
path.join(__dirname, '__fixtures__/preset-plugins.js'),
|
||||||
{docs: {path: '../'}},
|
{docs: {path: '../'}},
|
||||||
],
|
],
|
||||||
path.join(__dirname, '__fixtures__/preset-foo.js'),
|
path.join(__dirname, '__fixtures__/preset-themes.js'),
|
||||||
path.join(__dirname, '__fixtures__/preset-qux.js'),
|
path.join(__dirname, '__fixtures__/preset-mixed.js'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
} as LoadContext;
|
} as LoadContext;
|
||||||
|
|
|
@ -30,7 +30,7 @@ function indent(str: string) {
|
||||||
return `${spaces}${str.replace(/\n/g, `\n${spaces}`)}`;
|
return `${spaces}${str.replace(/\n/g, `\n${spaces}`)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createRouteCodeString = ({
|
function createRouteCodeString({
|
||||||
routePath,
|
routePath,
|
||||||
routeHash,
|
routeHash,
|
||||||
exact,
|
exact,
|
||||||
|
@ -42,7 +42,7 @@ const createRouteCodeString = ({
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
subroutesCodeStrings?: string[];
|
subroutesCodeStrings?: string[];
|
||||||
props: {[propName: string]: unknown};
|
props: {[propName: string]: unknown};
|
||||||
}) => {
|
}) {
|
||||||
const parts = [
|
const parts = [
|
||||||
`path: '${routePath}'`,
|
`path: '${routePath}'`,
|
||||||
`component: ComponentCreator('${routePath}','${routeHash}')`,
|
`component: ComponentCreator('${routePath}','${routeHash}')`,
|
||||||
|
@ -61,17 +61,30 @@ ${indent(removeSuffix(subroutesCodeStrings.join(',\n'), ',\n'))}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(props).forEach(([propName, propValue]) => {
|
Object.entries(props).forEach(([propName, propValue]) => {
|
||||||
// Figure out how to "unquote" JS attributes that don't need to be quoted
|
// Inspired by https://github.com/armanozak/should-quote/blob/main/packages/should-quote/src/lib/should-quote.ts
|
||||||
// Is this lib reliable? https://github.com/armanozak/should-quote
|
const shouldQuote = ((key: string) => {
|
||||||
const shouldQuote = true; // TODO
|
// Pre-sanitation to prevent injection
|
||||||
const key = shouldQuote ? `'${propName}'` : propName;
|
if (/[.,;:}/\s]/.test(key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// If this key can be used in an expression like ({a:0}).a
|
||||||
|
// eslint-disable-next-line no-eval
|
||||||
|
eval(`({${key}:0}).${key}`);
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})(propName);
|
||||||
|
// Escape quotes as well
|
||||||
|
const key = shouldQuote ? JSON.stringify(propName) : propName;
|
||||||
parts.push(`${key}: ${JSON.stringify(propValue)}`);
|
parts.push(`${key}: ${JSON.stringify(propValue)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
return `{
|
return `{
|
||||||
${indent(parts.join(',\n'))}
|
${indent(parts.join(',\n'))}
|
||||||
}`;
|
}`;
|
||||||
};
|
}
|
||||||
|
|
||||||
const NotFoundRouteCode = `{
|
const NotFoundRouteCode = `{
|
||||||
path: '*',
|
path: '*',
|
||||||
|
|
|
@ -7,7 +7,51 @@
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import themeAlias from '../alias';
|
import themeAlias, {sortAliases} from '../alias';
|
||||||
|
|
||||||
|
describe('sortAliases', () => {
|
||||||
|
// https://github.com/facebook/docusaurus/issues/6878
|
||||||
|
// Not sure if the risk actually happens, but still made tests to ensure that
|
||||||
|
// behavior is consistent
|
||||||
|
it('sorts reliably', () => {
|
||||||
|
expect(
|
||||||
|
Object.values(
|
||||||
|
sortAliases({
|
||||||
|
'@a/b': 'b',
|
||||||
|
'@a/b/c': 'c',
|
||||||
|
'@a/b/c/d': 'd',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toEqual(['d', 'c', 'b']);
|
||||||
|
expect(
|
||||||
|
Object.values(
|
||||||
|
sortAliases({
|
||||||
|
'@a/b': 'b',
|
||||||
|
'@a/b/c/d': 'd',
|
||||||
|
'@a/b/c': 'c',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toEqual(['d', 'c', 'b']);
|
||||||
|
expect(
|
||||||
|
Object.values(
|
||||||
|
sortAliases({
|
||||||
|
'@a/b/c/d': 'd',
|
||||||
|
'@a/b/c': 'c',
|
||||||
|
'@a/b': 'b',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toEqual(['d', 'c', 'b']);
|
||||||
|
expect(
|
||||||
|
Object.values(
|
||||||
|
sortAliases({
|
||||||
|
'@a/b/c': 'c',
|
||||||
|
'@a/b': 'b',
|
||||||
|
'@a/b/c/d': 'd',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toEqual(['d', 'c', 'b']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('themeAlias', () => {
|
describe('themeAlias', () => {
|
||||||
it('valid themePath 1 with components', async () => {
|
it('valid themePath 1 with components', async () => {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import path from 'path';
|
||||||
import {THEME_PATH} from '@docusaurus/utils';
|
import {THEME_PATH} from '@docusaurus/utils';
|
||||||
import themeAlias, {sortAliases} from './alias';
|
import themeAlias, {sortAliases} from './alias';
|
||||||
|
|
||||||
const ThemeFallbackDir = path.resolve(__dirname, '../../client/theme-fallback');
|
const ThemeFallbackDir = path.join(__dirname, '../../client/theme-fallback');
|
||||||
|
|
||||||
export async function loadThemeAliases(
|
export async function loadThemeAliases(
|
||||||
themePaths: string[],
|
themePaths: string[],
|
||||||
|
|
|
@ -7,10 +7,9 @@
|
||||||
|
|
||||||
import {jest} from '@jest/globals';
|
import {jest} from '@jest/globals';
|
||||||
import {
|
import {
|
||||||
ensureTranslationFileContent,
|
|
||||||
writeTranslationFileContent,
|
|
||||||
writePluginTranslations,
|
writePluginTranslations,
|
||||||
readTranslationFileContent,
|
writeCodeTranslations,
|
||||||
|
readCodeTranslationFileContent,
|
||||||
type WriteTranslationsOptions,
|
type WriteTranslationsOptions,
|
||||||
localizePluginTranslationFile,
|
localizePluginTranslationFile,
|
||||||
getPluginsDefaultCodeTranslationMessages,
|
getPluginsDefaultCodeTranslationMessages,
|
||||||
|
@ -35,129 +34,93 @@ async function createTmpSiteDir() {
|
||||||
async function createTmpTranslationFile(
|
async function createTmpTranslationFile(
|
||||||
content: TranslationFileContent | null,
|
content: TranslationFileContent | null,
|
||||||
) {
|
) {
|
||||||
const filePath = await tmp.tmpName({
|
const siteDir = await createTmpSiteDir();
|
||||||
prefix: 'jest-createTmpTranslationFile',
|
const filePath = path.join(siteDir, 'i18n/en/code.json');
|
||||||
postfix: '.json',
|
|
||||||
});
|
|
||||||
|
|
||||||
// null means we don't want a file, just a filename
|
// null means we don't want a file, just a filename
|
||||||
if (content !== null) {
|
if (content !== null) {
|
||||||
await fs.writeFile(filePath, JSON.stringify(content, null, 2));
|
await fs.outputFile(filePath, JSON.stringify(content, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filePath,
|
siteDir,
|
||||||
readFile: async () => JSON.parse(await fs.readFile(filePath, 'utf8')),
|
readFile() {
|
||||||
|
return fs.readJSON(filePath);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ensureTranslationFileContent', () => {
|
describe('writeCodeTranslations', () => {
|
||||||
it('passes valid translation file content', () => {
|
const consoleInfoMock = jest
|
||||||
ensureTranslationFileContent({});
|
.spyOn(console, 'info')
|
||||||
ensureTranslationFileContent({key1: {message: ''}});
|
.mockImplementation(() => {});
|
||||||
ensureTranslationFileContent({key1: {message: 'abc'}});
|
beforeEach(() => {
|
||||||
ensureTranslationFileContent({key1: {message: 'abc', description: 'desc'}});
|
consoleInfoMock.mockClear();
|
||||||
ensureTranslationFileContent({
|
|
||||||
key1: {message: 'abc', description: 'desc'},
|
|
||||||
key2: {message: 'def', description: 'desc'},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails for invalid translation file content', () => {
|
|
||||||
expect(() =>
|
|
||||||
ensureTranslationFileContent(null),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"\\"value\\" must be of type object"`,
|
|
||||||
);
|
|
||||||
expect(() =>
|
|
||||||
ensureTranslationFileContent(undefined),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(`"\\"value\\" is required"`);
|
|
||||||
expect(() =>
|
|
||||||
ensureTranslationFileContent('HEY'),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"\\"value\\" must be of type object"`,
|
|
||||||
);
|
|
||||||
expect(() =>
|
|
||||||
ensureTranslationFileContent(42),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"\\"value\\" must be of type object"`,
|
|
||||||
);
|
|
||||||
expect(() =>
|
|
||||||
ensureTranslationFileContent({key: {description: 'no message'}}),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(`"\\"key.message\\" is required"`);
|
|
||||||
expect(() =>
|
|
||||||
ensureTranslationFileContent({key: {message: 42}}),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"\\"key.message\\" must be a string"`,
|
|
||||||
);
|
|
||||||
expect(() =>
|
|
||||||
ensureTranslationFileContent({
|
|
||||||
key: {message: 'Message', description: 42},
|
|
||||||
}),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"\\"key.description\\" must be a string"`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('writeTranslationFileContent', () => {
|
|
||||||
it('creates new translation file', async () => {
|
it('creates new translation file', async () => {
|
||||||
const {filePath, readFile} = await createTmpTranslationFile(null);
|
const {siteDir, readFile} = await createTmpTranslationFile(null);
|
||||||
|
await writeCodeTranslations(
|
||||||
await writeTranslationFileContent({
|
{siteDir, locale: 'en'},
|
||||||
filePath,
|
{
|
||||||
content: {
|
|
||||||
key1: {message: 'key1 message'},
|
key1: {message: 'key1 message'},
|
||||||
key2: {message: 'key2 message'},
|
key2: {message: 'key2 message'},
|
||||||
key3: {message: 'key3 message'},
|
key3: {message: 'key3 message'},
|
||||||
},
|
},
|
||||||
});
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
await expect(readFile()).resolves.toEqual({
|
await expect(readFile()).resolves.toEqual({
|
||||||
key1: {message: 'key1 message'},
|
key1: {message: 'key1 message'},
|
||||||
key2: {message: 'key2 message'},
|
key2: {message: 'key2 message'},
|
||||||
key3: {message: 'key3 message'},
|
key3: {message: 'key3 message'},
|
||||||
});
|
});
|
||||||
|
expect(consoleInfoMock).toBeCalledWith(
|
||||||
|
expect.stringMatching(/3.* translations will be written/),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates new translation file with prefix', async () => {
|
it('creates new translation file with prefix', async () => {
|
||||||
const {filePath, readFile} = await createTmpTranslationFile(null);
|
const {siteDir, readFile} = await createTmpTranslationFile(null);
|
||||||
|
await writeCodeTranslations(
|
||||||
await writeTranslationFileContent({
|
{siteDir, locale: 'en'},
|
||||||
filePath,
|
{
|
||||||
content: {
|
|
||||||
key1: {message: 'key1 message'},
|
key1: {message: 'key1 message'},
|
||||||
key2: {message: 'key2 message'},
|
key2: {message: 'key2 message'},
|
||||||
key3: {message: 'key3 message'},
|
key3: {message: 'key3 message'},
|
||||||
},
|
},
|
||||||
options: {
|
{
|
||||||
messagePrefix: 'PREFIX ',
|
messagePrefix: 'PREFIX ',
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
await expect(readFile()).resolves.toEqual({
|
await expect(readFile()).resolves.toEqual({
|
||||||
key1: {message: 'PREFIX key1 message'},
|
key1: {message: 'PREFIX key1 message'},
|
||||||
key2: {message: 'PREFIX key2 message'},
|
key2: {message: 'PREFIX key2 message'},
|
||||||
key3: {message: 'PREFIX key3 message'},
|
key3: {message: 'PREFIX key3 message'},
|
||||||
});
|
});
|
||||||
|
expect(consoleInfoMock).toBeCalledWith(
|
||||||
|
expect.stringMatching(/3.* translations will be written/),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('appends missing translations', async () => {
|
it('appends missing translations', async () => {
|
||||||
const {filePath, readFile} = await createTmpTranslationFile({
|
const {siteDir, readFile} = await createTmpTranslationFile({
|
||||||
key1: {message: 'key1 message'},
|
key1: {message: 'key1 message'},
|
||||||
key2: {message: 'key2 message'},
|
key2: {message: 'key2 message'},
|
||||||
key3: {message: 'key3 message'},
|
key3: {message: 'key3 message'},
|
||||||
});
|
});
|
||||||
|
|
||||||
await writeTranslationFileContent({
|
await writeCodeTranslations(
|
||||||
filePath,
|
{siteDir, locale: 'en'},
|
||||||
content: {
|
{
|
||||||
key1: {message: 'key1 message new'},
|
key1: {message: 'key1 message new'},
|
||||||
key2: {message: 'key2 message new'},
|
key2: {message: 'key2 message new'},
|
||||||
key3: {message: 'key3 message new'},
|
key3: {message: 'key3 message new'},
|
||||||
key4: {message: 'key4 message new'},
|
key4: {message: 'key4 message new'},
|
||||||
},
|
},
|
||||||
});
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
await expect(readFile()).resolves.toEqual({
|
await expect(readFile()).resolves.toEqual({
|
||||||
key1: {message: 'key1 message'},
|
key1: {message: 'key1 message'},
|
||||||
|
@ -165,111 +128,139 @@ describe('writeTranslationFileContent', () => {
|
||||||
key3: {message: 'key3 message'},
|
key3: {message: 'key3 message'},
|
||||||
key4: {message: 'key4 message new'},
|
key4: {message: 'key4 message new'},
|
||||||
});
|
});
|
||||||
|
expect(consoleInfoMock).toBeCalledWith(
|
||||||
|
expect.stringMatching(/4.* translations will be written/),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('appends missing translations with prefix', async () => {
|
it('appends missing.* translations with prefix', async () => {
|
||||||
const {filePath, readFile} = await createTmpTranslationFile({
|
const {siteDir, readFile} = await createTmpTranslationFile({
|
||||||
key1: {message: 'key1 message'},
|
key1: {message: 'key1 message'},
|
||||||
});
|
});
|
||||||
|
|
||||||
await writeTranslationFileContent({
|
await writeCodeTranslations(
|
||||||
filePath,
|
{siteDir, locale: 'en'},
|
||||||
content: {
|
{
|
||||||
key1: {message: 'key1 message new'},
|
key1: {message: 'key1 message new'},
|
||||||
key2: {message: 'key2 message new'},
|
key2: {message: 'key2 message new'},
|
||||||
},
|
},
|
||||||
options: {
|
{
|
||||||
messagePrefix: 'PREFIX ',
|
messagePrefix: 'PREFIX ',
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
await expect(readFile()).resolves.toEqual({
|
await expect(readFile()).resolves.toEqual({
|
||||||
key1: {message: 'key1 message'},
|
key1: {message: 'key1 message'},
|
||||||
key2: {message: 'PREFIX key2 message new'},
|
key2: {message: 'PREFIX key2 message new'},
|
||||||
});
|
});
|
||||||
|
expect(consoleInfoMock).toBeCalledWith(
|
||||||
|
expect.stringMatching(/2.* translations will be written/),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('overrides missing translations', async () => {
|
it('overrides missing translations', async () => {
|
||||||
const {filePath, readFile} = await createTmpTranslationFile({
|
const {siteDir, readFile} = await createTmpTranslationFile({
|
||||||
key1: {message: 'key1 message'},
|
key1: {message: 'key1 message'},
|
||||||
});
|
});
|
||||||
|
|
||||||
await writeTranslationFileContent({
|
await writeCodeTranslations(
|
||||||
filePath,
|
{siteDir, locale: 'en'},
|
||||||
content: {
|
{
|
||||||
key1: {message: 'key1 message new'},
|
key1: {message: 'key1 message new'},
|
||||||
key2: {message: 'key2 message new'},
|
key2: {message: 'key2 message new'},
|
||||||
},
|
},
|
||||||
options: {
|
{
|
||||||
override: true,
|
override: true,
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
await expect(readFile()).resolves.toEqual({
|
await expect(readFile()).resolves.toEqual({
|
||||||
key1: {message: 'key1 message new'},
|
key1: {message: 'key1 message new'},
|
||||||
key2: {message: 'key2 message new'},
|
key2: {message: 'key2 message new'},
|
||||||
});
|
});
|
||||||
|
expect(consoleInfoMock).toBeCalledWith(
|
||||||
|
expect.stringMatching(/2.* translations will be written/),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('overrides missing translations with prefix', async () => {
|
it('overrides missing translations with prefix', async () => {
|
||||||
const {filePath, readFile} = await createTmpTranslationFile({
|
const {siteDir, readFile} = await createTmpTranslationFile({
|
||||||
key1: {message: 'key1 message'},
|
key1: {message: 'key1 message'},
|
||||||
});
|
});
|
||||||
|
|
||||||
await writeTranslationFileContent({
|
await writeCodeTranslations(
|
||||||
filePath,
|
{siteDir, locale: 'en'},
|
||||||
content: {
|
{
|
||||||
key1: {message: 'key1 message new'},
|
key1: {message: 'key1 message new'},
|
||||||
key2: {message: 'key2 message new'},
|
key2: {message: 'key2 message new'},
|
||||||
},
|
},
|
||||||
options: {
|
{
|
||||||
override: true,
|
override: true,
|
||||||
messagePrefix: 'PREFIX ',
|
messagePrefix: 'PREFIX ',
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
await expect(readFile()).resolves.toEqual({
|
await expect(readFile()).resolves.toEqual({
|
||||||
key1: {message: 'PREFIX key1 message new'},
|
key1: {message: 'PREFIX key1 message new'},
|
||||||
key2: {message: 'PREFIX key2 message new'},
|
key2: {message: 'PREFIX key2 message new'},
|
||||||
});
|
});
|
||||||
|
expect(consoleInfoMock).toBeCalledWith(
|
||||||
|
expect.stringMatching(/2.* translations will be written/),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('always overrides message description', async () => {
|
it('always overrides message description', async () => {
|
||||||
const {filePath, readFile} = await createTmpTranslationFile({
|
const {siteDir, readFile} = await createTmpTranslationFile({
|
||||||
key1: {message: 'key1 message', description: 'key1 desc'},
|
key1: {message: 'key1 message', description: 'key1 desc'},
|
||||||
key2: {message: 'key2 message', description: 'key2 desc'},
|
key2: {message: 'key2 message', description: 'key2 desc'},
|
||||||
key3: {message: 'key3 message', description: undefined},
|
key3: {message: 'key3 message', description: undefined},
|
||||||
});
|
});
|
||||||
|
|
||||||
await writeTranslationFileContent({
|
await writeCodeTranslations(
|
||||||
filePath,
|
{siteDir, locale: 'en'},
|
||||||
content: {
|
{
|
||||||
key1: {message: 'key1 message new', description: undefined},
|
key1: {message: 'key1 message new', description: undefined},
|
||||||
key2: {message: 'key2 message new', description: 'key2 desc new'},
|
key2: {message: 'key2 message new', description: 'key2 desc new'},
|
||||||
key3: {message: 'key3 message new', description: 'key3 desc new'},
|
key3: {message: 'key3 message new', description: 'key3 desc new'},
|
||||||
},
|
},
|
||||||
});
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
await expect(readFile()).resolves.toEqual({
|
await expect(readFile()).resolves.toEqual({
|
||||||
key1: {message: 'key1 message', description: undefined},
|
key1: {message: 'key1 message', description: undefined},
|
||||||
key2: {message: 'key2 message', description: 'key2 desc new'},
|
key2: {message: 'key2 message', description: 'key2 desc new'},
|
||||||
key3: {message: 'key3 message', description: 'key3 desc new'},
|
key3: {message: 'key3 message', description: 'key3 desc new'},
|
||||||
});
|
});
|
||||||
|
expect(consoleInfoMock).toBeCalledWith(
|
||||||
|
expect.stringMatching(/3.* translations will be written/),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not create empty translation files', async () => {
|
||||||
|
const {siteDir, readFile} = await createTmpTranslationFile(null);
|
||||||
|
|
||||||
|
await writeCodeTranslations({siteDir, locale: 'en'}, {}, {});
|
||||||
|
|
||||||
|
await expect(readFile()).rejects.toThrowError(
|
||||||
|
/ENOENT: no such file or directory, open /,
|
||||||
|
);
|
||||||
|
expect(consoleInfoMock).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws for invalid content', async () => {
|
it('throws for invalid content', async () => {
|
||||||
const {filePath} = await createTmpTranslationFile(
|
const {siteDir} = await createTmpTranslationFile(
|
||||||
// @ts-expect-error: bad content on purpose
|
// @ts-expect-error: bad content on purpose
|
||||||
{bad: 'content'},
|
{bad: 'content'},
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(() =>
|
||||||
writeTranslationFileContent({
|
writeCodeTranslations(
|
||||||
filePath,
|
{siteDir, locale: 'en'},
|
||||||
content: {
|
{
|
||||||
key1: {message: 'key1 message'},
|
key1: {message: 'key1 message'},
|
||||||
},
|
},
|
||||||
}),
|
{},
|
||||||
|
),
|
||||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
`"\\"bad\\" must be of type object"`,
|
`"\\"bad\\" must be of type object"`,
|
||||||
);
|
);
|
||||||
|
@ -308,7 +299,7 @@ describe('writePluginTranslations', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(readTranslationFileContent(filePath)).resolves.toEqual({
|
await expect(fs.readJSON(filePath)).resolves.toEqual({
|
||||||
key1: {message: 'key1 message'},
|
key1: {message: 'key1 message'},
|
||||||
key2: {message: 'key2 message'},
|
key2: {message: 'key2 message'},
|
||||||
key3: {message: 'key3 message'},
|
key3: {message: 'key3 message'},
|
||||||
|
@ -348,14 +339,12 @@ describe('writePluginTranslations', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(readTranslationFileContent(filePath)).resolves.toBeUndefined();
|
|
||||||
|
|
||||||
await doWritePluginTranslations({
|
await doWritePluginTranslations({
|
||||||
key1: {message: 'key1 message', description: 'key1 desc'},
|
key1: {message: 'key1 message', description: 'key1 desc'},
|
||||||
key2: {message: 'key2 message', description: 'key2 desc'},
|
key2: {message: 'key2 message', description: 'key2 desc'},
|
||||||
key3: {message: 'key3 message', description: 'key3 desc'},
|
key3: {message: 'key3 message', description: 'key3 desc'},
|
||||||
});
|
});
|
||||||
await expect(readTranslationFileContent(filePath)).resolves.toEqual({
|
await expect(fs.readJSON(filePath)).resolves.toEqual({
|
||||||
key1: {message: 'key1 message', description: 'key1 desc'},
|
key1: {message: 'key1 message', description: 'key1 desc'},
|
||||||
key2: {message: 'key2 message', description: 'key2 desc'},
|
key2: {message: 'key2 message', description: 'key2 desc'},
|
||||||
key3: {message: 'key3 message', description: 'key3 desc'},
|
key3: {message: 'key3 message', description: 'key3 desc'},
|
||||||
|
@ -368,7 +357,7 @@ describe('writePluginTranslations', () => {
|
||||||
},
|
},
|
||||||
{messagePrefix: 'PREFIX '},
|
{messagePrefix: 'PREFIX '},
|
||||||
);
|
);
|
||||||
await expect(readTranslationFileContent(filePath)).resolves.toEqual({
|
await expect(fs.readJSON(filePath)).resolves.toEqual({
|
||||||
key1: {message: 'key1 message', description: 'key1 desc'},
|
key1: {message: 'key1 message', description: 'key1 desc'},
|
||||||
key2: {message: 'key2 message', description: 'key2 desc'},
|
key2: {message: 'key2 message', description: 'key2 desc'},
|
||||||
key3: {message: 'key3 message', description: undefined},
|
key3: {message: 'key3 message', description: undefined},
|
||||||
|
@ -384,13 +373,39 @@ describe('writePluginTranslations', () => {
|
||||||
},
|
},
|
||||||
{messagePrefix: 'PREFIX ', override: true},
|
{messagePrefix: 'PREFIX ', override: true},
|
||||||
);
|
);
|
||||||
await expect(readTranslationFileContent(filePath)).resolves.toEqual({
|
await expect(fs.readJSON(filePath)).resolves.toEqual({
|
||||||
key1: {message: 'PREFIX key1 message 3', description: 'key1 desc'},
|
key1: {message: 'PREFIX key1 message 3', description: 'key1 desc'},
|
||||||
key2: {message: 'PREFIX key2 message 3', description: 'key2 desc'},
|
key2: {message: 'PREFIX key2 message 3', description: 'key2 desc'},
|
||||||
key3: {message: 'PREFIX key3 message 3', description: 'key3 desc'},
|
key3: {message: 'PREFIX key3 message 3', description: 'key3 desc'},
|
||||||
key4: {message: 'PREFIX key4 message 3', description: 'key4 desc'},
|
key4: {message: 'PREFIX key4 message 3', description: 'key4 desc'},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('throws with explicit extension', async () => {
|
||||||
|
const siteDir = await createTmpSiteDir();
|
||||||
|
|
||||||
|
await expect(() =>
|
||||||
|
writePluginTranslations({
|
||||||
|
siteDir,
|
||||||
|
locale: 'fr',
|
||||||
|
translationFile: {
|
||||||
|
path: 'my/translation/file.json',
|
||||||
|
content: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugin: {
|
||||||
|
name: 'my-plugin-name',
|
||||||
|
options: {
|
||||||
|
id: 'my-plugin-id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
options: {},
|
||||||
|
}),
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Translation file path at \\"my/translation/file.json\\" does not need to end with \\".json\\", we add the extension automatically."`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('localizePluginTranslationFile', () => {
|
describe('localizePluginTranslationFile', () => {
|
||||||
|
@ -420,22 +435,22 @@ describe('localizePluginTranslationFile', () => {
|
||||||
expect(localizedTranslationFile).toEqual(translationFile);
|
expect(localizedTranslationFile).toEqual(translationFile);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not localize if localized file does not exist 2', async () => {
|
it('normalizes partially localized translation files', async () => {
|
||||||
const siteDir = await createTmpSiteDir();
|
const siteDir = await createTmpSiteDir();
|
||||||
|
|
||||||
await writeTranslationFileContent({
|
await fs.outputJSON(
|
||||||
filePath: path.join(
|
path.join(
|
||||||
siteDir,
|
siteDir,
|
||||||
'i18n',
|
'i18n',
|
||||||
'fr',
|
'fr',
|
||||||
'my-plugin-name',
|
'my-plugin-name',
|
||||||
'my/translation/file.json',
|
'my/translation/file.json',
|
||||||
),
|
),
|
||||||
content: {
|
{
|
||||||
key2: {message: 'key2 message localized'},
|
key2: {message: 'key2 message localized'},
|
||||||
key4: {message: 'key4 message localized'},
|
key4: {message: 'key4 message localized'},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
const translationFile: TranslationFile = {
|
const translationFile: TranslationFile = {
|
||||||
path: 'my/translation/file',
|
path: 'my/translation/file',
|
||||||
|
@ -472,6 +487,68 @@ describe('localizePluginTranslationFile', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('readCodeTranslationFileContent', () => {
|
||||||
|
async function testReadTranslation(val: TranslationFileContent) {
|
||||||
|
const {siteDir} = await createTmpTranslationFile(val);
|
||||||
|
return readCodeTranslationFileContent({siteDir, locale: 'en'});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("returns undefined if file does't exist", async () => {
|
||||||
|
await expect(
|
||||||
|
readCodeTranslationFileContent({siteDir: 'foo', locale: 'en'}),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes valid translation file content', async () => {
|
||||||
|
await expect(testReadTranslation({})).resolves.toEqual({});
|
||||||
|
await expect(testReadTranslation({key1: {message: ''}})).resolves.toEqual({
|
||||||
|
key1: {message: ''},
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
testReadTranslation({key1: {message: 'abc', description: 'desc'}}),
|
||||||
|
).resolves.toEqual({key1: {message: 'abc', description: 'desc'}});
|
||||||
|
await expect(
|
||||||
|
testReadTranslation({
|
||||||
|
key1: {message: 'abc', description: 'desc'},
|
||||||
|
key2: {message: 'def', description: 'desc'},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({
|
||||||
|
key1: {message: 'abc', description: 'desc'},
|
||||||
|
key2: {message: 'def', description: 'desc'},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails for invalid translation file content', async () => {
|
||||||
|
await expect(() =>
|
||||||
|
testReadTranslation('HEY'),
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"\\"value\\" must be of type object"`,
|
||||||
|
);
|
||||||
|
await expect(() =>
|
||||||
|
testReadTranslation(42),
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"\\"value\\" must be of type object"`,
|
||||||
|
);
|
||||||
|
await expect(() =>
|
||||||
|
testReadTranslation({key: {description: 'no message'}}),
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"\\"key.message\\" is required"`,
|
||||||
|
);
|
||||||
|
await expect(() =>
|
||||||
|
testReadTranslation({key: {message: 42}}),
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"\\"key.message\\" must be a string"`,
|
||||||
|
);
|
||||||
|
await expect(() =>
|
||||||
|
testReadTranslation({
|
||||||
|
key: {message: 'Message', description: 42},
|
||||||
|
}),
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"\\"key.description\\" must be a string"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getPluginsDefaultCodeTranslationMessages', () => {
|
describe('getPluginsDefaultCodeTranslationMessages', () => {
|
||||||
function createTestPlugin(
|
function createTestPlugin(
|
||||||
fn: InitializedPlugin['getDefaultCodeTranslationMessages'],
|
fn: InitializedPlugin['getDefaultCodeTranslationMessages'],
|
||||||
|
@ -559,9 +636,11 @@ describe('getPluginsDefaultCodeTranslationMessages', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('applyDefaultCodeTranslations', () => {
|
describe('applyDefaultCodeTranslations', () => {
|
||||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
const consoleWarnMock = jest
|
||||||
|
.spyOn(console, 'warn')
|
||||||
|
.mockImplementation(() => {});
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
consoleSpy.mockClear();
|
consoleWarnMock.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works for no code and message', () => {
|
it('works for no code and message', () => {
|
||||||
|
@ -571,7 +650,7 @@ describe('applyDefaultCodeTranslations', () => {
|
||||||
defaultCodeMessages: {},
|
defaultCodeMessages: {},
|
||||||
}),
|
}),
|
||||||
).toEqual({});
|
).toEqual({});
|
||||||
expect(consoleSpy).toHaveBeenCalledTimes(0);
|
expect(consoleWarnMock).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works for code and message', () => {
|
it('works for code and message', () => {
|
||||||
|
@ -593,7 +672,7 @@ describe('applyDefaultCodeTranslations', () => {
|
||||||
description: 'description',
|
description: 'description',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(consoleSpy).toHaveBeenCalledTimes(0);
|
expect(consoleWarnMock).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works for code and message mismatch', () => {
|
it('works for code and message mismatch', () => {
|
||||||
|
@ -615,8 +694,8 @@ describe('applyDefaultCodeTranslations', () => {
|
||||||
description: 'description',
|
description: 'description',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
expect(consoleWarnMock).toHaveBeenCalledTimes(1);
|
||||||
expect(consoleSpy.mock.calls[0][0]).toMatch(/unknownId/);
|
expect(consoleWarnMock.mock.calls[0][0]).toMatch(/unknownId/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works for realistic scenario', () => {
|
it('works for realistic scenario', () => {
|
||||||
|
@ -657,8 +736,8 @@ describe('applyDefaultCodeTranslations', () => {
|
||||||
description: 'description 3',
|
description: 'description 3',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
expect(consoleWarnMock).toHaveBeenCalledTimes(1);
|
||||||
expect(consoleSpy.mock.calls[0][0]).toMatch(/idUnknown1/);
|
expect(consoleWarnMock.mock.calls[0][0]).toMatch(/idUnknown1/);
|
||||||
expect(consoleSpy.mock.calls[0][0]).toMatch(/idUnknown2/);
|
expect(consoleWarnMock.mock.calls[0][0]).toMatch(/idUnknown2/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -232,6 +232,7 @@ export default function MyComponent<T>(props: ComponentProps<T>) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'}) as string}/>
|
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'}) as string}/>
|
||||||
|
<input text={translate({message: 'code message 2',description: 'code description 2'}) as string}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -247,6 +248,10 @@ export default function MyComponent<T>(props: ComponentProps<T>) {
|
||||||
sourceCodeFilePath,
|
sourceCodeFilePath,
|
||||||
translations: {
|
translations: {
|
||||||
codeId: {message: 'code message', description: 'code description'},
|
codeId: {message: 'code message', description: 'code description'},
|
||||||
|
'code message 2': {
|
||||||
|
message: 'code message 2',
|
||||||
|
description: 'code description 2',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
warnings: [],
|
warnings: [],
|
||||||
});
|
});
|
||||||
|
@ -636,6 +641,26 @@ export default function MyComponent() {
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const plugin1File4 = path.join(plugin1Dir, 'src/theme/file4.jsx');
|
||||||
|
// Contains some invalid translations...
|
||||||
|
await fs.outputFile(
|
||||||
|
plugin1File4,
|
||||||
|
`
|
||||||
|
import {translate} from '@docusaurus/Translate';
|
||||||
|
|
||||||
|
export default function MyComponent() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input text={translate({id: index})}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const consoleWarnMock = jest
|
||||||
|
.spyOn(console, 'warn')
|
||||||
|
.mockImplementation(() => {});
|
||||||
const plugin1 = createTestPlugin(plugin1Dir);
|
const plugin1 = createTestPlugin(plugin1Dir);
|
||||||
|
|
||||||
const plugin2Dir = await createTmpDir();
|
const plugin2Dir = await createTmpDir();
|
||||||
|
@ -664,7 +689,11 @@ export default function MyComponent(props: Props) {
|
||||||
);
|
);
|
||||||
const plugin2 = createTestPlugin(plugin2Dir);
|
const plugin2 = createTestPlugin(plugin2Dir);
|
||||||
|
|
||||||
const plugins = [plugin1, plugin2];
|
const plugins = [
|
||||||
|
plugin1,
|
||||||
|
plugin2,
|
||||||
|
{name: 'dummy', options: {}, version: {type: 'synthetic'}} as const,
|
||||||
|
];
|
||||||
const translations = await extractSiteSourceCodeTranslations(
|
const translations = await extractSiteSourceCodeTranslations(
|
||||||
siteDir,
|
siteDir,
|
||||||
plugins,
|
plugins,
|
||||||
|
@ -692,5 +721,8 @@ export default function MyComponent(props: Props) {
|
||||||
message: 'plugin2 message 2',
|
message: 'plugin2 message 2',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
expect(consoleWarnMock.mock.calls[0][0]).toMatch(
|
||||||
|
/.*\[WARNING\].* Translation extraction warnings for file .*src.theme.file4\.jsx.*\n.*- translate\(\) first arg should be a statically evaluable object\./,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,7 +38,7 @@ const TranslationFileContentSchema = Joi.object<TranslationFileContent>()
|
||||||
)
|
)
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
export function ensureTranslationFileContent(
|
function ensureTranslationFileContent(
|
||||||
content: unknown,
|
content: unknown,
|
||||||
): asserts content is TranslationFileContent {
|
): asserts content is TranslationFileContent {
|
||||||
Joi.attempt(content, TranslationFileContentSchema, {
|
Joi.attempt(content, TranslationFileContentSchema, {
|
||||||
|
@ -48,7 +48,7 @@ export function ensureTranslationFileContent(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readTranslationFileContent(
|
async function readTranslationFileContent(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
): Promise<TranslationFileContent | undefined> {
|
): Promise<TranslationFileContent | undefined> {
|
||||||
if (await fs.pathExists(filePath)) {
|
if (await fs.pathExists(filePath)) {
|
||||||
|
@ -97,7 +97,7 @@ function mergeTranslationFileContent({
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function writeTranslationFileContent({
|
async function writeTranslationFileContent({
|
||||||
filePath,
|
filePath,
|
||||||
content: newContent,
|
content: newContent,
|
||||||
options = {},
|
options = {},
|
||||||
|
@ -139,7 +139,7 @@ Maybe you should remove them? ${unknownKeys}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// should we make this configurable?
|
// should we make this configurable?
|
||||||
export function getTranslationsDirPath(context: TranslationContext): string {
|
function getTranslationsDirPath(context: TranslationContext): string {
|
||||||
return path.resolve(path.join(context.siteDir, `i18n`));
|
return path.resolve(path.join(context.siteDir, `i18n`));
|
||||||
}
|
}
|
||||||
export function getTranslationsLocaleDirPath(
|
export function getTranslationsLocaleDirPath(
|
||||||
|
@ -148,9 +148,7 @@ export function getTranslationsLocaleDirPath(
|
||||||
return path.join(getTranslationsDirPath(context), context.locale);
|
return path.join(getTranslationsDirPath(context), context.locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCodeTranslationsFilePath(
|
function getCodeTranslationsFilePath(context: TranslationContext): string {
|
||||||
context: TranslationContext,
|
|
||||||
): string {
|
|
||||||
return path.join(getTranslationsLocaleDirPath(context), 'code.json');
|
return path.join(getTranslationsLocaleDirPath(context), 'code.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ function testStylelintRule(config, tests) {
|
||||||
}
|
}
|
||||||
const fixedOutput = await stylelint.lint({...options, fix: true});
|
const fixedOutput = await stylelint.lint({...options, fix: true});
|
||||||
const fixedCode = getOutputCss(fixedOutput);
|
const fixedCode = getOutputCss(fixedOutput);
|
||||||
expect(fixedCode).toBe(testCase.fixed);
|
expect(fixedCode).toBe(testCase.code);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -113,22 +113,63 @@ testStylelintRule(
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ruleName,
|
ruleName,
|
||||||
fix: false,
|
fix: true,
|
||||||
accept: [
|
accept: [
|
||||||
{
|
{
|
||||||
code: `
|
code: `
|
||||||
/**
|
/**
|
||||||
* Copyright
|
* Copyright
|
||||||
*/
|
*/
|
||||||
|
.foo {}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `/**
|
||||||
|
* Copyright
|
||||||
|
*/
|
||||||
|
|
||||||
|
.foo {}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `/**
|
||||||
|
* Copyright
|
||||||
|
*/
|
||||||
.foo {}`,
|
.foo {}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
reject: [
|
reject: [
|
||||||
|
{
|
||||||
|
code: `.foo {}`,
|
||||||
|
fixed: `/**
|
||||||
|
* Copyright
|
||||||
|
*/
|
||||||
|
.foo {}`,
|
||||||
|
message: messages.rejected,
|
||||||
|
line: 1,
|
||||||
|
column: 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
code: `
|
code: `
|
||||||
|
.foo {}`,
|
||||||
|
fixed: `/**
|
||||||
|
* Copyright
|
||||||
|
*/
|
||||||
|
.foo {}`,
|
||||||
|
message: messages.rejected,
|
||||||
|
line: 1,
|
||||||
|
column: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `/**
|
||||||
|
* Copyright
|
||||||
|
*/
|
||||||
|
|
||||||
|
.foo {}`,
|
||||||
|
fixed: `/**
|
||||||
|
* Copyright
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* copyright
|
* Copyright
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.foo {}`,
|
.foo {}`,
|
||||||
|
@ -137,7 +178,15 @@ testStylelintRule(
|
||||||
column: 1,
|
column: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: `
|
code: `/**
|
||||||
|
* Copyleft
|
||||||
|
*/
|
||||||
|
|
||||||
|
.foo {}`,
|
||||||
|
fixed: `/**
|
||||||
|
* Copyright
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyleft
|
* Copyleft
|
||||||
*/
|
*/
|
||||||
|
@ -148,7 +197,18 @@ testStylelintRule(
|
||||||
column: 1,
|
column: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: `
|
code: `/**
|
||||||
|
* Copyleft
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright
|
||||||
|
*/
|
||||||
|
.foo {}`,
|
||||||
|
fixed: `/**
|
||||||
|
* Copyright
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyleft
|
* Copyleft
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -15,7 +15,7 @@ const messages = stylelint.utils.ruleMessages(ruleName, {
|
||||||
const plugin = stylelint.createPlugin(
|
const plugin = stylelint.createPlugin(
|
||||||
ruleName,
|
ruleName,
|
||||||
(primaryOption, secondaryOption, context) => (root, result) => {
|
(primaryOption, secondaryOption, context) => (root, result) => {
|
||||||
const validOptions = stylelint.utils.validateOptions(
|
stylelint.utils.validateOptions(
|
||||||
result,
|
result,
|
||||||
ruleName,
|
ruleName,
|
||||||
{
|
{
|
||||||
|
@ -28,10 +28,6 @@ const plugin = stylelint.createPlugin(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!validOptions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
root.first &&
|
root.first &&
|
||||||
root.first.type === 'comment' &&
|
root.first.type === 'comment' &&
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue