diff --git a/packages/create-docusaurus/src/index.ts b/packages/create-docusaurus/src/index.ts index aa270c292f..1562e756b5 100755 --- a/packages/create-docusaurus/src/index.ts +++ b/packages/create-docusaurus/src/index.ts @@ -37,10 +37,7 @@ async function findPackageManagerFromLockFile(): Promise< SupportedPackageManager | undefined > { for (const packageManager of PackageManagersList) { - const lockFilePath = path.resolve( - process.cwd(), - SupportedPackageManagers[packageManager], - ); + const lockFilePath = path.resolve(SupportedPackageManagers[packageManager]); if (await fs.pathExists(lockFilePath)) { return packageManager; } @@ -152,7 +149,7 @@ async function copyTemplate( template: 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 // base template @@ -211,7 +208,7 @@ export default async function init( const templates = await readTemplates(templatesDir); const hasTS = (templateName: string) => fs.pathExists( - path.resolve(templatesDir, `${templateName}${TypeScriptTemplateSuffix}`), + path.join(templatesDir, `${templateName}${TypeScriptTemplateSuffix}`), ); let name = siteName; @@ -297,7 +294,7 @@ export default async function init( name: 'templateDir', validate: async (dir?: string) => { if (dir) { - const fullDir = path.resolve(process.cwd(), dir); + const fullDir = path.resolve(dir); if (await fs.pathExists(fullDir)) { return true; } @@ -351,8 +348,8 @@ export default async function init( logger.error`Copying Docusaurus template name=${template} failed!`; throw err; } - } else if (await fs.pathExists(path.resolve(process.cwd(), template))) { - const templateDir = path.resolve(process.cwd(), template); + } else if (await fs.pathExists(path.resolve(template))) { + const templateDir = path.resolve(template); try { await fs.copy(templateDir, dest); } catch (err) { diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/outside/doc1.md b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/outside/doc1.md new file mode 100644 index 0000000000..4fd86e1c55 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/outside/doc1.md @@ -0,0 +1 @@ +[link](../docs/doc1.md) diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/linkify.test.ts b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/linkify.test.ts index 4ee10a4acd..2cf56ba9dc 100644 --- a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/linkify.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/linkify.test.ts @@ -193,4 +193,14 @@ describe('linkify', () => { expect(transformedContent).not.toContain('](../doc2.md)'); 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 \\"/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/outside/doc1.md\\" does not belong to any docs version!"`, + ); + }); }); diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts b/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts index a029e92610..39e45e8f39 100644 --- a/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts +++ b/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts @@ -15,6 +15,10 @@ function getVersion(filePath: string, options: DocsMarkdownOption) { 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) { throw new Error( `Unexpected error: Markdown file at "${filePath}" does not belong to any docs version!`, diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/postProcessor.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/postProcessor.ts index 48dafff60c..44c652fe19 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/postProcessor.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/postProcessor.ts @@ -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.`, ); } - switch (category.link.type) { - case 'doc': - return { + return category.link.type === 'doc' + ? { type: 'doc', label: category.label, id: category.link.id, - }; - case 'generated-index': - return { + } + : { type: 'link', label: category.label, href: category.link.permalink, }; - default: - throw new Error('Unexpected sidebar category link type'); - } } // A non-collapsible category can't be collapsed! if (category.collapsible === false) { diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts index da095918d5..ee5de8bb58 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts @@ -376,18 +376,13 @@ export function toNavigationLink( return undefined; } - if (navigationItem.type === 'doc') { - return toDocNavigationLink(getDocById(navigationItem.id)); - } else if (navigationItem.type === 'category') { - if (navigationItem.link.type === 'doc') { - return toDocNavigationLink(getDocById(navigationItem.link.id)); - } else if (navigationItem.link.type === 'generated-index') { - return { - title: navigationItem.label, - permalink: navigationItem.link.permalink, - }; - } - throw new Error('unexpected category link type'); + if (navigationItem.type === 'category') { + return navigationItem.link.type === 'doc' + ? toDocNavigationLink(getDocById(navigationItem.link.id)) + : { + title: navigationItem.label, + permalink: navigationItem.link.permalink, + }; } - throw new Error('unexpected navigation item'); + return toDocNavigationLink(getDocById(navigationItem.id)); } diff --git a/packages/docusaurus-theme-translations/__tests__/update.test.ts b/packages/docusaurus-theme-translations/locales/__tests__/locales.test.ts similarity index 76% rename from packages/docusaurus-theme-translations/__tests__/update.test.ts rename to packages/docusaurus-theme-translations/locales/__tests__/locales.test.ts index d9b23c69d0..e88aa90f14 100644 --- a/packages/docusaurus-theme-translations/__tests__/update.test.ts +++ b/packages/docusaurus-theme-translations/locales/__tests__/locales.test.ts @@ -6,7 +6,7 @@ */ import {jest} from '@jest/globals'; -import {extractThemeCodeMessages} from '../update'; +import {extractThemeCodeMessages} from '../../src/utils'; import path from 'path'; import fs from 'fs-extra'; import _ from 'lodash'; @@ -16,20 +16,17 @@ jest.setTimeout(15000); 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 () => { - const baseMessagesDirPath = path.join(__dirname, '../locales/base'); + const baseMessagesDirPath = path.join(__dirname, '../base'); const baseMessages = Object.fromEntries( await Promise.all( ( await fs.readdir(baseMessagesDirPath) ).map(async (baseMessagesFile) => Object.entries( - JSON.parse( - ( - await fs.readFile( - path.join(baseMessagesDirPath, baseMessagesFile), - ) - ).toString(), - ) as Record, + (await fs.readJSON( + path.join(baseMessagesDirPath, baseMessagesFile), + 'utf-8', + )) as Record, ), ), ).then((translations) => diff --git a/packages/docusaurus-theme-translations/locales/ar/theme-common.json b/packages/docusaurus-theme-translations/locales/ar/theme-common.json index 588fef9b2c..24e99fddfe 100644 --- a/packages/docusaurus-theme-translations/locales/ar/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/ar/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "أحدث مشاركات المدونة", "theme.blog.tagTitle": "{nPosts} موسومة ب \"{tagName}\"", "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.light": "light mode", "theme.common.editThisPage": "تعديل هذه الصفحة", "theme.common.headingLinkTitle": "ارتباط مباشر بالعنوان", "theme.common.skipToMainContent": "انتقل إلى المحتوى الرئيسي", diff --git a/packages/docusaurus-theme-translations/locales/base/theme-common.json b/packages/docusaurus-theme-translations/locales/base/theme-common.json index 70177fbbe9..fb5f46320e 100644 --- a/packages/docusaurus-theme-translations/locales/base/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/base/theme-common.json @@ -53,10 +53,10 @@ "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___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___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___DESCRIPTION": "The link label to edit the current page", "theme.common.headingLinkTitle": "Direct link to heading", diff --git a/packages/docusaurus-theme-translations/locales/bn/theme-common.json b/packages/docusaurus-theme-translations/locales/bn/theme-common.json index d1f9b50770..a05c817a21 100644 --- a/packages/docusaurus-theme-translations/locales/bn/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/bn/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "সাম্প্রতিক ব্লগ পোস্ট নেভিগেশন", "theme.blog.tagTitle": "{nPosts} সঙ্গে ট্যাগ্গেড \"{tagName}\" ", "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.light": "light mode", "theme.common.editThisPage": "এই পেজটি এডিট করুন", "theme.common.headingLinkTitle": "হেডিং এর সঙ্গে সরাসরি লিংকড", "theme.common.skipToMainContent": "স্কিপ করে মূল কন্টেন্ট এ যান", diff --git a/packages/docusaurus-theme-translations/locales/cs/theme-common.json b/packages/docusaurus-theme-translations/locales/cs/theme-common.json index 884a93704a..e0245b23ad 100644 --- a/packages/docusaurus-theme-translations/locales/cs/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/cs/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Navigace s aktuálními články na blogu", "theme.blog.tagTitle": "{nPosts} s tagem \"{tagName}\"", "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.light": "light mode", "theme.common.editThisPage": "Upravit tuto stránku", "theme.common.headingLinkTitle": "Přímý odkaz na nadpis", "theme.common.skipToMainContent": "Přeskočit na hlavní obsah", diff --git a/packages/docusaurus-theme-translations/locales/da/theme-common.json b/packages/docusaurus-theme-translations/locales/da/theme-common.json index 8da1bae99a..0591720ce6 100644 --- a/packages/docusaurus-theme-translations/locales/da/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/da/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation", "theme.blog.tagTitle": "{nPosts} med følgende tag \"{tagName}\"", "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.light": "light mode", "theme.common.editThisPage": "Rediger denne side", "theme.common.headingLinkTitle": "Direkte link til overskrift", "theme.common.skipToMainContent": "Hop til hovedindhold", diff --git a/packages/docusaurus-theme-translations/locales/de/theme-common.json b/packages/docusaurus-theme-translations/locales/de/theme-common.json index 2520186a25..9be17350a4 100644 --- a/packages/docusaurus-theme-translations/locales/de/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/de/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation", "theme.blog.tagTitle": "{nPosts} getaggt mit \"{tagName}\"", "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.light": "light mode", "theme.common.editThisPage": "Diese Seite bearbeiten", "theme.common.headingLinkTitle": "Direkter Link zur Überschrift", "theme.common.skipToMainContent": "Zum Hauptinhalt springen", diff --git a/packages/docusaurus-theme-translations/locales/es/theme-common.json b/packages/docusaurus-theme-translations/locales/es/theme-common.json index 5e73c5ebc5..fbf4761467 100644 --- a/packages/docusaurus-theme-translations/locales/es/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/es/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Navegación de publicaciones recientes", "theme.blog.tagTitle": "{nPosts} etiquetados con \"{tagName}\"", "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.light": "light mode", "theme.common.editThisPage": "Editar esta página", "theme.common.headingLinkTitle": "Enlace directo al encabezado", "theme.common.skipToMainContent": "Saltar al contenido principal", diff --git a/packages/docusaurus-theme-translations/locales/fa/theme-common.json b/packages/docusaurus-theme-translations/locales/fa/theme-common.json index 7341b54f95..1ba33a3417 100644 --- a/packages/docusaurus-theme-translations/locales/fa/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/fa/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "کنترل پست های اخیر وبلاگ", "theme.blog.tagTitle": "{nPosts} با برچسب \"{tagName}\"", "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.light": "light mode", "theme.common.editThisPage": "ویرایش مطالب این صفحه", "theme.common.headingLinkTitle": "لینک مستقیم به عنوان", "theme.common.skipToMainContent": "پرش به مطلب اصلی", diff --git a/packages/docusaurus-theme-translations/locales/fil/theme-common.json b/packages/docusaurus-theme-translations/locales/fil/theme-common.json index e569c96bec..af8d60182d 100644 --- a/packages/docusaurus-theme-translations/locales/fil/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/fil/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation", "theme.blog.tagTitle": "{nPosts} na may tag na \"{tagName}\"", "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.light": "light mode", "theme.common.editThisPage": "I-edit ang page", "theme.common.headingLinkTitle": "Direktang link patungo sa heading", "theme.common.skipToMainContent": "Lumaktaw patungo sa pangunahing content", diff --git a/packages/docusaurus-theme-translations/locales/fr/theme-common.json b/packages/docusaurus-theme-translations/locales/fr/theme-common.json index 3e29910434..efaa07c611 100644 --- a/packages/docusaurus-theme-translations/locales/fr/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/fr/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Navigation article de blog récent", "theme.blog.tagTitle": "{nPosts} tagués avec « {tagName} »", "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.light": "light mode", "theme.common.editThisPage": "Éditer cette page", "theme.common.headingLinkTitle": "Lien direct vers le titre", "theme.common.skipToMainContent": "Aller au contenu principal", diff --git a/packages/docusaurus-theme-translations/locales/he/theme-common.json b/packages/docusaurus-theme-translations/locales/he/theme-common.json index e5c757aa59..fab29a6c24 100644 --- a/packages/docusaurus-theme-translations/locales/he/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/he/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "מעבר לרשומות אחרונות בבלוג", "theme.blog.tagTitle": "{nPosts} עם התגית \"{tagName}\"", "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.light": "light mode", "theme.common.editThisPage": "ערוך דף זה", "theme.common.headingLinkTitle": "קישור ישיר לכותרת", "theme.common.skipToMainContent": "דלג לתוכן הראשי", diff --git a/packages/docusaurus-theme-translations/locales/hi/theme-common.json b/packages/docusaurus-theme-translations/locales/hi/theme-common.json index 8695d4ccd5..1850be8228 100644 --- a/packages/docusaurus-theme-translations/locales/hi/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/hi/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "नया ब्लॉग पोस्ट नेविगेशन", "theme.blog.tagTitle": "{nPosts} पोस्ट \"{tagName}\" टैग के साथ", "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.light": "light mode", "theme.common.editThisPage": "इस पेज को बदलें", "theme.common.headingLinkTitle": "शीर्षक का सीधा लिंक", "theme.common.skipToMainContent": "मुख्य कंटेंट तक स्किप करें", diff --git a/packages/docusaurus-theme-translations/locales/it/theme-common.json b/packages/docusaurus-theme-translations/locales/it/theme-common.json index 2b576f920b..feed8869eb 100644 --- a/packages/docusaurus-theme-translations/locales/it/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/it/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Navigazione dei post recenti del blog", "theme.blog.tagTitle": "{nPosts} etichettati con \"{tagName}\"", "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.light": "light mode", "theme.common.editThisPage": "Modifica questa pagina", "theme.common.headingLinkTitle": "Link diretto all'intestazione", "theme.common.skipToMainContent": "Passa al contenuto principale", diff --git a/packages/docusaurus-theme-translations/locales/ja/theme-common.json b/packages/docusaurus-theme-translations/locales/ja/theme-common.json index f55dcc8ca3..9264284920 100644 --- a/packages/docusaurus-theme-translations/locales/ja/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/ja/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation", "theme.blog.tagTitle": "「{tagName}」タグの記事が{nPosts}あります", "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.light": "light mode", "theme.common.editThisPage": "このページを編集", "theme.common.headingLinkTitle": "見出しへの直接リンク", "theme.common.skipToMainContent": "メインコンテンツまでスキップ", diff --git a/packages/docusaurus-theme-translations/locales/ko/theme-common.json b/packages/docusaurus-theme-translations/locales/ko/theme-common.json index c3eb573c55..688c695499 100644 --- a/packages/docusaurus-theme-translations/locales/ko/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/ko/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "최근 블로그 문서 둘러보기", "theme.blog.tagTitle": "\"{tagName}\" 태그로 연결된 {nPosts}개의 게시물이 있습니다.", "theme.colorToggle.ariaLabel": "어두운 모드와 밝은 모드 전환하기 (현재 {mode})", - "theme.colorToggle.ariaLabel.mode.light": "밝은 모드", "theme.colorToggle.ariaLabel.mode.dark": "어두운 모드", + "theme.colorToggle.ariaLabel.mode.light": "밝은 모드", "theme.common.editThisPage": "페이지 편집", "theme.common.headingLinkTitle": "제목으로 바로 가기", "theme.common.skipToMainContent": "본문으로 건너뛰기", diff --git a/packages/docusaurus-theme-translations/locales/pl/theme-common.json b/packages/docusaurus-theme-translations/locales/pl/theme-common.json index 8f64904758..a4e7bf78fa 100644 --- a/packages/docusaurus-theme-translations/locales/pl/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/pl/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation", "theme.blog.tagTitle": "{nPosts} z tagiem \"{tagName}\"", "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.light": "light mode", "theme.common.editThisPage": "Edytuj tą stronę", "theme.common.headingLinkTitle": "Bezpośredni link do nagłówka", "theme.common.skipToMainContent": "Przejdź do głównej zawartości", diff --git a/packages/docusaurus-theme-translations/locales/pt-BR/theme-common.json b/packages/docusaurus-theme-translations/locales/pt-BR/theme-common.json index 5b8b39f072..31a708844d 100644 --- a/packages/docusaurus-theme-translations/locales/pt-BR/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/pt-BR/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation", "theme.blog.tagTitle": "{nPosts} marcadas com \"{tagName}\"", "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.light": "light mode", "theme.common.editThisPage": "Editar essa página", "theme.common.headingLinkTitle": "Link direto para o título", "theme.common.skipToMainContent": "Pular para o conteúdo principal", diff --git a/packages/docusaurus-theme-translations/locales/pt-PT/theme-common.json b/packages/docusaurus-theme-translations/locales/pt-PT/theme-common.json index caf4e20d73..349ce35e09 100644 --- a/packages/docusaurus-theme-translations/locales/pt-PT/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/pt-PT/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Blog recent posts navigation", "theme.blog.tagTitle": "{nPosts} marcadas com \"{tagName}\"", "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.light": "light mode", "theme.common.editThisPage": "Editar esta página", "theme.common.headingLinkTitle": "Link direto para o título", "theme.common.skipToMainContent": "Saltar para o conteúdo principal", diff --git a/packages/docusaurus-theme-translations/locales/ru/theme-common.json b/packages/docusaurus-theme-translations/locales/ru/theme-common.json index ac996abdd7..36cc2692eb 100644 --- a/packages/docusaurus-theme-translations/locales/ru/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/ru/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Навигация по последним постам в блоге", "theme.blog.tagTitle": "{nPosts} с тегом \"{tagName}\"", "theme.colorToggle.ariaLabel": "Переключение между темным и светлым режимом (сейчас используется {mode})", - "theme.colorToggle.ariaLabel.mode.light": "Светлый режим", "theme.colorToggle.ariaLabel.mode.dark": "Тёмный режим", + "theme.colorToggle.ariaLabel.mode.light": "Светлый режим", "theme.common.editThisPage": "Отредактировать эту страницу", "theme.common.headingLinkTitle": "Прямая ссылка на этот заголовок", "theme.common.skipToMainContent": "Перейти к основному содержимому", diff --git a/packages/docusaurus-theme-translations/locales/sr/theme-common.json b/packages/docusaurus-theme-translations/locales/sr/theme-common.json index cf1940efbf..294ab67609 100644 --- a/packages/docusaurus-theme-translations/locales/sr/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/sr/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Недавни постови на блогу", "theme.blog.tagTitle": "{nPosts} означени са \"{tagName}\"", "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.light": "light mode", "theme.common.editThisPage": "Уреди ову страницу", "theme.common.headingLinkTitle": "Веза до наслова", "theme.common.skipToMainContent": "Пређи на главни садржај", diff --git a/packages/docusaurus-theme-translations/locales/tr/theme-common.json b/packages/docusaurus-theme-translations/locales/tr/theme-common.json index 76b335687d..b27a78a202 100644 --- a/packages/docusaurus-theme-translations/locales/tr/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/tr/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "Blog son gönderiler navigasyonu", "theme.blog.tagTitle": "\"{tagName}\" ile etiketlenmiş {nPosts}", "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.light": "light mode", "theme.common.editThisPage": "Bu sayfayı düzenle", "theme.common.headingLinkTitle": "Başlığa doğrudan bağlantı", "theme.common.skipToMainContent": "Ana içeriğe geç", diff --git a/packages/docusaurus-theme-translations/locales/vi/theme-common.json b/packages/docusaurus-theme-translations/locales/vi/theme-common.json index 545fcf55d6..f56dc9927c 100644 --- a/packages/docusaurus-theme-translations/locales/vi/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/vi/theme-common.json @@ -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.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.mode.light": "chế độ sáng", "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.headingLinkTitle": "Đường dẫn trực tiếp tới đề mục này", "theme.common.skipToMainContent": "Nhảy tới nội dung", diff --git a/packages/docusaurus-theme-translations/locales/zh-Hans/theme-common.json b/packages/docusaurus-theme-translations/locales/zh-Hans/theme-common.json index eb407517ba..d2579b9efe 100644 --- a/packages/docusaurus-theme-translations/locales/zh-Hans/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/zh-Hans/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "最近博文导航", "theme.blog.tagTitle": "{nPosts} 含有标签「{tagName}」", "theme.colorToggle.ariaLabel": "切换浅色/暗黑模式(当前为{mode})", - "theme.colorToggle.ariaLabel.mode.light": "浅色模式", "theme.colorToggle.ariaLabel.mode.dark": "暗黑模式", + "theme.colorToggle.ariaLabel.mode.light": "浅色模式", "theme.common.editThisPage": "编辑此页", "theme.common.headingLinkTitle": "标题的直接链接", "theme.common.skipToMainContent": "跳到主要内容", diff --git a/packages/docusaurus-theme-translations/locales/zh-Hant/theme-common.json b/packages/docusaurus-theme-translations/locales/zh-Hant/theme-common.json index 732aff68e2..81a5a7d369 100644 --- a/packages/docusaurus-theme-translations/locales/zh-Hant/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/zh-Hant/theme-common.json @@ -26,8 +26,8 @@ "theme.blog.sidebar.navAriaLabel": "最近部落格文章導覽", "theme.blog.tagTitle": "{nPosts} 含有標籤「{tagName}」", "theme.colorToggle.ariaLabel": "切換淺色/暗黑模式(當前為{mode})", - "theme.colorToggle.ariaLabel.mode.light": "淺色模式", "theme.colorToggle.ariaLabel.mode.dark": "暗黑模式", + "theme.colorToggle.ariaLabel.mode.light": "淺色模式", "theme.common.editThisPage": "編輯此頁", "theme.common.headingLinkTitle": "標題的直接連結", "theme.common.skipToMainContent": "跳至主要内容", diff --git a/packages/docusaurus-theme-translations/package.json b/packages/docusaurus-theme-translations/package.json index 98f574e82a..b690f7d485 100644 --- a/packages/docusaurus-theme-translations/package.json +++ b/packages/docusaurus-theme-translations/package.json @@ -14,9 +14,9 @@ }, "license": "MIT", "scripts": { - "build": "tsc", - "watch": "tsc --watch", - "update": "node ./update.js" + "build": "tsc -p tsconfig.build.json", + "watch": "tsc -p tsconfig.build.json --watch", + "update": "node ./update.mjs" }, "dependencies": { "fs-extra": "^10.0.1", diff --git a/packages/docusaurus-theme-translations/src/__tests__/__fixtures__/theme/index.js b/packages/docusaurus-theme-translations/src/__tests__/__fixtures__/theme/index.js new file mode 100644 index 0000000000..80b33a00c3 --- /dev/null +++ b/packages/docusaurus-theme-translations/src/__tests__/__fixtures__/theme/index.js @@ -0,0 +1,5 @@ +import Translate from '@docusaurus/Translate'; + +export default function Foo() { + return {index}; +} diff --git a/packages/docusaurus-theme-translations/src/__tests__/utils.test.ts b/packages/docusaurus-theme-translations/src/__tests__/utils.test.ts new file mode 100644 index 0000000000..72f5cfd839 --- /dev/null +++ b/packages/docusaurus-theme-translations/src/__tests__/utils.test.ts @@ -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 text. + File: packages/docusaurus-theme-translations/src/__tests__/__fixtures__/theme/index.js at line 4 + Full code: {index} + " + `); + }); +}); diff --git a/packages/docusaurus-theme-translations/src/utils.ts b/packages/docusaurus-theme-translations/src/utils.ts new file mode 100644 index 0000000000..78c666db7a --- /dev/null +++ b/packages/docusaurus-theme-translations/src/utils.ts @@ -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 { + // 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; +} diff --git a/packages/docusaurus-theme-translations/tsconfig.build.json b/packages/docusaurus-theme-translations/tsconfig.build.json new file mode 100644 index 0000000000..aee99fc0f3 --- /dev/null +++ b/packages/docusaurus-theme-translations/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "incremental": true, + "tsBuildInfoFile": "./lib/.tsbuildinfo", + "sourceMap": true, + "declarationMap": true, + "rootDir": "src", + "outDir": "lib" + } +} diff --git a/packages/docusaurus-theme-translations/tsconfig.json b/packages/docusaurus-theme-translations/tsconfig.json index aee99fc0f3..59be626a27 100644 --- a/packages/docusaurus-theme-translations/tsconfig.json +++ b/packages/docusaurus-theme-translations/tsconfig.json @@ -1,11 +1,10 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "incremental": true, - "tsBuildInfoFile": "./lib/.tsbuildinfo", - "sourceMap": true, - "declarationMap": true, - "rootDir": "src", - "outDir": "lib" - } + "module": "esnext", + "noEmit": true, + "checkJs": true, + "allowJs": true + }, + "include": ["update.mjs", "src"] } diff --git a/packages/docusaurus-theme-translations/update.d.ts b/packages/docusaurus-theme-translations/update.d.ts deleted file mode 100644 index ff8c770de0..0000000000 --- a/packages/docusaurus-theme-translations/update.d.ts +++ /dev/null @@ -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; diff --git a/packages/docusaurus-theme-translations/update.js b/packages/docusaurus-theme-translations/update.js deleted file mode 100644 index 6576fb182b..0000000000 --- a/packages/docusaurus-theme-translations/update.js +++ /dev/null @@ -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} 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} - */ -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>} - */ -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} 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} */ - 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} 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} */ - 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; diff --git a/packages/docusaurus-theme-translations/update.mjs b/packages/docusaurus-theme-translations/update.mjs new file mode 100644 index 0000000000..3f5cab9603 --- /dev/null +++ b/packages/docusaurus-theme-translations/update.mjs @@ -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} 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>} + */ +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} 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} */ + 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} 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} */ +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')}`; diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 0378d5e4f8..f29f4d521d 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -288,7 +288,7 @@ export interface Plugin { } export type InitializedPlugin = Plugin & { - readonly options: PluginOptions; + readonly options: Required; readonly version: DocusaurusPluginVersionInformation; }; diff --git a/packages/docusaurus-utils-validation/src/__tests__/validationUtils.test.ts b/packages/docusaurus-utils-validation/src/__tests__/validationUtils.test.ts index b67cba1041..d7f0742bfc 100644 --- a/packages/docusaurus-utils-validation/src/__tests__/validationUtils.test.ts +++ b/packages/docusaurus-utils-validation/src/__tests__/validationUtils.test.ts @@ -8,7 +8,107 @@ import {jest} from '@jest/globals'; import Joi from '../Joi'; 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', () => { it('accepts good values', () => { diff --git a/packages/docusaurus-utils-validation/src/validationUtils.ts b/packages/docusaurus-utils-validation/src/validationUtils.ts index 42ee0801b1..4e88d495b3 100644 --- a/packages/docusaurus-utils-validation/src/validationUtils.ts +++ b/packages/docusaurus-utils-validation/src/validationUtils.ts @@ -44,7 +44,7 @@ export function normalizeThemeConfig( schema: Joi.ObjectSchema, themeConfig: Partial, ): 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 // otherwise one theme would fail validating the data of another theme const finalSchema = schema.unknown(); diff --git a/packages/docusaurus-utils/src/__tests__/__snapshots__/markdownParser.test.ts.snap b/packages/docusaurus-utils/src/__tests__/__snapshots__/markdownUtils.test.ts.snap similarity index 100% rename from packages/docusaurus-utils/src/__tests__/__snapshots__/markdownParser.test.ts.snap rename to packages/docusaurus-utils/src/__tests__/__snapshots__/markdownUtils.test.ts.snap diff --git a/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts b/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts similarity index 90% rename from packages/docusaurus-utils/src/__tests__/markdownParser.test.ts rename to packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts index a3de772987..ae1edf89be 100644 --- a/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts +++ b/packages/docusaurus-utils/src/__tests__/markdownUtils.test.ts @@ -10,7 +10,8 @@ import { parseMarkdownContentTitle, parseMarkdownString, parseMarkdownHeadingId, -} from '../markdownParser'; + writeMarkdownHeadingId, +} from '../markdownUtils'; import dedent from 'dedent'; 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); + }); +}); diff --git a/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts b/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts index 178ff254d2..ce067a1489 100644 --- a/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts @@ -15,11 +15,18 @@ import { removeTrailingSlash, resolvePathname, encodePath, + buildSshUrl, + buildHttpsUrl, + hasSSHProtocol, } from '../urlUtils'; describe('normalizeUrl', () => { it('normalizes urls correctly', () => { const asserts = [ + { + input: [], + output: '', + }, { input: ['/', ''], output: '/', @@ -248,3 +255,60 @@ describe('encodePath', () => { 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); + }); +}); diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 9c66f3a9f4..26cb7ae657 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -45,6 +45,9 @@ export { addLeadingSlash, addTrailingSlash, removeTrailingSlash, + hasSSHProtocol, + buildHttpsUrl, + buildSshUrl, } from './urlUtils'; export { type Tag, @@ -60,7 +63,9 @@ export { parseFrontMatter, parseMarkdownContentTitle, parseMarkdownString, -} from './markdownParser'; + writeMarkdownHeadingId, + type WriteHeadingIDOptions, +} from './markdownUtils'; export { type ContentPaths, type BrokenMarkdownLink, diff --git a/packages/docusaurus-utils/src/markdownParser.ts b/packages/docusaurus-utils/src/markdownUtils.ts similarity index 76% rename from packages/docusaurus-utils/src/markdownParser.ts rename to packages/docusaurus-utils/src/markdownUtils.ts index 354461b414..8901c26fcd 100644 --- a/packages/docusaurus-utils/src/markdownParser.ts +++ b/packages/docusaurus-utils/src/markdownUtils.ts @@ -7,6 +7,7 @@ import logger from '@docusaurus/logger'; import matter from 'gray-matter'; +import {createSlugger, type Slugger} from './slugger'; // Input: ## Some heading {#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; } } + +function unwrapMarkdownLinks(line: string): string { + return line.replace(/\[(?[^\]]+)\]\([^)]+\)/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'); +} diff --git a/packages/docusaurus-utils/src/urlUtils.ts b/packages/docusaurus-utils/src/urlUtils.ts index a083a330b8..cf6f8aae0f 100644 --- a/packages/docusaurus-utils/src/urlUtils.ts +++ b/packages/docusaurus-utils/src/urlUtils.ts @@ -154,3 +154,40 @@ export function addTrailingSlash(str: string): string { export function removeTrailingSlash(str: string): string { 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 + } +} diff --git a/packages/docusaurus/bin/beforeCli.mjs b/packages/docusaurus/bin/beforeCli.mjs index 283c8d576b..d2524c3814 100644 --- a/packages/docusaurus/bin/beforeCli.mjs +++ b/packages/docusaurus/bin/beforeCli.mjs @@ -98,9 +98,7 @@ export default async function beforeCli() { .filter((p) => p.startsWith('@docusaurus')) .map((p) => p.concat('@latest')) .join(' '); - const isYarnUsed = await fs.pathExists( - path.resolve(process.cwd(), 'yarn.lock'), - ); + const isYarnUsed = await fs.pathExists(path.resolve('yarn.lock')); const upgradeCommand = isYarnUsed ? `yarn upgrade ${siteDocusaurusPackagesForUpdate}` : `npm i ${siteDocusaurusPackagesForUpdate}`; diff --git a/packages/docusaurus/src/commands/__tests__/deploy.test.ts b/packages/docusaurus/src/commands/__tests__/deploy.test.ts deleted file mode 100644 index a326ba08fa..0000000000 --- a/packages/docusaurus/src/commands/__tests__/deploy.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/packages/docusaurus/src/commands/__tests__/writeHeadingIds.test.ts b/packages/docusaurus/src/commands/__tests__/writeHeadingIds.test.ts deleted file mode 100644 index f8b9df882b..0000000000 --- a/packages/docusaurus/src/commands/__tests__/writeHeadingIds.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/packages/docusaurus/src/commands/deploy.ts b/packages/docusaurus/src/commands/deploy.ts index 6270eb3d29..75a4cd9799 100644 --- a/packages/docusaurus/src/commands/deploy.ts +++ b/packages/docusaurus/src/commands/deploy.ts @@ -8,6 +8,7 @@ import fs from 'fs-extra'; import shell from 'shelljs'; import logger from '@docusaurus/logger'; +import {hasSSHProtocol, buildSshUrl, buildHttpsUrl} from '@docusaurus/utils'; import {loadContext} from '../server'; import build from './build'; 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( siteDir: string, cliOptions: Partial = {}, diff --git a/packages/docusaurus/src/commands/start.ts b/packages/docusaurus/src/commands/start.ts index b1d469faa8..776128478c 100644 --- a/packages/docusaurus/src/commands/start.ts +++ b/packages/docusaurus/src/commands/start.ts @@ -123,7 +123,7 @@ export default async function start( plugins: [ // Generates an `index.html` file with the