From 72eefc7623a083650a15a02d2c4804534686f3b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 May 2024 10:34:25 +0200 Subject: [PATCH 01/11] chore(deps): bump actions/dependency-review-action from 4.3.1 to 4.3.2 (#10112) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index a3cd885d54..984e65a461 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -15,4 +15,4 @@ jobs: - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Dependency Review - uses: actions/dependency-review-action@e58c696e52cac8e62d61cc21fda89565d71505d7 # 4.3.1 + uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # 4.3.2 From 801964dc4929c7827bba5994679c593fa1f1b9db Mon Sep 17 00:00:00 2001 From: Petar_mc <160483025+PetarMc1@users.noreply.github.com> Date: Thu, 9 May 2024 12:34:28 +0300 Subject: [PATCH 02/11] feat(theme-translations): Add Bulgarian default theme translations (bg) (#10111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sébastien Lorber --- .../locales/bg/plugin-ideal-image.json | 7 ++ .../locales/bg/plugin-pwa.json | 5 ++ .../locales/bg/theme-common.json | 74 +++++++++++++++++++ .../locales/bg/theme-live-codeblock.json | 4 + .../locales/bg/theme-search-algolia.json | 35 +++++++++ 5 files changed, 125 insertions(+) create mode 100644 packages/docusaurus-theme-translations/locales/bg/plugin-ideal-image.json create mode 100644 packages/docusaurus-theme-translations/locales/bg/plugin-pwa.json create mode 100644 packages/docusaurus-theme-translations/locales/bg/theme-common.json create mode 100644 packages/docusaurus-theme-translations/locales/bg/theme-live-codeblock.json create mode 100644 packages/docusaurus-theme-translations/locales/bg/theme-search-algolia.json diff --git a/packages/docusaurus-theme-translations/locales/bg/plugin-ideal-image.json b/packages/docusaurus-theme-translations/locales/bg/plugin-ideal-image.json new file mode 100644 index 0000000000..833f1b1306 --- /dev/null +++ b/packages/docusaurus-theme-translations/locales/bg/plugin-ideal-image.json @@ -0,0 +1,7 @@ +{ + "theme.IdealImageMessage.404error": "404. Снимката не е намерена", + "theme.IdealImageMessage.error": "Грешка. Щракнете, за да презаредите.", + "theme.IdealImageMessage.load": "Щракнете, за да заредите {sizeMessage}", + "theme.IdealImageMessage.loading": "Зарежда...", + "theme.IdealImageMessage.offline": "Браузърът Ви е извън обхват. Снимката не е заредена." +} diff --git a/packages/docusaurus-theme-translations/locales/bg/plugin-pwa.json b/packages/docusaurus-theme-translations/locales/bg/plugin-pwa.json new file mode 100644 index 0000000000..e8efb6783a --- /dev/null +++ b/packages/docusaurus-theme-translations/locales/bg/plugin-pwa.json @@ -0,0 +1,5 @@ +{ + "theme.PwaReloadPopup.closeButtonAriaLabel": "Затваряне", + "theme.PwaReloadPopup.info": "Налична е нова версия", + "theme.PwaReloadPopup.refreshButtonText": "Опресняване" +} diff --git a/packages/docusaurus-theme-translations/locales/bg/theme-common.json b/packages/docusaurus-theme-translations/locales/bg/theme-common.json new file mode 100644 index 0000000000..11cf230e1c --- /dev/null +++ b/packages/docusaurus-theme-translations/locales/bg/theme-common.json @@ -0,0 +1,74 @@ +{ + "theme.AnnouncementBar.closeButtonAriaLabel": "Затваряне", + "theme.BackToTopButton.buttonAriaLabel": "Превъртете обратно най-горе", + "theme.CodeBlock.copied": "Копирано", + "theme.CodeBlock.copy": "Копиране", + "theme.CodeBlock.copyButtonAriaLabel": "Копирайте кода в клипборда", + "theme.CodeBlock.wordWrapToggle": "Включване на пренасянето на думи", + "theme.DocSidebarItem.collapseCategoryAriaLabel": "Свиване на категорията '{label}'", + "theme.DocSidebarItem.expandCategoryAriaLabel": "Разширяване на категорията'{label}'", + "theme.ErrorPageContent.title": "Тази страница се срина.", + "theme.ErrorPageContent.tryAgain": "Опитайте отново", + "theme.NavBar.navAriaLabel": "Основен", + "theme.NotFound.p1": "Не успяхме да намерим това, което търсите.", + "theme.NotFound.p2": "Моля, свържете се със собственика на сайта, който ви е свързал с оригиналния URL адрес, и ги уведомете, че връзката им е повредена.", + "theme.NotFound.title": "Страницата не е намерена", + "theme.TOCCollapsible.toggleButtonLabel": "На тази страница", + "theme.admonition.caution": "Внимание", + "theme.admonition.danger": "Опасност", + "theme.admonition.info": "Информация", + "theme.admonition.note": "Бележка", + "theme.admonition.tip": "Съвет", + "theme.admonition.warning": "Внимание", + "theme.blog.archive.description": "Архив", + "theme.blog.archive.title": "Архив", + "theme.blog.paginator.navAriaLabel": "Навигация в страницата със списък на блогове", + "theme.blog.paginator.newerEntries": "По-нови записи", + "theme.blog.paginator.olderEntries": "По-стари записи", + "theme.blog.post.paginator.navAriaLabel": "Навигация на страница с публикации в блогове", + "theme.blog.post.paginator.newerPost": "По-нова публикация", + "theme.blog.post.paginator.olderPost": "По-стар пост", + "theme.blog.post.plurals": "Една публикация|{count} публикации", + "theme.blog.post.readMore": "Прочетете повче", + "theme.blog.post.readMoreLabel": "Прочетете повече за {title}", + "theme.blog.post.readingTime.plurals": "Една минута четене|{readingTime} минути четене", + "theme.blog.sidebar.navAriaLabel": "Навигация в последните публикации в блога", + "theme.blog.tagTitle": "{nPosts} маркиран с/със \"{tagName}\"", + "theme.colorToggle.ariaLabel": "Превключване между тъмен и светъл режим (В момента {mode})", + "theme.colorToggle.ariaLabel.mode.dark": "тъмен режим", + "theme.colorToggle.ariaLabel.mode.light": "светъл режим", + "theme.common.editThisPage": "Редактирай тази страница", + "theme.common.headingLinkTitle": "Директна връзка към {heading}", + "theme.common.skipToMainContent": "Преминете към основното съдържание", + "theme.docs.DocCard.categoryDescription.plurals": "един предмет|{count} предмета", + "theme.docs.breadcrumbs.home": "Начална страница", + "theme.docs.breadcrumbs.navAriaLabel": "Галета", + "theme.docs.paginator.navAriaLabel": "Страници с документaция", + "theme.docs.paginator.next": "Следваща страница", + "theme.docs.paginator.previous": "Предишна страница", + "theme.docs.sidebar.closeSidebarButtonAriaLabel": "Затваряне на лентата за навигация", + "theme.docs.sidebar.collapseButtonAriaLabel": "Свиване на страничната лента", + "theme.docs.sidebar.collapseButtonTitle": "Свиване на страничната лента", + "theme.docs.sidebar.expandButtonAriaLabel": "Разгъване на страничната лентаr", + "theme.docs.sidebar.expandButtonTitle": "Разгъване на страничната лента", + "theme.docs.sidebar.navAriaLabel": "Странична лента на документацията", + "theme.docs.sidebar.toggleSidebarButtonAriaLabel": "Превключване на навигационната лента", + "theme.docs.tagDocListPageTitle": "{nDocsTagged} с/със \"{tagName}\"", + "theme.docs.tagDocListPageTitle.nDocsTagged": "Един документ маркиран|{count} документи маркирани", + "theme.docs.versionBadge.label": "Версия: {versionLabel}", + "theme.docs.versions.latestVersionLinkLabel": "най-новата версия", + "theme.docs.versions.latestVersionSuggestionLabel": "За актуална документация вижте {latestVersionLink} ({versionLabel}).", + "theme.docs.versions.unmaintainedVersionLabel": "Това е документация за {siteTitle} {versionLabel}, който вече не се поддържа активно.", + "theme.docs.versions.unreleasedVersionLabel": "Това е непубликувана документация за версия {versionLabel} на {siteTitle}.", + "theme.lastUpdated.atDate": " на {date}", + "theme.lastUpdated.byUser": " от {user}", + "theme.lastUpdated.lastUpdatedAtBy": "Последно обновено{atDate}{byUser}", + "theme.navbar.mobileLanguageDropdown.label": "Езици", + "theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Назад към главното меню", + "theme.navbar.mobileVersionsDropdown.label": "Версии", + "theme.tags.tagsListLabel": "Етикети:", + "theme.tags.tagsPageLink": "Вижте всички етикети", + "theme.tags.tagsPageTitle": "Етикети", + "theme.unlistedContent.message": "Тази страница е скрита. Търсачките няма да я индексират и само потребители с директна връзка имат достъп до него.", + "theme.unlistedContent.title": "Скрита страница" +} diff --git a/packages/docusaurus-theme-translations/locales/bg/theme-live-codeblock.json b/packages/docusaurus-theme-translations/locales/bg/theme-live-codeblock.json new file mode 100644 index 0000000000..4f4a503fe0 --- /dev/null +++ b/packages/docusaurus-theme-translations/locales/bg/theme-live-codeblock.json @@ -0,0 +1,4 @@ +{ + "theme.Playground.liveEditor": "Live Editor", + "theme.Playground.result": "Result" +} diff --git a/packages/docusaurus-theme-translations/locales/bg/theme-search-algolia.json b/packages/docusaurus-theme-translations/locales/bg/theme-search-algolia.json new file mode 100644 index 0000000000..100008c134 --- /dev/null +++ b/packages/docusaurus-theme-translations/locales/bg/theme-search-algolia.json @@ -0,0 +1,35 @@ +{ + "theme.SearchBar.label": "Търсене", + "theme.SearchBar.seeAll": "Вижте всички {count} резултата", + "theme.SearchModal.errorScreen.helpText": "Може да проверите вашата мрежова връзка.", + "theme.SearchModal.errorScreen.titleText": "Не може да се извлекат резултати", + "theme.SearchModal.footer.closeKeyAriaLabel": "Клавиш Escape", + "theme.SearchModal.footer.closeText": "за затваряне", + "theme.SearchModal.footer.navigateDownKeyAriaLabel": "Стрелка надолу", + "theme.SearchModal.footer.navigateText": "за навигация", + "theme.SearchModal.footer.navigateUpKeyAriaLabel": "Стрелка нагоре", + "theme.SearchModal.footer.searchByText": "Търсене от", + "theme.SearchModal.footer.selectKeyAriaLabel": "Клавиш Enter ", + "theme.SearchModal.footer.selectText": "за избиране", + "theme.SearchModal.noResultsScreen.noResultsText": "Няма резултати за", + "theme.SearchModal.noResultsScreen.reportMissingResultsLinkText": "Информирай ни.", + "theme.SearchModal.noResultsScreen.reportMissingResultsText": "Вярвате, че тази заявка трябва да върне резултати?", + "theme.SearchModal.noResultsScreen.suggestedQueryText": "Опитайте да потърсите", + "theme.SearchModal.placeholder": "Търсене в документацията", + "theme.SearchModal.searchBox.cancelButtonText": "Отказ", + "theme.SearchModal.searchBox.resetButtonTitle": "Изчистване на заявката", + "theme.SearchModal.startScreen.favoriteSearchesTitle": "Любими", + "theme.SearchModal.startScreen.noRecentSearchesText": "Няма скорошни търсения", + "theme.SearchModal.startScreen.recentSearchesTitle": "Скорошни", + "theme.SearchModal.startScreen.removeFavoriteSearchButtonTitle": "Премахване на това търсене от любимите", + "theme.SearchModal.startScreen.removeRecentSearchButtonTitle": "Премахване това търсене от историята", + "theme.SearchModal.startScreen.saveRecentSearchButtonTitle": "Запазване това търсене", + "theme.SearchPage.algoliaLabel": "Търсене от Algolia", + "theme.SearchPage.documentsFound.plurals": "Намерен е един документ|{count} намерени документи", + "theme.SearchPage.emptyResultsTitle": "Потърсете в документацията", + "theme.SearchPage.existingResultsTitle": "Резултати от търсенето за \"{query}\"", + "theme.SearchPage.fetchingNewResults": "звличане на нови резултати...", + "theme.SearchPage.inputLabel": "Търсене", + "theme.SearchPage.inputPlaceholder": "Въведете вашето търсене тук", + "theme.SearchPage.noResultsText": "Не бяха намерени резултати" +} From 02f5d70ed0f404f984afafa640563b800d1f5e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Thu, 9 May 2024 13:42:50 +0200 Subject: [PATCH 03/11] chore: Ignore more playwright console errors (#10117) --- argos/tests/screenshot.spec.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/argos/tests/screenshot.spec.ts b/argos/tests/screenshot.spec.ts index b822715188..b1f6824288 100644 --- a/argos/tests/screenshot.spec.ts +++ b/argos/tests/screenshot.spec.ts @@ -111,10 +111,18 @@ function throwOnConsole(page: Page) { 'Failed to load resource: the server responded with a status of 404 (Not Found)', // TODO looks like a legit hydration bug to fix + // on /blog/releases/2.4 + 'Warning: Prop `%s` did not match. Server: %s Client: %s%s href "/docs" "/docs?docusaurus-theme=light"', + 'Warning: Prop `%s` did not match. Server: %s Client: %s%s href "/docs" "/docs?docusaurus-theme=dark"', + // on /blog/releases/3.0 + 'Warning: Prop `%s` did not match. Server: %s Client: %s%s href "/docs" "/docs?docusaurus-data-navbar=false&docusaurus-data-red-border"', + // on /docs/styling-layout + 'Warning: Prop `%s` did not match. Server: %s Client: %s%s href "/docs" "/docs?docusaurus-data-navbar=false&docusaurus-data-red-border"', 'Warning: Prop `%s` did not match. Server: %s Client: %s%s href "/docs/configuration" "/docs/configuration?docusaurus-theme=light"', + 'Warning: Prop `%s` did not match. Server: %s Client: %s%s href "/docs/configuration" "/docs/configuration?docusaurus-theme=dark"', // TODO weird problem related to KaTeX fonts refusing to decode? - // on http://localhost:3000/docs/markdown-features/math-equations + // on /docs/markdown-features/math-equations 'Failed to decode downloaded font: http://localhost:3000/katex/fonts/', 'OTS parsing error: Failed to convert WOFF 2.0 font to SFNT', ]; From ab9a4e751f3dfd1c4cd5ab70688e4b482d11103b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Thu, 9 May 2024 16:29:07 +0200 Subject: [PATCH 04/11] fix(theme-translations): fix missing pluralization for label DocCard.categoryDescription.plurals (#10118) --- packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx | 2 +- .../locales/base/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/bn/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/cs/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/da/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/de/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/es/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/fil/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/fr/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/he/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/hi/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/hu/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/it/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/nb/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/nl/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/pl/theme-common.json | 2 +- .../locales/pt-BR/theme-common.json | 2 +- .../locales/pt-PT/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/sl/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/sr/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/sv/theme-common.json | 2 +- .../docusaurus-theme-translations/locales/tr/theme-common.json | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx index 54e9def9a3..8348cf8014 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx @@ -32,7 +32,7 @@ function useCategoryItemsPlural() { count, translate( { - message: '{count} items', + message: '1 item|{count} items', id: 'theme.docs.DocCard.categoryDescription.plurals', description: 'The default description for a category card in the generated index about how many items this category includes', diff --git a/packages/docusaurus-theme-translations/locales/base/theme-common.json b/packages/docusaurus-theme-translations/locales/base/theme-common.json index af68335bac..fcaeb33e9a 100644 --- a/packages/docusaurus-theme-translations/locales/base/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/base/theme-common.json @@ -81,7 +81,7 @@ "theme.common.headingLinkTitle___DESCRIPTION": "Title for link to heading", "theme.common.skipToMainContent": "Skip to main content", "theme.common.skipToMainContent___DESCRIPTION": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation", - "theme.docs.DocCard.categoryDescription.plurals": "{count} items", + "theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items", "theme.docs.DocCard.categoryDescription.plurals___DESCRIPTION": "The default description for a category card in the generated index about how many items this category includes", "theme.docs.breadcrumbs.home": "Home page", "theme.docs.breadcrumbs.home___DESCRIPTION": "The ARIA label for the home page in the breadcrumbs", diff --git a/packages/docusaurus-theme-translations/locales/bn/theme-common.json b/packages/docusaurus-theme-translations/locales/bn/theme-common.json index b495a439be..a9f72207ac 100644 --- a/packages/docusaurus-theme-translations/locales/bn/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/bn/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "এই পেজটি এডিট করুন", "theme.common.headingLinkTitle": "{heading} এর সঙ্গে সরাসরি লিংকড", "theme.common.skipToMainContent": "স্কিপ করে মূল কন্টেন্ট এ যান", - "theme.docs.DocCard.categoryDescription.plurals": "{count} items", + "theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items", "theme.docs.breadcrumbs.home": "Home page", "theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs", "theme.docs.paginator.navAriaLabel": "ডক্স পেজ", diff --git a/packages/docusaurus-theme-translations/locales/cs/theme-common.json b/packages/docusaurus-theme-translations/locales/cs/theme-common.json index f1ac6ef1fa..f5859c4236 100644 --- a/packages/docusaurus-theme-translations/locales/cs/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/cs/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Upravit tuto stránku", "theme.common.headingLinkTitle": "Přímý odkaz na {heading}", "theme.common.skipToMainContent": "Přeskočit na hlavní obsah", - "theme.docs.DocCard.categoryDescription.plurals": "{count} items", + "theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items", "theme.docs.breadcrumbs.home": "Home page", "theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs", "theme.docs.paginator.navAriaLabel": "Stránka dokumentace", diff --git a/packages/docusaurus-theme-translations/locales/da/theme-common.json b/packages/docusaurus-theme-translations/locales/da/theme-common.json index 5dedec0af8..6b05c8a653 100644 --- a/packages/docusaurus-theme-translations/locales/da/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/da/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Rediger denne side", "theme.common.headingLinkTitle": "Direkte link til {heading}", "theme.common.skipToMainContent": "Hop til hovedindhold", - "theme.docs.DocCard.categoryDescription.plurals": "{count} items", + "theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items", "theme.docs.breadcrumbs.home": "Home page", "theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs", "theme.docs.paginator.navAriaLabel": "Dokumentside", diff --git a/packages/docusaurus-theme-translations/locales/de/theme-common.json b/packages/docusaurus-theme-translations/locales/de/theme-common.json index 019215a501..d40a928e07 100644 --- a/packages/docusaurus-theme-translations/locales/de/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/de/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Diese Seite bearbeiten", "theme.common.headingLinkTitle": "Direkter Link zur {heading}", "theme.common.skipToMainContent": "Zum Hauptinhalt springen", - "theme.docs.DocCard.categoryDescription.plurals": "{count} Einträge", + "theme.docs.DocCard.categoryDescription.plurals": "1 Eintrag|{count} Einträge", "theme.docs.breadcrumbs.home": "Home page", "theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs", "theme.docs.paginator.navAriaLabel": "Dokumentation Seiten", diff --git a/packages/docusaurus-theme-translations/locales/es/theme-common.json b/packages/docusaurus-theme-translations/locales/es/theme-common.json index a8e4444a6f..f581fbcb7e 100644 --- a/packages/docusaurus-theme-translations/locales/es/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/es/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Editar esta página", "theme.common.headingLinkTitle": "Enlace directo al {heading}", "theme.common.skipToMainContent": "Saltar al contenido principal", - "theme.docs.DocCard.categoryDescription.plurals": "{count} artículos", + "theme.docs.DocCard.categoryDescription.plurals": "1 artículo|{count} artículos", "theme.docs.breadcrumbs.home": "Página de Inicio", "theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs", "theme.docs.paginator.navAriaLabel": "Página del documento", diff --git a/packages/docusaurus-theme-translations/locales/fil/theme-common.json b/packages/docusaurus-theme-translations/locales/fil/theme-common.json index 576179af58..866b297b78 100644 --- a/packages/docusaurus-theme-translations/locales/fil/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/fil/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "I-edit ang page", "theme.common.headingLinkTitle": "Direktang link patungo sa {heading}", "theme.common.skipToMainContent": "Lumaktaw patungo sa pangunahing content", - "theme.docs.DocCard.categoryDescription.plurals": "{count} items", + "theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items", "theme.docs.breadcrumbs.home": "Home page", "theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs", "theme.docs.paginator.navAriaLabel": "Docs Pages", diff --git a/packages/docusaurus-theme-translations/locales/fr/theme-common.json b/packages/docusaurus-theme-translations/locales/fr/theme-common.json index 86ea9aa2d1..e309ee30c9 100644 --- a/packages/docusaurus-theme-translations/locales/fr/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/fr/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Éditer cette page", "theme.common.headingLinkTitle": "Lien direct vers {heading}", "theme.common.skipToMainContent": "Aller au contenu principal", - "theme.docs.DocCard.categoryDescription.plurals": "{count} éléments", + "theme.docs.DocCard.categoryDescription.plurals": "1 élément|{count} éléments", "theme.docs.breadcrumbs.home": "Page d'accueil", "theme.docs.breadcrumbs.navAriaLabel": "Fil d'Ariane", "theme.docs.paginator.navAriaLabel": "Pages de documentation", diff --git a/packages/docusaurus-theme-translations/locales/he/theme-common.json b/packages/docusaurus-theme-translations/locales/he/theme-common.json index 50bba034e7..7571401c64 100644 --- a/packages/docusaurus-theme-translations/locales/he/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/he/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "ערוך דף זה", "theme.common.headingLinkTitle": "קישור ישיר אל {heading}", "theme.common.skipToMainContent": "דלג לתוכן הראשי", - "theme.docs.DocCard.categoryDescription.plurals": "{count} items", + "theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items", "theme.docs.breadcrumbs.home": "Home page", "theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs", "theme.docs.paginator.navAriaLabel": "רשימת דוקומנטאציה", diff --git a/packages/docusaurus-theme-translations/locales/hi/theme-common.json b/packages/docusaurus-theme-translations/locales/hi/theme-common.json index 1d36ea1154..bcace79c48 100644 --- a/packages/docusaurus-theme-translations/locales/hi/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/hi/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "इस पेज को बदलें", "theme.common.headingLinkTitle": "{heading} का सीधा लिंक", "theme.common.skipToMainContent": "मुख्य कंटेंट तक स्किप करें", - "theme.docs.DocCard.categoryDescription.plurals": "{count} items", + "theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items", "theme.docs.breadcrumbs.home": "Home page", "theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs", "theme.docs.paginator.navAriaLabel": "डॉक्स पेज", diff --git a/packages/docusaurus-theme-translations/locales/hu/theme-common.json b/packages/docusaurus-theme-translations/locales/hu/theme-common.json index c98fe3920e..c7ed797705 100644 --- a/packages/docusaurus-theme-translations/locales/hu/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/hu/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Szerkesztés GitHub-on", "theme.common.headingLinkTitle": "Közvetlen hivatkozás erre: {heading}", "theme.common.skipToMainContent": "Ugrás a fő tartalomhoz", - "theme.docs.DocCard.categoryDescription.plurals": "{count} elemek", + "theme.docs.DocCard.categoryDescription.plurals": "1 elem|{count} elemek", "theme.docs.breadcrumbs.home": "Kezdőlap", "theme.docs.breadcrumbs.navAriaLabel": "Navigációs sáv a jelenlegi oldalhoz", "theme.docs.paginator.navAriaLabel": "Dokumentációs oldal", diff --git a/packages/docusaurus-theme-translations/locales/it/theme-common.json b/packages/docusaurus-theme-translations/locales/it/theme-common.json index f969fec7b4..f9a94c9261 100644 --- a/packages/docusaurus-theme-translations/locales/it/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/it/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Modifica questa pagina", "theme.common.headingLinkTitle": "Link diretto a {heading}", "theme.common.skipToMainContent": "Passa al contenuto principale", - "theme.docs.DocCard.categoryDescription.plurals": "{count} elementi", + "theme.docs.DocCard.categoryDescription.plurals": "1 elemento|{count} elementi", "theme.docs.breadcrumbs.home": "Pagina principale", "theme.docs.breadcrumbs.navAriaLabel": "Briciole di pane", "theme.docs.paginator.navAriaLabel": "Pagina del documento", diff --git a/packages/docusaurus-theme-translations/locales/nb/theme-common.json b/packages/docusaurus-theme-translations/locales/nb/theme-common.json index 8854f8f6ab..3a11f6f55a 100644 --- a/packages/docusaurus-theme-translations/locales/nb/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/nb/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Rediger denne siden", "theme.common.headingLinkTitle": "Direkte lenke til {heading}", "theme.common.skipToMainContent": "Gå til hovedinnhold", - "theme.docs.DocCard.categoryDescription.plurals": "{count} artikler", + "theme.docs.DocCard.categoryDescription.plurals": "1 artikkel|{count} artikler", "theme.docs.breadcrumbs.home": "Hjemmeside", "theme.docs.breadcrumbs.navAriaLabel": "Søkvei", "theme.docs.paginator.navAriaLabel": "Dokumenter side", diff --git a/packages/docusaurus-theme-translations/locales/nl/theme-common.json b/packages/docusaurus-theme-translations/locales/nl/theme-common.json index fec11895e2..798d3590c4 100644 --- a/packages/docusaurus-theme-translations/locales/nl/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/nl/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Bewerk deze pagina", "theme.common.headingLinkTitle": "Direct link naar {heading}", "theme.common.skipToMainContent": "Ga naar hoofdinhoud", - "theme.docs.DocCard.categoryDescription.plurals": "{count} artikelen", + "theme.docs.DocCard.categoryDescription.plurals": "1 artikel|{count} artikelen", "theme.docs.breadcrumbs.home": "Homepagina", "theme.docs.breadcrumbs.navAriaLabel": "Broodkruimels", "theme.docs.paginator.navAriaLabel": "Documentatie pagina", diff --git a/packages/docusaurus-theme-translations/locales/pl/theme-common.json b/packages/docusaurus-theme-translations/locales/pl/theme-common.json index 4c31e1b8ac..402235f6a3 100644 --- a/packages/docusaurus-theme-translations/locales/pl/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/pl/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Edytuj tę stronę", "theme.common.headingLinkTitle": "Bezpośredni link do {heading}", "theme.common.skipToMainContent": "Przejdź do głównej zawartości", - "theme.docs.DocCard.categoryDescription.plurals": "{count} elementów", + "theme.docs.DocCard.categoryDescription.plurals": "1 element|{count} elementy|{count} elementów", "theme.docs.breadcrumbs.home": "Strona główna", "theme.docs.breadcrumbs.navAriaLabel": "Pasek nawigacji", "theme.docs.paginator.navAriaLabel": "Strona dokumentacji", 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 28e456d18a..7bcab9cbbb 100644 --- a/packages/docusaurus-theme-translations/locales/pt-BR/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/pt-BR/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Editar essa página", "theme.common.headingLinkTitle": "Link direto para {heading}", "theme.common.skipToMainContent": "Pular para o conteúdo principal", - "theme.docs.DocCard.categoryDescription.plurals": "{count} items", + "theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items", "theme.docs.breadcrumbs.home": "Página Inicial", "theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs", "theme.docs.paginator.navAriaLabel": "Páginas de documentação", 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 0d979546ff..8daa2bead6 100644 --- a/packages/docusaurus-theme-translations/locales/pt-PT/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/pt-PT/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Editar esta página", "theme.common.headingLinkTitle": "Link direto para {heading}", "theme.common.skipToMainContent": "Saltar para o conteúdo principal", - "theme.docs.DocCard.categoryDescription.plurals": "{count} items", + "theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items", "theme.docs.breadcrumbs.home": "Home page", "theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs", "theme.docs.paginator.navAriaLabel": "Páginas de documento", diff --git a/packages/docusaurus-theme-translations/locales/sl/theme-common.json b/packages/docusaurus-theme-translations/locales/sl/theme-common.json index bd3b265baf..e9c8eb0f41 100644 --- a/packages/docusaurus-theme-translations/locales/sl/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/sl/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Uredi to stran", "theme.common.headingLinkTitle": "Direktna povezava na {heading}", "theme.common.skipToMainContent": "Preskoči na vsebino", - "theme.docs.DocCard.categoryDescription.plurals": "{count} vnosov", + "theme.docs.DocCard.categoryDescription.plurals": "1 vnos|2 vnosy|{count} vnosy|{count} vnosov", "theme.docs.breadcrumbs.home": "Domača stran", "theme.docs.breadcrumbs.navAriaLabel": "Drobtine", "theme.docs.paginator.navAriaLabel": "Strani z dokumenti", diff --git a/packages/docusaurus-theme-translations/locales/sr/theme-common.json b/packages/docusaurus-theme-translations/locales/sr/theme-common.json index f29b6f56f2..696e96728c 100644 --- a/packages/docusaurus-theme-translations/locales/sr/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/sr/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Уреди ову страницу", "theme.common.headingLinkTitle": "Веза до {heading}", "theme.common.skipToMainContent": "Пређи на главни садржај", - "theme.docs.DocCard.categoryDescription.plurals": "{count} items", + "theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items", "theme.docs.breadcrumbs.home": "Home page", "theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs", "theme.docs.paginator.navAriaLabel": "странице докумената", diff --git a/packages/docusaurus-theme-translations/locales/sv/theme-common.json b/packages/docusaurus-theme-translations/locales/sv/theme-common.json index 07b0cd9315..c5f0dc3160 100644 --- a/packages/docusaurus-theme-translations/locales/sv/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/sv/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Redigera denna sida", "theme.common.headingLinkTitle": "Direktlänk till {heading}", "theme.common.skipToMainContent": "Hoppa till huvudinnehåll", - "theme.docs.DocCard.categoryDescription.plurals": "{count} artiklar", + "theme.docs.DocCard.categoryDescription.plurals": "1 artikel|{count} artiklar", "theme.docs.breadcrumbs.home": "Hemsida", "theme.docs.breadcrumbs.navAriaLabel": "Sökväg", "theme.docs.paginator.navAriaLabel": "Dokumentsidor", diff --git a/packages/docusaurus-theme-translations/locales/tr/theme-common.json b/packages/docusaurus-theme-translations/locales/tr/theme-common.json index 99f0cd64ff..9d1abe76ef 100644 --- a/packages/docusaurus-theme-translations/locales/tr/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/tr/theme-common.json @@ -40,7 +40,7 @@ "theme.common.editThisPage": "Bu sayfayı düzenle", "theme.common.headingLinkTitle": "{heading} doğrudan bağlantı", "theme.common.skipToMainContent": "Ana içeriğe geç", - "theme.docs.DocCard.categoryDescription.plurals": "{count} öğe", + "theme.docs.DocCard.categoryDescription.plurals": "1 öğe|{count} öğe", "theme.docs.breadcrumbs.home": "Ana sayfa", "theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs", "theme.docs.paginator.navAriaLabel": "Dokümanlar sayfası", From cb6895197d24f9e311f543c1013c0295689f9a7c Mon Sep 17 00:00:00 2001 From: Mikey O'Toole Date: Fri, 10 May 2024 09:15:57 +0100 Subject: [PATCH 05/11] docs: v3 prepare your site blog post should point users to the upgrade guide (#10124) --- .../09-29-preparing-your-site-for-docusaurus-v3/index.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/blog/2023/09-29-preparing-your-site-for-docusaurus-v3/index.mdx b/website/blog/2023/09-29-preparing-your-site-for-docusaurus-v3/index.mdx index cbb7083fe1..740a11eec4 100644 --- a/website/blog/2023/09-29-preparing-your-site-for-docusaurus-v3/index.mdx +++ b/website/blog/2023/09-29-preparing-your-site-for-docusaurus-v3/index.mdx @@ -7,6 +7,12 @@ slug: /preparing-your-site-for-docusaurus-v3 image: ./img/social-card.png --- +:::warning + +This blog post was written when Docusaurus v3 was in beta. There are some changes in dependency versions and upgrade steps you should be aware of if upgrading to Docusaurus v3 current stable releases. Use the [upgrade guide](https://docusaurus.io/docs/next/migration/v3) for the most up-to-date migration steps. + +::: + **Docusaurus v3** is now [**in beta**](https://github.com/facebook/docusaurus/discussions/9312) and the official release is around the corner. This is the perfect time to start **preparing your site** for this new major version. Docusaurus v3 comes with a few **breaking changes**, many of which can be **handled today under Docusaurus v2**. Preparing your site ahead of time can be done incrementally, and will make it easier to upgrade to v3. From 620e46350a57bf1588fb20ce8622a3c871a366de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Fri, 10 May 2024 14:41:51 +0200 Subject: [PATCH 06/11] feat(core): site storage config options (experimental) (#10121) --- .../src/index.d.ts | 7 + .../docusaurus-theme-classic/src/index.ts | 99 +++--- .../src/utils/storageUtils.ts | 12 +- packages/docusaurus-types/src/config.d.ts | 15 + packages/docusaurus-types/src/context.d.ts | 24 ++ packages/docusaurus-types/src/index.d.ts | 3 + .../__snapshots__/config.test.ts.snap | 60 ++++ .../__tests__/__snapshots__/site.test.ts.snap | 10 + .../server/__tests__/configValidation.test.ts | 306 +++++++++++++++++- .../src/server/__tests__/site.test.ts | 4 + .../src/server/__tests__/storage.test.ts | 165 ++++++++++ .../docusaurus/src/server/codegen/codegen.ts | 17 + .../docusaurus/src/server/configValidation.ts | 30 ++ packages/docusaurus/src/server/site.ts | 12 +- packages/docusaurus/src/server/storage.ts | 44 +++ website/_dogfooding/dogfooding.config.ts | 1 + .../_dogfooding/migrateStorageNamespace.ts | 31 ++ website/community/5-release-process.mdx | 6 + website/docs/api/docusaurus.config.js.mdx | 35 ++ website/docusaurus.config.ts | 5 + 20 files changed, 828 insertions(+), 58 deletions(-) create mode 100644 packages/docusaurus/src/server/__tests__/storage.test.ts create mode 100644 packages/docusaurus/src/server/storage.ts create mode 100644 website/_dogfooding/migrateStorageNamespace.ts diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index 1c4fb78360..8da692e648 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -26,6 +26,13 @@ declare module '@generated/site-metadata' { export = siteMetadata; } +declare module '@generated/site-storage' { + import type {SiteStorage} from '@docusaurus/types'; + + const siteStorage: SiteStorage; + export = siteStorage; +} + declare module '@generated/registry' { import type {Registry} from '@docusaurus/types'; diff --git a/packages/docusaurus-theme-classic/src/index.ts b/packages/docusaurus-theme-classic/src/index.ts index 48a80606ac..f366c5a4a3 100644 --- a/packages/docusaurus-theme-classic/src/index.ts +++ b/packages/docusaurus-theme-classic/src/index.ts @@ -10,7 +10,7 @@ import {createRequire} from 'module'; import rtlcss from 'rtlcss'; import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations'; import {getTranslationFiles, translateThemeConfig} from './translations'; -import type {LoadContext, Plugin} from '@docusaurus/types'; +import type {LoadContext, Plugin, SiteStorage} from '@docusaurus/types'; import type {ThemeConfig} from '@docusaurus/theme-common'; import type {Plugin as PostCssPlugin} from 'postcss'; import type {PluginOptions} from '@docusaurus/theme-classic'; @@ -23,58 +23,66 @@ const ContextReplacementPlugin = requireFromDocusaurusCore( 'webpack/lib/ContextReplacementPlugin', ) as typeof webpack.ContextReplacementPlugin; -// Need to be inlined to prevent dark mode FOUC -// Make sure the key is the same as the one in `/theme/hooks/useTheme.js` -const ThemeStorageKey = 'theme'; // Support for ?docusaurus-theme=dark const ThemeQueryStringKey = 'docusaurus-theme'; // Support for ?docusaurus-data-mode=embed&docusaurus-data-myAttr=42 const DataQueryStringPrefixKey = 'docusaurus-data-'; const noFlashColorMode = ({ - defaultMode, - respectPrefersColorScheme, -}: ThemeConfig['colorMode']) => + colorMode: {defaultMode, respectPrefersColorScheme}, + siteStorage, +}: { + colorMode: ThemeConfig['colorMode']; + siteStorage: SiteStorage; +}) => { + // Need to be inlined to prevent dark mode FOUC + // Make sure the key is the same as the one in the color mode React context + // Currently defined in: `docusaurus-theme-common/src/contexts/colorMode.tsx` + const themeStorageKey = `theme${siteStorage.namespace}`; + /* language=js */ - `(function() { - var defaultMode = '${defaultMode}'; - var respectPrefersColorScheme = ${respectPrefersColorScheme}; + return `(function() { + var defaultMode = '${defaultMode}'; + var respectPrefersColorScheme = ${respectPrefersColorScheme}; - function setDataThemeAttribute(theme) { - document.documentElement.setAttribute('data-theme', theme); - } - - function getQueryStringTheme() { - try { - return new URLSearchParams(window.location.search).get('${ThemeQueryStringKey}') - } catch(e) {} - } - - function getStoredTheme() { - try { - return localStorage.getItem('${ThemeStorageKey}'); - } catch (err) {} - } - - var initialTheme = getQueryStringTheme() || getStoredTheme(); - if (initialTheme !== null) { - setDataThemeAttribute(initialTheme); - } else { - if ( - respectPrefersColorScheme && - window.matchMedia('(prefers-color-scheme: dark)').matches - ) { - setDataThemeAttribute('dark'); - } else if ( - respectPrefersColorScheme && - window.matchMedia('(prefers-color-scheme: light)').matches - ) { - setDataThemeAttribute('light'); - } else { - setDataThemeAttribute(defaultMode === 'dark' ? 'dark' : 'light'); + function setDataThemeAttribute(theme) { + document.documentElement.setAttribute('data-theme', theme); } - } -})();`; + + function getQueryStringTheme() { + try { + return new URLSearchParams(window.location.search).get('${ThemeQueryStringKey}') + } catch (e) { + } + } + + function getStoredTheme() { + try { + return window['${siteStorage.type}'].getItem('${themeStorageKey}'); + } catch (err) { + } + } + + var initialTheme = getQueryStringTheme() || getStoredTheme(); + if (initialTheme !== null) { + setDataThemeAttribute(initialTheme); + } else { + if ( + respectPrefersColorScheme && + window.matchMedia('(prefers-color-scheme: dark)').matches + ) { + setDataThemeAttribute('dark'); + } else if ( + respectPrefersColorScheme && + window.matchMedia('(prefers-color-scheme: light)').matches + ) { + setDataThemeAttribute('light'); + } else { + setDataThemeAttribute(defaultMode === 'dark' ? 'dark' : 'light'); + } + } + })();`; +}; /* language=js */ const DataAttributeQueryStringInlineJavaScript = ` @@ -126,6 +134,7 @@ export default function themeClassic( ): Plugin { const { i18n: {currentLocale, localeConfigs}, + siteStorage, } = context; const themeConfig = context.siteConfig.themeConfig as ThemeConfig; const { @@ -218,7 +227,7 @@ export default function themeClassic( { tagName: 'script', innerHTML: ` -${noFlashColorMode(colorMode)} +${noFlashColorMode({colorMode, siteStorage})} ${DataAttributeQueryStringInlineJavaScript} ${announcementBar ? AnnouncementBarInlineJavaScript : ''} `, diff --git a/packages/docusaurus-theme-common/src/utils/storageUtils.ts b/packages/docusaurus-theme-common/src/utils/storageUtils.ts index 680dc1c4f7..a8c8825b22 100644 --- a/packages/docusaurus-theme-common/src/utils/storageUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/storageUtils.ts @@ -6,12 +6,15 @@ */ import {useCallback, useRef, useSyncExternalStore} from 'react'; +import SiteStorage from '@generated/site-storage'; -const StorageTypes = ['localStorage', 'sessionStorage', 'none'] as const; +export type StorageType = (typeof SiteStorage)['type'] | 'none'; -export type StorageType = (typeof StorageTypes)[number]; +const DefaultStorageType: StorageType = SiteStorage.type; -const DefaultStorageType: StorageType = 'localStorage'; +function applyNamespace(storageKey: string): string { + return `${storageKey}${SiteStorage.namespace}`; +} // window.addEventListener('storage') only works for different windows... // so for current window we have to dispatch the event manually @@ -134,9 +137,10 @@ Please only call storage APIs in effects and event handlers.`); * this API can be a no-op. See also https://github.com/facebook/docusaurus/issues/6036 */ export function createStorageSlot( - key: string, + keyInput: string, options?: {persistence?: StorageType}, ): StorageSlot { + const key = applyNamespace(keyInput); if (typeof window === 'undefined') { return createServerStorageSlot(key); } diff --git a/packages/docusaurus-types/src/config.d.ts b/packages/docusaurus-types/src/config.d.ts index 62627281cc..422ce7a574 100644 --- a/packages/docusaurus-types/src/config.d.ts +++ b/packages/docusaurus-types/src/config.d.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import type {SiteStorage} from './context'; import type {RuleSetRule} from 'webpack'; import type {Required as RequireKeys, DeepPartial} from 'utility-types'; import type {I18nConfig} from './i18n'; @@ -115,6 +116,15 @@ export type MarkdownConfig = { anchors: MarkdownAnchorsConfig; }; +export type StorageConfig = { + type: SiteStorage['type']; + namespace: boolean | string; +}; + +export type FutureConfig = { + experimental_storage: StorageConfig; +}; + /** * Docusaurus config, after validation/normalization. */ @@ -171,6 +181,11 @@ export type DocusaurusConfig = { * @see https://docusaurus.io/docs/api/docusaurus-config#i18n */ i18n: I18nConfig; + /** + * Docusaurus future flags and experimental features. + * Similar to Remix future flags, see https://remix.run/blog/future-flags + */ + future: FutureConfig; /** * This option adds `` to * every page to tell search engines to avoid indexing your site. diff --git a/packages/docusaurus-types/src/context.d.ts b/packages/docusaurus-types/src/context.d.ts index 68c4f78d6e..e399c53421 100644 --- a/packages/docusaurus-types/src/context.d.ts +++ b/packages/docusaurus-types/src/context.d.ts @@ -27,6 +27,25 @@ export type SiteMetadata = { readonly pluginVersions: {[pluginName: string]: PluginVersionInformation}; }; +export type SiteStorage = { + /** + * Which browser storage do you want to use? + * Between "localStorage" and "sessionStorage". + * The default is "localStorage". + */ + type: 'localStorage' | 'sessionStorage'; + + /** + * Applies a namespace to the theme storage key + * For readability, the namespace is applied at the end of the key + * The final storage key will be = `${key}${namespace}` + * + * The default namespace is "" for retro-compatibility reasons + * If you want a separator, the namespace should contain it ("-myNamespace") + */ + namespace: string; +}; + export type GlobalData = {[pluginName: string]: {[pluginId: string]: unknown}}; export type LoadContext = { @@ -50,6 +69,11 @@ export type LoadContext = { baseUrl: string; i18n: I18n; codeTranslations: CodeTranslations; + + /** + * Defines the default browser storage behavior for a site + */ + siteStorage: SiteStorage; }; export type Props = LoadContext & { diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index a674464101..1504e6d034 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -12,6 +12,8 @@ export { DefaultParseFrontMatter, ParseFrontMatter, DocusaurusConfig, + FutureConfig, + StorageConfig, Config, } from './config'; @@ -20,6 +22,7 @@ export { DocusaurusContext, GlobalData, LoadContext, + SiteStorage, Props, } from './context'; diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap index 72d6a58aa5..47a2827d2d 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap @@ -7,6 +7,12 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = ` "baseUrlIssueBanner": true, "clientModules": [], "customFields": {}, + "future": { + "experimental_storage": { + "namespace": false, + "type": "localStorage", + }, + }, "headTags": [], "i18n": { "defaultLocale": "en", @@ -61,6 +67,12 @@ exports[`loadSiteConfig website with ts + js config 1`] = ` "baseUrlIssueBanner": true, "clientModules": [], "customFields": {}, + "future": { + "experimental_storage": { + "namespace": false, + "type": "localStorage", + }, + }, "headTags": [], "i18n": { "defaultLocale": "en", @@ -115,6 +127,12 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = ` "baseUrlIssueBanner": true, "clientModules": [], "customFields": {}, + "future": { + "experimental_storage": { + "namespace": false, + "type": "localStorage", + }, + }, "headTags": [], "i18n": { "defaultLocale": "en", @@ -169,6 +187,12 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = ` "baseUrlIssueBanner": true, "clientModules": [], "customFields": {}, + "future": { + "experimental_storage": { + "namespace": false, + "type": "localStorage", + }, + }, "headTags": [], "i18n": { "defaultLocale": "en", @@ -223,6 +247,12 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = ` "baseUrlIssueBanner": true, "clientModules": [], "customFields": {}, + "future": { + "experimental_storage": { + "namespace": false, + "type": "localStorage", + }, + }, "headTags": [], "i18n": { "defaultLocale": "en", @@ -277,6 +307,12 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = ` "baseUrlIssueBanner": true, "clientModules": [], "customFields": {}, + "future": { + "experimental_storage": { + "namespace": false, + "type": "localStorage", + }, + }, "headTags": [], "i18n": { "defaultLocale": "en", @@ -331,6 +367,12 @@ exports[`loadSiteConfig website with valid async config 1`] = ` "baseUrlIssueBanner": true, "clientModules": [], "customFields": {}, + "future": { + "experimental_storage": { + "namespace": false, + "type": "localStorage", + }, + }, "headTags": [], "i18n": { "defaultLocale": "en", @@ -387,6 +429,12 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = ` "baseUrlIssueBanner": true, "clientModules": [], "customFields": {}, + "future": { + "experimental_storage": { + "namespace": false, + "type": "localStorage", + }, + }, "headTags": [], "i18n": { "defaultLocale": "en", @@ -443,6 +491,12 @@ exports[`loadSiteConfig website with valid config creator function 1`] = ` "baseUrlIssueBanner": true, "clientModules": [], "customFields": {}, + "future": { + "experimental_storage": { + "namespace": false, + "type": "localStorage", + }, + }, "headTags": [], "i18n": { "defaultLocale": "en", @@ -502,6 +556,12 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = ` ], "customFields": {}, "favicon": "img/docusaurus.ico", + "future": { + "experimental_storage": { + "namespace": false, + "type": "localStorage", + }, + }, "headTags": [], "i18n": { "defaultLocale": "en", diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap index c2562812b7..a7837a67e1 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap @@ -77,6 +77,12 @@ exports[`load loads props for site with custom i18n path 1`] = ` "baseUrlIssueBanner": true, "clientModules": [], "customFields": {}, + "future": { + "experimental_storage": { + "namespace": false, + "type": "localStorage", + }, + }, "headTags": [], "i18n": { "defaultLocale": "en", @@ -137,6 +143,10 @@ exports[`load loads props for site with custom i18n path 1`] = ` "pluginVersions": {}, "siteVersion": undefined, }, + "siteStorage": { + "namespace": "", + "type": "localStorage", + }, "siteVersion": undefined, } `; diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index c20ff64314..cf7fee3269 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -8,8 +8,10 @@ import { ConfigSchema, DEFAULT_CONFIG, + DEFAULT_STORAGE_CONFIG, validateConfig, } from '../configValidation'; +import type {StorageConfig} from '@docusaurus/types/src/config'; import type {Config, DocusaurusConfig} from '@docusaurus/types'; import type {DeepPartial} from 'utility-types'; @@ -35,6 +37,12 @@ describe('normalizeConfig', () => { const userConfig: Config = { ...DEFAULT_CONFIG, ...baseConfig, + future: { + experimental_storage: { + type: 'sessionStorage', + namespace: true, + }, + }, tagline: 'my awesome site', organizationName: 'facebook', projectName: 'docusaurus', @@ -588,12 +596,8 @@ describe('markdown', () => { }); it('throw for bad markdown format', () => { - expect(() => - normalizeConfig( - // @ts-expect-error: bad value - {markdown: {format: null}}, - ), - ).toThrowErrorMatchingInlineSnapshot(` + expect(() => normalizeConfig({markdown: {format: null}})) + .toThrowErrorMatchingInlineSnapshot(` ""markdown.format" must be one of [mdx, md, detect] "markdown.format" must be a string " @@ -612,7 +616,6 @@ describe('markdown', () => { it('throw for null object', () => { expect(() => { normalizeConfig({ - // @ts-expect-error: test markdown: null, }); }).toThrowErrorMatchingInlineSnapshot(` @@ -621,3 +624,292 @@ describe('markdown', () => { `); }); }); + +describe('future', () => { + it('accepts future - undefined', () => { + expect( + normalizeConfig({ + future: undefined, + }), + ).toEqual(expect.objectContaining({future: DEFAULT_CONFIG.future})); + }); + + it('accepts future - empty', () => { + expect( + normalizeConfig({ + future: {}, + }), + ).toEqual(expect.objectContaining({future: DEFAULT_CONFIG.future})); + }); + + it('accepts future', () => { + const future: DocusaurusConfig['future'] = { + experimental_storage: { + type: 'sessionStorage', + namespace: 'myNamespace', + }, + }; + expect( + normalizeConfig({ + future, + }), + ).toEqual(expect.objectContaining({future})); + }); + + it('rejects future - unknown key', () => { + const future: DocusaurusConfig['future'] = { + // @ts-expect-error: invalid + doesNotExistKey: { + type: 'sessionStorage', + namespace: 'myNamespace', + }, + }; + expect(() => + normalizeConfig({ + future, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "These field(s) ("future.doesNotExistKey",) are not recognized in docusaurus.config.js. + If you still want these fields to be in your configuration, put them in the "customFields" field. + See https://docusaurus.io/docs/api/docusaurus-config/#customfields" + `); + }); + + describe('storage', () => { + it('accepts storage - undefined', () => { + expect( + normalizeConfig({ + future: { + experimental_storage: undefined, + }, + }), + ).toEqual(expect.objectContaining({future: DEFAULT_CONFIG.future})); + }); + + it('accepts storage - empty', () => { + expect( + normalizeConfig({ + future: {experimental_storage: {}}, + }), + ).toEqual(expect.objectContaining({future: DEFAULT_CONFIG.future})); + }); + + it('accepts storage - full', () => { + const storage: StorageConfig = { + type: 'sessionStorage', + namespace: 'myNamespace', + }; + expect( + normalizeConfig({ + future: { + experimental_storage: storage, + }, + }), + ).toEqual( + expect.objectContaining({ + future: { + experimental_storage: storage, + }, + }), + ); + }); + + it('rejects storage - boolean', () => { + // @ts-expect-error: invalid + const storage: Partial = true; + expect(() => + normalizeConfig({ + future: { + experimental_storage: storage, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""future.experimental_storage" must be of type object + " + `); + }); + + it('rejects storage - number', () => { + // @ts-expect-error: invalid + const storage: Partial = 42; + expect(() => + normalizeConfig({ + future: { + experimental_storage: storage, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""future.experimental_storage" must be of type object + " + `); + }); + + describe('type', () => { + it('accepts type', () => { + const storage: Partial = { + type: 'sessionStorage', + }; + expect( + normalizeConfig({ + future: { + experimental_storage: storage, + }, + }), + ).toEqual( + expect.objectContaining({ + future: { + experimental_storage: { + ...DEFAULT_STORAGE_CONFIG, + ...storage, + }, + }, + }), + ); + }); + + it('accepts type - undefined', () => { + const storage: Partial = { + type: undefined, + }; + expect( + normalizeConfig({ + future: { + experimental_storage: storage, + }, + }), + ).toEqual( + expect.objectContaining({ + future: { + experimental_storage: { + ...DEFAULT_STORAGE_CONFIG, + type: 'localStorage', + }, + }, + }), + ); + }); + + it('rejects type - null', () => { + // @ts-expect-error: invalid + const storage: Partial = {type: 42}; + expect(() => + normalizeConfig({ + future: { + experimental_storage: storage, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""future.experimental_storage.type" must be one of [localStorage, sessionStorage] + "future.experimental_storage.type" must be a string + " + `); + }); + + it('rejects type - number', () => { + // @ts-expect-error: invalid + const storage: Partial = {type: 42}; + expect(() => + normalizeConfig({ + future: { + experimental_storage: storage, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""future.experimental_storage.type" must be one of [localStorage, sessionStorage] + "future.experimental_storage.type" must be a string + " + `); + }); + + it('rejects type - invalid enum value', () => { + // @ts-expect-error: invalid + const storage: Partial = {type: 'badType'}; + expect(() => + normalizeConfig({ + future: { + experimental_storage: storage, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""future.experimental_storage.type" must be one of [localStorage, sessionStorage] + " + `); + }); + }); + + describe('namespace', () => { + it('accepts namespace - boolean', () => { + const storage: Partial = { + namespace: true, + }; + expect( + normalizeConfig({ + future: { + experimental_storage: storage, + }, + }), + ).toEqual( + expect.objectContaining({ + future: { + experimental_storage: { + ...DEFAULT_STORAGE_CONFIG, + ...storage, + }, + }, + }), + ); + }); + + it('accepts namespace - string', () => { + const storage: Partial = { + namespace: 'myNamespace', + }; + expect( + normalizeConfig({ + future: { + experimental_storage: storage, + }, + }), + ).toEqual( + expect.objectContaining({ + future: { + experimental_storage: { + ...DEFAULT_STORAGE_CONFIG, + ...storage, + }, + }, + }), + ); + }); + + it('rejects namespace - null', () => { + const storage: Partial = {namespace: null}; + expect(() => + normalizeConfig({ + future: { + experimental_storage: storage, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""future.experimental_storage.namespace" must be one of [string, boolean] + " + `); + }); + + it('rejects namespace - number', () => { + // @ts-expect-error: invalid + const storage: Partial = {namespace: 42}; + expect(() => + normalizeConfig({ + future: { + experimental_storage: storage, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""future.experimental_storage.namespace" must be one of [string, boolean] + " + `); + }); + }); + }); +}); diff --git a/packages/docusaurus/src/server/__tests__/site.test.ts b/packages/docusaurus/src/server/__tests__/site.test.ts index 235060c3e4..dc6027c780 100644 --- a/packages/docusaurus/src/server/__tests__/site.test.ts +++ b/packages/docusaurus/src/server/__tests__/site.test.ts @@ -38,6 +38,10 @@ describe('load', () => { siteConfig: { baseUrl: '/zh-Hans/', }, + siteStorage: { + namespace: '', + type: 'localStorage', + }, plugins: site2.props.plugins, }), ); diff --git a/packages/docusaurus/src/server/__tests__/storage.test.ts b/packages/docusaurus/src/server/__tests__/storage.test.ts new file mode 100644 index 0000000000..f91b9ddbef --- /dev/null +++ b/packages/docusaurus/src/server/__tests__/storage.test.ts @@ -0,0 +1,165 @@ +/** + * 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 {createSiteStorage} from '../storage'; +import { + DEFAULT_FUTURE_CONFIG, + DEFAULT_STORAGE_CONFIG, +} from '../configValidation'; +import type {FutureConfig, StorageConfig, SiteStorage} from '@docusaurus/types'; + +function test({ + url = 'https://docusaurus.io', + baseUrl = '/', + storage = {}, +}: { + url?: string; + baseUrl?: string; + storage?: Partial; +}): SiteStorage { + const future: FutureConfig = { + ...DEFAULT_FUTURE_CONFIG, + experimental_storage: { + ...DEFAULT_STORAGE_CONFIG, + ...storage, + }, + }; + + return createSiteStorage({url, baseUrl, future}); +} + +const DefaultSiteStorage: SiteStorage = { + type: 'localStorage', + namespace: '', +}; + +describe('storage', () => { + it('default', () => { + expect(test({})).toEqual(DefaultSiteStorage); + }); + + describe('type', () => { + it('localStorage', () => { + expect(test({storage: {type: 'localStorage'}})).toEqual({ + ...DefaultSiteStorage, + type: 'localStorage', + }); + }); + + it('sessionStorage', () => { + expect(test({storage: {type: 'sessionStorage'}})).toEqual({ + ...DefaultSiteStorage, + type: 'sessionStorage', + }); + }); + }); + + describe('namespace', () => { + describe('true', () => { + function testAutomaticNamespace( + { + url, + baseUrl, + }: { + url: string; + baseUrl: string; + }, + expectedNamespace: string, + ) { + return expect(test({url, baseUrl, storage: {namespace: true}})).toEqual( + expect.objectContaining({namespace: expectedNamespace}), + ); + } + + it('automatic namespace - https://docusaurus.io/', () => { + testAutomaticNamespace( + { + url: 'https://docusaurus.io', + baseUrl: '/', + }, + '-189', + ); + }); + + it('automatic namespace - https://docusaurus.io/baseUrl/', () => { + testAutomaticNamespace( + { + url: 'https://docusaurus.io', + baseUrl: '/baseUrl/', + }, + '-b21', + ); + }); + + it('automatic namespace - https://example.com/', () => { + testAutomaticNamespace( + { + url: 'https://example.com', + baseUrl: '/', + }, + '-182', + ); + }); + + it('automatic namespace - https://example.com/baseUrl/', () => { + testAutomaticNamespace( + { + url: 'https://example.com', + baseUrl: '/baseUrl/', + }, + '-ad6', + ); + }); + + it('automatic namespace - is not slash sensitive', () => { + const expectedNamespace = '-b21'; + testAutomaticNamespace( + { + url: 'https://docusaurus.io', + baseUrl: '/baseUrl/', + }, + expectedNamespace, + ); + testAutomaticNamespace( + { + url: 'https://docusaurus.io/', + baseUrl: '/baseUrl/', + }, + expectedNamespace, + ); + testAutomaticNamespace( + { + url: 'https://docusaurus.io/', + baseUrl: '/baseUrl', + }, + expectedNamespace, + ); + testAutomaticNamespace( + { + url: 'https://docusaurus.io', + baseUrl: 'baseUrl', + }, + expectedNamespace, + ); + }); + }); + + it('false', () => { + expect(test({storage: {namespace: false}})).toEqual({ + ...DefaultSiteStorage, + namespace: '', + }); + }); + + it('string', () => { + expect(test({storage: {namespace: 'my-namespace'}})).toEqual({ + ...DefaultSiteStorage, + namespace: '-my-namespace', + }); + }); + }); +}); diff --git a/packages/docusaurus/src/server/codegen/codegen.ts b/packages/docusaurus/src/server/codegen/codegen.ts index d6df2de34e..cc8d458a66 100644 --- a/packages/docusaurus/src/server/codegen/codegen.ts +++ b/packages/docusaurus/src/server/codegen/codegen.ts @@ -18,6 +18,7 @@ import type { I18n, PluginRouteConfig, SiteMetadata, + SiteStorage, } from '@docusaurus/types'; function genWarning({generatedFilesDir}: {generatedFilesDir: string}) { @@ -131,6 +132,20 @@ function genSiteMetadata({ ); } +function genSiteStorage({ + generatedFilesDir, + siteStorage, +}: { + generatedFilesDir: string; + siteStorage: SiteStorage; +}) { + return generate( + generatedFilesDir, + 'site-storage.json', + JSON.stringify(siteStorage, null, 2), + ); +} + type CodegenParams = { generatedFilesDir: string; siteConfig: DocusaurusConfig; @@ -140,6 +155,7 @@ type CodegenParams = { i18n: I18n; codeTranslations: CodeTranslations; siteMetadata: SiteMetadata; + siteStorage: SiteStorage; routes: PluginRouteConfig[]; }; @@ -151,6 +167,7 @@ export async function generateSiteFiles(params: CodegenParams): Promise { generateRouteFiles(params), genGlobalData(params), genSiteMetadata(params), + genSiteStorage(params), genI18n(params), genCodeTranslations(params), ]); diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 3f6fac5fb1..bf581897a4 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -16,6 +16,7 @@ import { addLeadingSlash, removeTrailingSlash, } from '@docusaurus/utils-common'; +import type {FutureConfig, StorageConfig} from '@docusaurus/types/src/config'; import type { DocusaurusConfig, I18nConfig, @@ -31,6 +32,15 @@ export const DEFAULT_I18N_CONFIG: I18nConfig = { localeConfigs: {}, }; +export const DEFAULT_STORAGE_CONFIG: StorageConfig = { + type: 'localStorage', + namespace: false, +}; + +export const DEFAULT_FUTURE_CONFIG: FutureConfig = { + experimental_storage: DEFAULT_STORAGE_CONFIG, +}; + export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = { format: 'mdx', // TODO change this to "detect" in Docusaurus v4? mermaid: false, @@ -50,6 +60,7 @@ export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = { export const DEFAULT_CONFIG: Pick< DocusaurusConfig, | 'i18n' + | 'future' | 'onBrokenLinks' | 'onBrokenAnchors' | 'onBrokenMarkdownLinks' @@ -71,6 +82,7 @@ export const DEFAULT_CONFIG: Pick< | 'markdown' > = { i18n: DEFAULT_I18N_CONFIG, + future: DEFAULT_FUTURE_CONFIG, onBrokenLinks: 'throw', onBrokenAnchors: 'warn', // TODO Docusaurus v4: change to throw onBrokenMarkdownLinks: 'warn', @@ -181,6 +193,23 @@ const I18N_CONFIG_SCHEMA = Joi.object({ .optional() .default(DEFAULT_I18N_CONFIG); +const STORAGE_CONFIG_SCHEMA = Joi.object({ + type: Joi.string() + .equal('localStorage', 'sessionStorage') + .default(DEFAULT_STORAGE_CONFIG.type), + namespace: Joi.alternatives() + .try(Joi.string(), Joi.boolean()) + .default(DEFAULT_STORAGE_CONFIG.namespace), +}) + .optional() + .default(DEFAULT_STORAGE_CONFIG); + +const FUTURE_CONFIG_SCHEMA = Joi.object({ + experimental_storage: STORAGE_CONFIG_SCHEMA, +}) + .optional() + .default(DEFAULT_FUTURE_CONFIG); + const SiteUrlSchema = Joi.string() .required() .custom((value: string, helpers) => { @@ -215,6 +244,7 @@ export const ConfigSchema = Joi.object({ url: SiteUrlSchema, trailingSlash: Joi.boolean(), // No default value! undefined = retrocompatible legacy behavior! i18n: I18N_CONFIG_SCHEMA, + future: FUTURE_CONFIG_SCHEMA, onBrokenLinks: Joi.string() .equal('ignore', 'log', 'warn', 'throw') .default(DEFAULT_CONFIG.onBrokenLinks), diff --git a/packages/docusaurus/src/server/site.ts b/packages/docusaurus/src/server/site.ts index 55dcb743c8..19e18b3f17 100644 --- a/packages/docusaurus/src/server/site.ts +++ b/packages/docusaurus/src/server/site.ts @@ -25,6 +25,7 @@ import { import {PerfLogger} from '../utils'; import {generateSiteFiles} from './codegen/codegen'; import {getRoutesPaths, handleDuplicateRoutes} from './routes'; +import {createSiteStorage} from './storage'; import type {LoadPluginsResult} from './plugins/plugins'; import type { DocusaurusConfig, @@ -111,9 +112,12 @@ export async function loadContext( const codeTranslations = await loadSiteCodeTranslations({localizationDir}); + const siteStorage = createSiteStorage(siteConfig); + return { siteDir, siteVersion, + siteStorage, generatedFilesDir, localizationDir, siteConfig, @@ -135,6 +139,7 @@ function createSiteProps( siteVersion, siteConfig, siteConfigPath, + siteStorage, outDir, baseUrl, i18n, @@ -159,6 +164,7 @@ function createSiteProps( siteConfigPath, siteMetadata, siteVersion, + siteStorage, siteDir, outDir, baseUrl, @@ -190,6 +196,7 @@ async function createSiteFiles({ generatedFilesDir, siteConfig, siteMetadata, + siteStorage, i18n, codeTranslations, routes, @@ -202,6 +209,7 @@ async function createSiteFiles({ clientModules, siteConfig, siteMetadata, + siteStorage, i18n, codeTranslations, globalData, @@ -224,7 +232,7 @@ export async function loadSite(params: LoadContextParams): Promise { const {plugins, routes, globalData} = await loadPlugins(context); - const props = await createSiteProps({plugins, routes, globalData, context}); + const props = createSiteProps({plugins, routes, globalData, context}); const site: Site = {props, params}; @@ -253,7 +261,7 @@ export async function reloadSitePlugin( context: site.props, }); - const newProps = await createSiteProps({ + const newProps = createSiteProps({ plugins, routes, globalData, diff --git a/packages/docusaurus/src/server/storage.ts b/packages/docusaurus/src/server/storage.ts new file mode 100644 index 0000000000..657f4a359c --- /dev/null +++ b/packages/docusaurus/src/server/storage.ts @@ -0,0 +1,44 @@ +/** + * 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 {normalizeUrl, simpleHash} from '@docusaurus/utils'; +import {addTrailingSlash} from '@docusaurus/utils-common'; +import type {DocusaurusConfig, SiteStorage} from '@docusaurus/types'; + +type PartialFuture = Pick; + +type PartialConfig = Pick & { + future: PartialFuture; +}; + +function automaticNamespace(config: PartialConfig): string { + const normalizedUrl = addTrailingSlash( + normalizeUrl([config.url, config.baseUrl]), + ); + return simpleHash(normalizedUrl, 3); +} + +function getNamespaceString(config: PartialConfig): string | null { + if (config.future.experimental_storage.namespace === true) { + return automaticNamespace(config); + } else if (config.future.experimental_storage.namespace === false) { + return null; + } else { + return config.future.experimental_storage.namespace; + } +} + +export function createSiteStorage(config: PartialConfig): SiteStorage { + const {type} = config.future.experimental_storage; + const namespaceString = getNamespaceString(config); + + const namespace = namespaceString ? `-${namespaceString}` : ''; + return { + type, + namespace, + }; +} diff --git a/website/_dogfooding/dogfooding.config.ts b/website/_dogfooding/dogfooding.config.ts index ebb6b22885..7bcc7babea 100644 --- a/website/_dogfooding/dogfooding.config.ts +++ b/website/_dogfooding/dogfooding.config.ts @@ -104,6 +104,7 @@ export const dogfoodingPluginInstances: PluginConfig[] = [ return [ require.resolve('./clientModuleExample.ts'), require.resolve('./clientModuleCSS.css'), + require.resolve('./migrateStorageNamespace.ts'), ]; }, }; diff --git a/website/_dogfooding/migrateStorageNamespace.ts b/website/_dogfooding/migrateStorageNamespace.ts new file mode 100644 index 0000000000..b0d7d81e13 --- /dev/null +++ b/website/_dogfooding/migrateStorageNamespace.ts @@ -0,0 +1,31 @@ +/** + * 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 ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import SiteStorage from '@generated/site-storage'; + +// The purpose is to test a migration script for storage namespacing +// See also: https://github.com/facebook/docusaurus/pull/10121 + +if (ExecutionEnvironment.canUseDOM) { + const migrateStorageKey = (key: string) => { + const value = localStorage.getItem(key); + if (value !== null && SiteStorage.namespace) { + const newKey = `${key}${SiteStorage.namespace}`; + console.log(`Updating storage key [${key} => ${newKey}], value=${value}`); + localStorage.setItem(newKey, value); + localStorage.removeItem(key); + } + }; + + const storageKeys = [ + 'theme', + 'docusaurus.announcement.id', + 'docusaurus.announcement.dismiss', + 'docs-preferred-version-default', + ]; + storageKeys.forEach(migrateStorageKey); +} diff --git a/website/community/5-release-process.mdx b/website/community/5-release-process.mdx index 8185f7a4d3..fae6a1a046 100644 --- a/website/community/5-release-process.mdx +++ b/website/community/5-release-process.mdx @@ -156,6 +156,12 @@ We will outline what accounts as the public API surface. - `@docusaurus/types` TypeScript types - We still retain the freedom to make types stricter (which may break type-checking). +❌ Our public API **excludes**: + +- Docusaurus config `future` +- All features prefixed by `experimental_` or `unstable_` +- All features prefixed by `v_` (`v6_` `v7_`, etc.) + :::tip For non-theme APIs, any documented API is considered public (and will be stable); any undocumented API is considered internal. diff --git a/website/docs/api/docusaurus.config.js.mdx b/website/docs/api/docusaurus.config.js.mdx index 572a2bc6d1..295774c412 100644 --- a/website/docs/api/docusaurus.config.js.mdx +++ b/website/docs/api/docusaurus.config.js.mdx @@ -174,6 +174,41 @@ export default { - `calendar`: the [calendar](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar) used to calculate the date era. Note that it doesn't control the actual string displayed: `MM/DD/YYYY` and `DD/MM/YYYY` are both `gregory`. To choose the format (`DD/MM/YYYY` or `MM/DD/YYYY`), set your locale name to `en-GB` or `en-US` (`en` means `en-US`). - `path`: Root folder that all plugin localization folders of this locale are relative to. Will be resolved against `i18n.path`. Defaults to the locale's name. Note: this has no effect on the locale's `baseUrl`—customization of base URL is a work-in-progress. +### `future` {#future} + +- Type: `Object` + +The `future` configuration object permits to opt-in for upcoming/unstable/experimental Docusaurus features that are not ready for prime time. + +It is also a way to opt-in for upcoming breaking changes coming in the next major versions, enabling you to prepare your site for the next version while staying on the previous one. The [Remix Future Flags blog post](https://remix.run/blog/future-flags) greatly explains this idea. + +:::danger Breaking changes in minor versions + +Features prefixed by `experimental_` or `unstable_` are subject to changes in **minor versions**, and not considered as [Semantic Versioning breaking changes](/community/release-process). + +Features prefixed by `v_` (`v6_` `v7_`, etc.) are future flags that are expected to be turned on by default in the next major versions. These are less likely to change, but we keep the possibility to do so. + +`future` API breaking changes should be easy to handle, and will be documented in minor/major version blog posts. + +::: + +Example: + +```js title="docusaurus.config.js" +export default { + future: { + experimental_storage: { + type: 'localStorage', + namespace: true, + }, + }, +}; +``` + +- `experimental_storage`: Site-wide browser storage options that theme authors should strive to respect. + - `type`: The browser storage theme authors should use. Possible values are `localStorage` and `sessionStorage`. Defaults to `localStorage`. + - `namespace`: Whether to namespace the browser storage keys to avoid storage key conflicts when Docusaurus sites are hosted under the same domain, or on localhost. Possible values are `string | boolean`. The namespace is appended at the end of the storage keys `key-namespace`. Use `true` to automatically generate a random namespace from your site `url + baseUrl`. Defaults to `false` (no namespace, historical behavior). + ### `noIndex` {#noIndex} - Type: `boolean` diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index c63bd9aa3c..aae27b1a4d 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -147,6 +147,11 @@ export default async function createConfigAsync() { baseUrl, baseUrlIssueBanner: true, url: 'https://docusaurus.io', + future: { + experimental_storage: { + namespace: true, + }, + }, // Dogfood both settings: // - force trailing slashes for deploy previews // - avoid trailing slashes in prod From e48b7818af44950728088ce334d7eadbbec2bca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Fri, 10 May 2024 15:03:35 +0200 Subject: [PATCH 07/11] chore: fix v3.3 blog post (#10128) --- website/blog/releases/3.3/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/blog/releases/3.3/index.mdx b/website/blog/releases/3.3/index.mdx index d4858687ef..88907a9d90 100644 --- a/website/blog/releases/3.3/index.mdx +++ b/website/blog/releases/3.3/index.mdx @@ -21,7 +21,7 @@ import IframeWindow from '@site/src/components/BrowserWindow/IframeWindow'; ### Prepare for React 19 -The React core team recently [released the first **React 19 beta**](https://react.dev/blog/2024/04/25/react-19). They also [published an upgrade guide and a ** React v18.3 release**](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) with new warnings to help us identify issues **before upgrading to React 19**. +The React core team recently [released the first **React 19 beta**](https://react.dev/blog/2024/04/25/react-19). They also [published an upgrade guide and a **React v18.3 release**](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) with new warnings to help us identify issues **before upgrading to React 19**. Docusaurus v3 depends on React `18.x`. When initializing a new Docusaurus sites, it will use that new React `18.3` release. It's also the case if you decide to upgrade your dependencies, or re-generate your package manager lockfile. From 02e38d8ccf7811af27a9c15ddbadf4d26cfc0eab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Fri, 10 May 2024 15:53:23 +0200 Subject: [PATCH 08/11] refactor: fix i18n bug in node v22 (#10129) --- packages/docusaurus/src/server/i18n.ts | 53 +++++++++++++++++++++----- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/packages/docusaurus/src/server/i18n.ts b/packages/docusaurus/src/server/i18n.ts index f87cc0e93c..c44a65f41f 100644 --- a/packages/docusaurus/src/server/i18n.ts +++ b/packages/docusaurus/src/server/i18n.ts @@ -10,10 +10,36 @@ import {getLangDir} from 'rtl-detect'; import type {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types'; import type {LoadContextParams} from './site'; +function inferLanguageDisplayName(locale: string) { + const tryLocale = (l: string) => { + try { + return new Intl.DisplayNames(l, { + type: 'language', + fallback: 'code', + }).of(l)!; + } catch (e) { + // This is to compensate "of()" that is a bit strict + // Looks like starting Node 22, this locale throws: "en-US-u-ca-buddhist" + // RangeError: invalid_argument + return null; + } + }; + + const parts = locale.split('-'); + + // This is a best effort, we try various locale forms that could give a result + return ( + tryLocale(locale) ?? + tryLocale(`${parts[0]}-${parts[1]}`) ?? + tryLocale(parts[0]!) + ); +} + function getDefaultLocaleLabel(locale: string) { - const languageName = new Intl.DisplayNames(locale, {type: 'language'}).of( - locale, - )!; + const languageName = inferLanguageDisplayName(locale); + if (!languageName) { + return locale; + } return ( languageName.charAt(0).toLocaleUpperCase(locale) + languageName.substring(1) ); @@ -44,13 +70,20 @@ function getDefaultCalendar(localeStr: string) { } export function getDefaultLocaleConfig(locale: string): I18nLocaleConfig { - return { - label: getDefaultLocaleLabel(locale), - direction: getLangDir(locale), - htmlLang: locale, - calendar: getDefaultCalendar(locale), - path: locale, - }; + try { + return { + label: getDefaultLocaleLabel(locale), + direction: getLangDir(locale), + htmlLang: locale, + calendar: getDefaultCalendar(locale), + path: locale, + }; + } catch (e) { + throw new Error( + `Docusaurus couldn't get default locale config for ${locale}`, + {cause: e}, + ); + } } export async function loadI18n( From 394ce84691ff3993d584de4e7c8ae2df30659dda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Fri, 10 May 2024 16:54:59 +0200 Subject: [PATCH 09/11] fix(core): the broken anchor checker should not be sensitive pathname trailing slashes (#10130) --- .../src/server/__tests__/brokenLinks.test.ts | 26 +++++++++++++++++++ packages/docusaurus/src/server/brokenLinks.ts | 10 ++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 2f962db07c..61b4469693 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -148,6 +148,32 @@ describe('handleBrokenLinks', () => { }); }); + it('accepts valid non-strict link with anchor', async () => { + await testBrokenLinks({ + routes: [{path: '/page1', strict: false}, {path: '/page2/'}], + collectedLinks: { + '/page1': { + links: [ + '/page1#page1anchor', + '/page1/#page1anchor', + '/page2#page2anchor', + '/page2/#page2anchor', + ], + anchors: ['page1anchor'], + }, + '/page2/': { + links: [ + '/page1#page1anchor', + '/page1/#page1anchor', + '/page2#page2anchor', + '/page2/#page2anchor', + ], + anchors: ['page2anchor'], + }, + }, + }); + }); + it('accepts valid links and anchors, sparse arrays', async () => { await testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 9e940f7b8c..3ee1971f81 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -125,7 +125,15 @@ function createBrokenLinksHelper({ return false; } const targetPage = - collectedLinks.get(pathname) || collectedLinks.get(decodeURI(pathname)); + collectedLinks.get(pathname) ?? + collectedLinks.get(decodeURI(pathname)) ?? + // The broken link checker should not care about a trailing slash + // Those are already covered by the broken pathname checker + // See https://github.com/facebook/docusaurus/issues/10116 + collectedLinks.get(addTrailingSlash(pathname)) ?? + collectedLinks.get(addTrailingSlash(decodeURI(pathname))) ?? + collectedLinks.get(removeTrailingSlash(pathname)) ?? + collectedLinks.get(removeTrailingSlash(decodeURI(pathname))); // link with anchor to a page that does not exist (or did not collect any // link/anchor) is considered as a broken anchor if (!targetPage) { From 29b7a4ddbbd057372d10370fc062075d79c98b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Fri, 10 May 2024 18:17:21 +0200 Subject: [PATCH 10/11] fix(core): codegen should generate unique route prop filenames (#10131) --- .../src/__tests__/hashUtils.test.ts | 26 +++++++ packages/docusaurus-utils/src/hashUtils.ts | 23 ++++++- .../codegen/__tests__/codegenRoutes.test.ts | 68 ++++++++++++++++++- .../src/server/codegen/codegenRoutes.ts | 60 ++++++++++------ 4 files changed, 152 insertions(+), 25 deletions(-) diff --git a/packages/docusaurus-utils/src/__tests__/hashUtils.test.ts b/packages/docusaurus-utils/src/__tests__/hashUtils.test.ts index 6cc32c0e46..c51acd087e 100644 --- a/packages/docusaurus-utils/src/__tests__/hashUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/hashUtils.test.ts @@ -48,4 +48,30 @@ describe('docuHash', () => { expect(docuHash(file)).toBe(asserts[file]); }); }); + + it('docuHash works with hashLength option', () => { + const asserts: {[key: string]: string} = { + '': '-d41d8', + '/': 'index', + '/foo-bar': 'foo-bar-09652', + '/foo/bar': 'foo-bar-1df48', + }; + Object.keys(asserts).forEach((file) => { + expect(docuHash(file, {hashLength: 5})).toBe(asserts[file]); + }); + }); + + it('docuHash works with hashExtra option', () => { + expect(docuHash('')).toBe('-d41'); + expect(docuHash('', {hashExtra: ''})).toBe('-d41'); + expect(docuHash('', {hashExtra: 'some-extra'})).toBe('-928'); + + expect(docuHash('/')).toBe('index'); + expect(docuHash('/', {hashExtra: ''})).toBe('index-6a9'); + expect(docuHash('/', {hashExtra: 'some-extra'})).toBe('index-68e'); + + expect(docuHash('/foo/bar')).toBe('foo-bar-1df'); + expect(docuHash('/foo/bar', {hashExtra: ''})).toBe('foo-bar-1df'); + expect(docuHash('/foo/bar', {hashExtra: 'some-extra'})).toBe('foo-bar-7d4'); + }); }); diff --git a/packages/docusaurus-utils/src/hashUtils.ts b/packages/docusaurus-utils/src/hashUtils.ts index 8d6e344b47..8e216cba9e 100644 --- a/packages/docusaurus-utils/src/hashUtils.ts +++ b/packages/docusaurus-utils/src/hashUtils.ts @@ -25,11 +25,28 @@ export function simpleHash(str: string, length: number): string { * collision. Also removes part of the string if its larger than the allowed * filename per OS, avoiding `ERRNAMETOOLONG` error. */ -export function docuHash(str: string): string { - if (str === '/') { +export function docuHash( + strInput: string, + options?: { + // String that contributes to the hash value + // but does not contribute to the returned string + hashExtra?: string; + // Length of the hash to append + hashLength?: number; + }, +): string { + // TODO check this historical behavior + // I'm not sure it makes sense to keep it... + if (strInput === '/' && typeof options?.hashExtra === 'undefined') { return 'index'; } - const shortHash = simpleHash(str, 3); + const str = strInput === '/' ? 'index' : strInput; + + const hashExtra = options?.hashExtra ?? ''; + const hashLength = options?.hashLength ?? 3; + + const stringToHash = str + hashExtra; + const shortHash = simpleHash(stringToHash, hashLength); const parsedPath = `${_.kebabCase(str)}-${shortHash}`; if (isNameTooLong(parsedPath)) { return `${shortName(_.kebabCase(str))}-${shortHash}`; diff --git a/packages/docusaurus/src/server/codegen/__tests__/codegenRoutes.test.ts b/packages/docusaurus/src/server/codegen/__tests__/codegenRoutes.test.ts index c1e55c9178..4353e27330 100644 --- a/packages/docusaurus/src/server/codegen/__tests__/codegenRoutes.test.ts +++ b/packages/docusaurus/src/server/codegen/__tests__/codegenRoutes.test.ts @@ -5,9 +5,75 @@ * LICENSE file in the root directory of this source tree. */ -import {generateRoutesCode, genChunkName} from '../codegenRoutes'; +import { + generateRoutesCode, + genChunkName, + generateRoutePropFilename, +} from '../codegenRoutes'; import type {RouteConfig} from '@docusaurus/types'; +describe('generateRoutePropFilename', () => { + it('generate filename based on route path', () => { + expect( + generateRoutePropFilename({ + path: '/some/route-path/', + component: '@theme/Home', + }), + ).toEqual(expect.stringMatching(/^some-route-path-[a-z\d]{3}.json$/)); + }); + + it('generate filename for /', () => { + expect( + generateRoutePropFilename({ + path: '/', + component: '@theme/Home', + }), + ).toEqual(expect.stringMatching(/^index-[a-z\d]{3}.json$/)); + }); + + it('generate filename for /category/', () => { + expect( + generateRoutePropFilename({ + path: '/category/', + component: '@theme/Home', + }), + ).toEqual(expect.stringMatching(/^category-[a-z\d]{3}.json$/)); + }); + + it('generate unique filenames for /', () => { + expect( + generateRoutePropFilename({path: '/', component: '@theme/Home'}), + ).toEqual(generateRoutePropFilename({path: '/', component: '@theme/Home'})); + expect( + generateRoutePropFilename({path: '/', component: '@theme/Home'}), + ).not.toEqual( + generateRoutePropFilename({ + path: '/', + component: '@theme/AnotherComponent', + }), + ); + }); + + it('generate unique filenames for /some/path', () => { + expect( + generateRoutePropFilename({path: '/some/path', component: '@theme/Home'}), + ).toEqual( + generateRoutePropFilename({path: '/some/path', component: '@theme/Home'}), + ); + expect( + generateRoutePropFilename({ + path: '/some/path', + component: '@theme/Home', + }), + ).not.toEqual( + generateRoutePropFilename({ + path: '/some/path', + component: '@theme/AnotherComponent', + }), + ); + }); +}); + describe('genChunkName', () => { it('works', () => { const firstAssert: {[key: string]: string} = { diff --git a/packages/docusaurus/src/server/codegen/codegenRoutes.ts b/packages/docusaurus/src/server/codegen/codegenRoutes.ts index cb40afc585..b637fd956d 100644 --- a/packages/docusaurus/src/server/codegen/codegenRoutes.ts +++ b/packages/docusaurus/src/server/codegen/codegenRoutes.ts @@ -320,6 +320,22 @@ type GenerateRouteFilesParams = { baseUrl: string; }; +// The generated filename per route must be unique to avoid conflicts +// See also https://github.com/facebook/docusaurus/issues/10125 +export function generateRoutePropFilename(route: RouteConfig): string { + // TODO if possible, we could try to shorten the filename by removing + // the plugin routeBasePath prefix from the name + return `${docuHash( + route.path, + // Note: using hash(route.path + route.component) is not technically + // as robust as hashing the entire prop content object. + // But it's faster and should be good enough considering it's very unlikely + // anyone would have 2 routes on the same path also rendering the exact + // same component. + {hashExtra: route.component}, + )}.json`; +} + async function generateRoutePropModule({ generatedFilesDir, route, @@ -339,7 +355,7 @@ async function generateRoutePropModule({ plugin.name, plugin.id, 'p', - `${docuHash(route.path)}.json`, + generateRoutePropFilename(route), ); const modulePath = path.posix.join(generatedFilesDir, relativePath); const aliasedPath = path.posix.join('@generated', relativePath); @@ -376,29 +392,31 @@ async function preprocessRouteProps({ route: RouteConfig; plugin: PluginIdentifier; }): Promise { - const propsModulePathPromise = route.props - ? generateRoutePropModule({ - generatedFilesDir, - route, - plugin, - }) - : undefined; + const getPropsModulePathPromise = () => + route.props + ? generateRoutePropModule({ + generatedFilesDir, + route, + plugin, + }) + : undefined; - const subRoutesPromise = route.routes - ? Promise.all( - route.routes.map((subRoute: RouteConfig) => { - return preprocessRouteProps({ - generatedFilesDir, - route: subRoute, - plugin, - }); - }), - ) - : undefined; + const getSubRoutesPromise = () => + route.routes + ? Promise.all( + route.routes.map((subRoute: RouteConfig) => { + return preprocessRouteProps({ + generatedFilesDir, + route: subRoute, + plugin, + }); + }), + ) + : undefined; const [propsModulePath, subRoutes] = await Promise.all([ - propsModulePathPromise, - subRoutesPromise, + getPropsModulePathPromise(), + getSubRoutesPromise(), ]); const newRoute: RouteConfig = { From ff5039f4137c80de010308d775b5f0bc74be9f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Mon, 13 May 2024 15:03:48 +0200 Subject: [PATCH 11/11] fix(core): `configurePostCss()` should run after `configureWebpack()` (#10132) --- .../src/__tests__/index.test.ts | 2 +- packages/docusaurus/src/commands/build.ts | 11 +- .../docusaurus/src/commands/start/webpack.ts | 4 +- .../src/webpack/__tests__/configure.test.ts | 458 ++++++++++++++++++ .../src/webpack/__tests__/utils.test.ts | 260 +--------- packages/docusaurus/src/webpack/configure.ts | 156 ++++++ packages/docusaurus/src/webpack/utils.ts | 134 ----- 7 files changed, 620 insertions(+), 405 deletions(-) create mode 100644 packages/docusaurus/src/webpack/__tests__/configure.test.ts create mode 100644 packages/docusaurus/src/webpack/configure.ts diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index 65c1b32179..b6975a29b0 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -13,7 +13,7 @@ import {isMatch} from 'picomatch'; import commander from 'commander'; import webpack from 'webpack'; import {loadContext} from '@docusaurus/core/src/server/site'; -import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils'; +import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/configure'; import {sortRoutes} from '@docusaurus/core/src/server/plugins/routeConfig'; import {posixPath} from '@docusaurus/utils'; import {normalizePluginOptions} from '@docusaurus/utils-validation'; diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index 21a7b14b8b..b823dbd1b6 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -15,11 +15,8 @@ import {handleBrokenLinks} from '../server/brokenLinks'; import {createBuildClientConfig} from '../webpack/client'; import createServerConfig from '../webpack/server'; -import { - executePluginsConfigurePostCss, - executePluginsConfigureWebpack, - compile, -} from '../webpack/utils'; +import {executePluginsConfigureWebpack} from '../webpack/configure'; +import {compile} from '../webpack/utils'; import {PerfLogger} from '../utils'; import {loadI18n} from '../server/i18n'; @@ -325,10 +322,6 @@ async function getBuildClientConfig({ bundleAnalyzer: cliOptions.bundleAnalyzer ?? false, }); let {config} = result; - config = executePluginsConfigurePostCss({ - plugins, - config, - }); config = executePluginsConfigureWebpack({ plugins, config, diff --git a/packages/docusaurus/src/commands/start/webpack.ts b/packages/docusaurus/src/commands/start/webpack.ts index 4a9d8fa841..c3b0ceec82 100644 --- a/packages/docusaurus/src/commands/start/webpack.ts +++ b/packages/docusaurus/src/commands/start/webpack.ts @@ -13,12 +13,11 @@ import WebpackDevServer from 'webpack-dev-server'; import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware'; import {createPollingOptions} from './watcher'; import { - executePluginsConfigurePostCss, - executePluginsConfigureWebpack, formatStatsErrorMessage, getHttpsConfig, printStatsWarnings, } from '../../webpack/utils'; +import {executePluginsConfigureWebpack} from '../../webpack/configure'; import {createStartClientConfig} from '../../webpack/client'; import type {StartCLIOptions} from './start'; import type {Props} from '@docusaurus/types'; @@ -135,7 +134,6 @@ async function getStartClientConfig({ minify, poll, }); - config = executePluginsConfigurePostCss({plugins, config}); config = executePluginsConfigureWebpack({ plugins, config, diff --git a/packages/docusaurus/src/webpack/__tests__/configure.test.ts b/packages/docusaurus/src/webpack/__tests__/configure.test.ts new file mode 100644 index 0000000000..8d1ce1e911 --- /dev/null +++ b/packages/docusaurus/src/webpack/__tests__/configure.test.ts @@ -0,0 +1,458 @@ +/** + * 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 * as path from 'path'; +import * as webpack from 'webpack'; +import {fromPartial} from '@total-typescript/shoehorn'; +import { + applyConfigureWebpack, + applyConfigurePostCss, + executePluginsConfigureWebpack, +} from '../configure'; +import type {Configuration} from 'webpack'; +import type {LoadedPlugin, Plugin} from '@docusaurus/types'; + +describe('extending generated webpack config', () => { + it('direct mutation on generated webpack config object', async () => { + // Fake generated webpack config + let config: Configuration = { + output: { + path: __dirname, + filename: 'bundle.js', + }, + }; + + // @ts-expect-error: Testing an edge-case that we did not write types for + const configureWebpack: NonNullable = ( + generatedConfig, + isServer, + ) => { + if (!isServer) { + generatedConfig.entry = 'entry.js'; + generatedConfig.output = { + path: path.join(__dirname, 'dist'), + filename: 'new.bundle.js', + }; + } + // Implicitly returning undefined to test null-safety + }; + + config = applyConfigureWebpack(configureWebpack, config, false, undefined, { + content: 42, + }); + expect(config).toEqual({ + entry: 'entry.js', + output: { + path: path.join(__dirname, 'dist'), + filename: 'new.bundle.js', + }, + }); + const errors = webpack.validate(config); + expect(errors).toBeUndefined(); + }); + + it('webpack-merge with user webpack config object', async () => { + let config: Configuration = { + output: { + path: __dirname, + filename: 'bundle.js', + }, + }; + + const configureWebpack: Plugin['configureWebpack'] = () => ({ + entry: 'entry.js', + output: { + path: path.join(__dirname, 'dist'), + filename: 'new.bundle.js', + }, + }); + + config = applyConfigureWebpack(configureWebpack, config, false, undefined, { + content: 42, + }); + expect(config).toEqual({ + entry: 'entry.js', + output: { + path: path.join(__dirname, 'dist'), + filename: 'new.bundle.js', + }, + }); + const errors = webpack.validate(config); + expect(errors).toBeUndefined(); + }); + + it('webpack-merge with custom strategy', async () => { + const config: Configuration = { + module: { + rules: [{use: 'xxx'}, {use: 'yyy'}], + }, + }; + + const createConfigureWebpack = + (mergeStrategy?: { + [key: string]: 'prepend' | 'append'; + }): NonNullable => + () => ({ + module: { + rules: [{use: 'zzz'}], + }, + mergeStrategy, + }); + + const defaultStrategyMergeConfig = applyConfigureWebpack( + createConfigureWebpack(), + config, + false, + undefined, + {content: 42}, + ); + expect(defaultStrategyMergeConfig).toEqual({ + module: { + rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}], + }, + }); + + const prependRulesStrategyConfig = applyConfigureWebpack( + createConfigureWebpack({'module.rules': 'prepend'}), + config, + false, + undefined, + {content: 42}, + ); + expect(prependRulesStrategyConfig).toEqual({ + module: { + rules: [{use: 'zzz'}, {use: 'xxx'}, {use: 'yyy'}], + }, + }); + + const uselessMergeStrategyConfig = applyConfigureWebpack( + createConfigureWebpack({uselessAttributeName: 'append'}), + config, + false, + undefined, + {content: 42}, + ); + expect(uselessMergeStrategyConfig).toEqual({ + module: { + rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}], + }, + }); + }); +}); + +describe('extending PostCSS', () => { + it('user plugin should be appended in PostCSS loader', () => { + let webpackConfig: Configuration = { + output: { + path: __dirname, + filename: 'bundle.js', + }, + module: { + rules: [ + { + test: 'any', + use: [ + { + loader: 'some-loader-1', + options: {}, + }, + { + loader: 'some-loader-2', + options: {}, + }, + { + loader: 'postcss-loader-1', + options: { + postcssOptions: { + plugins: [['default-postcss-loader-1-plugin']], + }, + }, + }, + { + loader: 'some-loader-3', + options: {}, + }, + ], + }, + { + test: '2nd-test', + use: [ + { + loader: 'postcss-loader-2', + options: { + postcssOptions: { + plugins: [['default-postcss-loader-2-plugin']], + }, + }, + }, + ], + }, + ], + }, + }; + + function createFakePlugin(name: string) { + return [name, {}]; + } + + // Run multiple times: ensure last run does not override previous runs + webpackConfig = applyConfigurePostCss( + (postCssOptions) => ({ + ...postCssOptions, + plugins: [ + ...postCssOptions.plugins, + createFakePlugin('postcss-plugin-1'), + ], + }), + webpackConfig, + ); + + webpackConfig = applyConfigurePostCss( + (postCssOptions) => ({ + ...postCssOptions, + plugins: [ + createFakePlugin('postcss-plugin-2'), + ...postCssOptions.plugins, + ], + }), + webpackConfig, + ); + + webpackConfig = applyConfigurePostCss( + (postCssOptions) => ({ + ...postCssOptions, + plugins: [ + ...postCssOptions.plugins, + createFakePlugin('postcss-plugin-3'), + ], + }), + webpackConfig, + ); + + // @ts-expect-error: relax type + const postCssLoader1 = webpackConfig.module?.rules[0].use[2]; + expect(postCssLoader1.loader).toBe('postcss-loader-1'); + + const pluginNames1 = postCssLoader1.options.postcssOptions.plugins.map( + (p: unknown[]) => p[0], + ); + expect(pluginNames1).toHaveLength(4); + expect(pluginNames1).toEqual([ + 'postcss-plugin-2', + 'default-postcss-loader-1-plugin', + 'postcss-plugin-1', + 'postcss-plugin-3', + ]); + + // @ts-expect-error: relax type + const postCssLoader2 = webpackConfig.module?.rules[1].use[0]; + expect(postCssLoader2.loader).toBe('postcss-loader-2'); + + const pluginNames2 = postCssLoader2.options.postcssOptions.plugins.map( + (p: unknown[]) => p[0], + ); + expect(pluginNames2).toHaveLength(4); + expect(pluginNames2).toEqual([ + 'postcss-plugin-2', + 'default-postcss-loader-2-plugin', + 'postcss-plugin-1', + 'postcss-plugin-3', + ]); + }); +}); + +describe('executePluginsConfigureWebpack', () => { + function fakePlugin(partialPlugin: Partial): LoadedPlugin { + return fromPartial({ + ...partialPlugin, + }); + } + + it('can merge Webpack aliases of 2 plugins into base config', () => { + const config = executePluginsConfigureWebpack({ + config: {resolve: {alias: {'initial-alias': 'initial-alias-value'}}}, + isServer: false, + jsLoader: 'babel', + plugins: [ + fakePlugin({ + configureWebpack: () => { + return {resolve: {alias: {'p1-alias': 'p1-alias-value'}}}; + }, + }), + fakePlugin({ + configureWebpack: () => { + return {resolve: {alias: {'p2-alias': 'p2-alias-value'}}}; + }, + }), + ], + }); + + expect(config).toMatchInlineSnapshot( + {}, + ` + { + "resolve": { + "alias": { + "initial-alias": "initial-alias-value", + "p1-alias": "p1-alias-value", + "p2-alias": "p2-alias-value", + }, + }, + } + `, + ); + }); + + it('can configurePostCSS() for all loaders added through configureWebpack()', () => { + const config = executePluginsConfigureWebpack({ + config: {}, + isServer: false, + jsLoader: 'babel', + plugins: [ + fakePlugin({ + configurePostCss: (postCssOptions) => { + // Imperative mutation should work + postCssOptions.plugins.push('p1-added-postcss-plugin'); + return postCssOptions; + }, + configureWebpack: () => { + return { + module: { + rules: [ + { + test: /\.module.scss$/, + use: 'some-loader', + options: { + postcssOptions: { + plugins: ['p1-initial-postcss-plugin'], + }, + }, + }, + ], + }, + }; + }, + }), + fakePlugin({ + configurePostCss: (postCssOptions) => { + postCssOptions.plugins.push('p2-added-postcss-plugin'); + return postCssOptions; + }, + configureWebpack: () => { + return { + module: { + rules: [ + { + test: /\.module.scss$/, + use: [ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: ['p2-initial-postcss-plugin'], + }, + }, + }, + ], + }, + ], + }, + }; + }, + }), + fakePlugin({ + configurePostCss: (postCssOptions) => { + // Functional/immutable copy mutation should work + return { + ...postCssOptions, + plugins: [...postCssOptions.plugins, 'p3-added-postcss-plugin'], + }; + }, + configureWebpack: () => { + return { + module: { + rules: [ + { + test: /\.module.scss$/, + oneOf: [ + { + use: 'some-loader', + options: { + postcssOptions: { + plugins: ['p3-initial-postcss-plugin'], + }, + }, + }, + ], + }, + ], + }, + }; + }, + }), + ], + }); + + expect(config.module.rules).toHaveLength(3); + expect(config.module.rules[0]).toMatchInlineSnapshot(` + { + "options": { + "postcssOptions": { + "plugins": [ + "p1-initial-postcss-plugin", + "p1-added-postcss-plugin", + "p2-added-postcss-plugin", + "p3-added-postcss-plugin", + ], + }, + }, + "test": /\\\\\\.module\\.scss\\$/, + "use": "some-loader", + } + `); + expect(config.module.rules[1]).toMatchInlineSnapshot(` + { + "test": /\\\\\\.module\\.scss\\$/, + "use": [ + { + "loader": "postcss-loader", + "options": { + "postcssOptions": { + "plugins": [ + "p2-initial-postcss-plugin", + "p1-added-postcss-plugin", + "p2-added-postcss-plugin", + "p3-added-postcss-plugin", + ], + }, + }, + }, + ], + } + `); + expect(config.module.rules[2]).toMatchInlineSnapshot(` + { + "oneOf": [ + { + "options": { + "postcssOptions": { + "plugins": [ + "p3-initial-postcss-plugin", + "p1-added-postcss-plugin", + "p2-added-postcss-plugin", + "p3-added-postcss-plugin", + ], + }, + }, + "use": "some-loader", + }, + ], + "test": /\\\\\\.module\\.scss\\$/, + } + `); + }); +}); diff --git a/packages/docusaurus/src/webpack/__tests__/utils.test.ts b/packages/docusaurus/src/webpack/__tests__/utils.test.ts index dcc23d6351..5f0a769e65 100644 --- a/packages/docusaurus/src/webpack/__tests__/utils.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/utils.test.ts @@ -6,15 +6,8 @@ */ import path from 'path'; -import webpack, {type Configuration, type RuleSetRule} from 'webpack'; - -import { - getCustomizableJSLoader, - applyConfigureWebpack, - applyConfigurePostCss, - getHttpsConfig, -} from '../utils'; -import type {Plugin} from '@docusaurus/types'; +import {getCustomizableJSLoader, getHttpsConfig} from '../utils'; +import type {RuleSetRule} from 'webpack'; describe('customize JS loader', () => { it('getCustomizableJSLoader defaults to babel loader', () => { @@ -50,255 +43,6 @@ describe('customize JS loader', () => { }); }); -describe('extending generated webpack config', () => { - it('direct mutation on generated webpack config object', async () => { - // Fake generated webpack config - let config: Configuration = { - output: { - path: __dirname, - filename: 'bundle.js', - }, - }; - - // @ts-expect-error: Testing an edge-case that we did not write types for - const configureWebpack: NonNullable = ( - generatedConfig, - isServer, - ) => { - if (!isServer) { - generatedConfig.entry = 'entry.js'; - generatedConfig.output = { - path: path.join(__dirname, 'dist'), - filename: 'new.bundle.js', - }; - } - // Implicitly returning undefined to test null-safety - }; - - config = applyConfigureWebpack(configureWebpack, config, false, undefined, { - content: 42, - }); - expect(config).toEqual({ - entry: 'entry.js', - output: { - path: path.join(__dirname, 'dist'), - filename: 'new.bundle.js', - }, - }); - const errors = webpack.validate(config); - expect(errors).toBeUndefined(); - }); - - it('webpack-merge with user webpack config object', async () => { - let config: Configuration = { - output: { - path: __dirname, - filename: 'bundle.js', - }, - }; - - const configureWebpack: Plugin['configureWebpack'] = () => ({ - entry: 'entry.js', - output: { - path: path.join(__dirname, 'dist'), - filename: 'new.bundle.js', - }, - }); - - config = applyConfigureWebpack(configureWebpack, config, false, undefined, { - content: 42, - }); - expect(config).toEqual({ - entry: 'entry.js', - output: { - path: path.join(__dirname, 'dist'), - filename: 'new.bundle.js', - }, - }); - const errors = webpack.validate(config); - expect(errors).toBeUndefined(); - }); - - it('webpack-merge with custom strategy', async () => { - const config: Configuration = { - module: { - rules: [{use: 'xxx'}, {use: 'yyy'}], - }, - }; - - const createConfigureWebpack = - (mergeStrategy?: { - [key: string]: 'prepend' | 'append'; - }): NonNullable => - () => ({ - module: { - rules: [{use: 'zzz'}], - }, - mergeStrategy, - }); - - const defaultStrategyMergeConfig = applyConfigureWebpack( - createConfigureWebpack(), - config, - false, - undefined, - {content: 42}, - ); - expect(defaultStrategyMergeConfig).toEqual({ - module: { - rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}], - }, - }); - - const prependRulesStrategyConfig = applyConfigureWebpack( - createConfigureWebpack({'module.rules': 'prepend'}), - config, - false, - undefined, - {content: 42}, - ); - expect(prependRulesStrategyConfig).toEqual({ - module: { - rules: [{use: 'zzz'}, {use: 'xxx'}, {use: 'yyy'}], - }, - }); - - const uselessMergeStrategyConfig = applyConfigureWebpack( - createConfigureWebpack({uselessAttributeName: 'append'}), - config, - false, - undefined, - {content: 42}, - ); - expect(uselessMergeStrategyConfig).toEqual({ - module: { - rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}], - }, - }); - }); -}); - -describe('extending PostCSS', () => { - it('user plugin should be appended in PostCSS loader', () => { - let webpackConfig: Configuration = { - output: { - path: __dirname, - filename: 'bundle.js', - }, - module: { - rules: [ - { - test: 'any', - use: [ - { - loader: 'some-loader-1', - options: {}, - }, - { - loader: 'some-loader-2', - options: {}, - }, - { - loader: 'postcss-loader-1', - options: { - postcssOptions: { - plugins: [['default-postcss-loader-1-plugin']], - }, - }, - }, - { - loader: 'some-loader-3', - options: {}, - }, - ], - }, - { - test: '2nd-test', - use: [ - { - loader: 'postcss-loader-2', - options: { - postcssOptions: { - plugins: [['default-postcss-loader-2-plugin']], - }, - }, - }, - ], - }, - ], - }, - }; - - function createFakePlugin(name: string) { - return [name, {}]; - } - - // Run multiple times: ensure last run does not override previous runs - webpackConfig = applyConfigurePostCss( - (postCssOptions) => ({ - ...postCssOptions, - plugins: [ - ...postCssOptions.plugins, - createFakePlugin('postcss-plugin-1'), - ], - }), - webpackConfig, - ); - - webpackConfig = applyConfigurePostCss( - (postCssOptions) => ({ - ...postCssOptions, - plugins: [ - createFakePlugin('postcss-plugin-2'), - ...postCssOptions.plugins, - ], - }), - webpackConfig, - ); - - webpackConfig = applyConfigurePostCss( - (postCssOptions) => ({ - ...postCssOptions, - plugins: [ - ...postCssOptions.plugins, - createFakePlugin('postcss-plugin-3'), - ], - }), - webpackConfig, - ); - - // @ts-expect-error: relax type - const postCssLoader1 = webpackConfig.module?.rules[0].use[2]; - expect(postCssLoader1.loader).toBe('postcss-loader-1'); - - const pluginNames1 = postCssLoader1.options.postcssOptions.plugins.map( - (p: unknown[]) => p[0], - ); - expect(pluginNames1).toHaveLength(4); - expect(pluginNames1).toEqual([ - 'postcss-plugin-2', - 'default-postcss-loader-1-plugin', - 'postcss-plugin-1', - 'postcss-plugin-3', - ]); - - // @ts-expect-error: relax type - const postCssLoader2 = webpackConfig.module?.rules[1].use[0]; - expect(postCssLoader2.loader).toBe('postcss-loader-2'); - - const pluginNames2 = postCssLoader2.options.postcssOptions.plugins.map( - (p: unknown[]) => p[0], - ); - expect(pluginNames2).toHaveLength(4); - expect(pluginNames2).toEqual([ - 'postcss-plugin-2', - 'default-postcss-loader-2-plugin', - 'postcss-plugin-1', - 'postcss-plugin-3', - ]); - }); -}); - describe('getHttpsConfig', () => { const originalEnv = process.env; diff --git a/packages/docusaurus/src/webpack/configure.ts b/packages/docusaurus/src/webpack/configure.ts new file mode 100644 index 0000000000..cf499645fe --- /dev/null +++ b/packages/docusaurus/src/webpack/configure.ts @@ -0,0 +1,156 @@ +/** + * 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 { + mergeWithCustomize, + customizeArray, + customizeObject, +} from 'webpack-merge'; +import {getCustomizableJSLoader, getStyleLoaders} from './utils'; + +import type {Configuration, RuleSetRule} from 'webpack'; +import type { + Plugin, + PostCssOptions, + ConfigureWebpackUtils, + LoadedPlugin, +} from '@docusaurus/types'; + +/** + * Helper function to modify webpack config + * @param configureWebpack a webpack config or a function to modify config + * @param config initial webpack config + * @param isServer indicates if this is a server webpack configuration + * @param jsLoader custom js loader config + * @param content content loaded by the plugin + * @returns final/ modified webpack config + */ +export function applyConfigureWebpack( + configureWebpack: NonNullable, + config: Configuration, + isServer: boolean, + jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined, + content: unknown, +): Configuration { + // Export some utility functions + const utils: ConfigureWebpackUtils = { + getStyleLoaders, + getJSLoader: getCustomizableJSLoader(jsLoader), + }; + if (typeof configureWebpack === 'function') { + const {mergeStrategy, ...res} = + configureWebpack(config, isServer, utils, content) ?? {}; + const customizeRules = mergeStrategy ?? {}; + return mergeWithCustomize({ + customizeArray: customizeArray(customizeRules), + customizeObject: customizeObject(customizeRules), + })(config, res); + } + return config; +} + +export function applyConfigurePostCss( + configurePostCss: NonNullable, + config: Configuration, +): Configuration { + type LocalPostCSSLoader = object & { + options: {postcssOptions: PostCssOptions}; + }; + + // Not ideal heuristic but good enough for our use-case? + function isPostCssLoader(loader: unknown): loader is LocalPostCSSLoader { + return !!(loader as LocalPostCSSLoader)?.options?.postcssOptions; + } + + // Does not handle all edge cases, but good enough for now + function overridePostCssOptions(entry: RuleSetRule) { + if (isPostCssLoader(entry)) { + entry.options.postcssOptions = configurePostCss( + entry.options.postcssOptions, + ); + } else if (Array.isArray(entry.oneOf)) { + entry.oneOf.forEach((r) => { + if (r) { + overridePostCssOptions(r); + } + }); + } else if (Array.isArray(entry.use)) { + entry.use + .filter((u) => typeof u === 'object') + .forEach((rule) => overridePostCssOptions(rule as RuleSetRule)); + } + } + + config.module?.rules?.forEach((rule) => + overridePostCssOptions(rule as RuleSetRule), + ); + + return config; +} + +// Plugin Lifecycle - configurePostCss() +function executePluginsConfigurePostCss({ + plugins, + config, +}: { + plugins: LoadedPlugin[]; + config: Configuration; +}): Configuration { + let resultConfig = config; + plugins.forEach((plugin) => { + const {configurePostCss} = plugin; + if (configurePostCss) { + resultConfig = applyConfigurePostCss( + configurePostCss.bind(plugin), + resultConfig, + ); + } + }); + return resultConfig; +} + +// Plugin Lifecycle - configureWebpack() +export function executePluginsConfigureWebpack({ + plugins, + config, + isServer, + jsLoader, +}: { + plugins: LoadedPlugin[]; + config: Configuration; + isServer: boolean; + jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined; +}): Configuration { + // Step1 - Configure Webpack + let resultConfig = config; + plugins.forEach((plugin) => { + const {configureWebpack} = plugin; + if (configureWebpack) { + resultConfig = applyConfigureWebpack( + configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`. + resultConfig, + isServer, + jsLoader, + plugin.content, + ); + } + }); + + // Step2 - For client code, configure PostCSS + // The order matters! We want to configure PostCSS on loaders + // that were potentially added by configureWebpack + // See https://github.com/facebook/docusaurus/issues/10106 + // Note: it's useless to configure postCSS for the server + if (!isServer) { + resultConfig = executePluginsConfigurePostCss({ + plugins, + config: resultConfig, + }); + } + + return resultConfig; +} diff --git a/packages/docusaurus/src/webpack/utils.ts b/packages/docusaurus/src/webpack/utils.ts index ee6c61f1ec..86a695ea79 100644 --- a/packages/docusaurus/src/webpack/utils.ts +++ b/packages/docusaurus/src/webpack/utils.ts @@ -11,20 +11,9 @@ import crypto from 'crypto'; import logger from '@docusaurus/logger'; import {BABEL_CONFIG_FILE_NAME} from '@docusaurus/utils'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; -import { - mergeWithCustomize, - customizeArray, - customizeObject, -} from 'webpack-merge'; import webpack, {type Configuration, type RuleSetRule} from 'webpack'; import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages'; import type {TransformOptions} from '@babel/core'; -import type { - Plugin, - PostCssOptions, - ConfigureWebpackUtils, - LoadedPlugin, -} from '@docusaurus/types'; export function formatStatsErrorMessage( statsJson: ReturnType | undefined, @@ -181,129 +170,6 @@ export const getCustomizableJSLoader = ? getDefaultBabelLoader({isServer, babelOptions}) : jsLoader(isServer); -/** - * Helper function to modify webpack config - * @param configureWebpack a webpack config or a function to modify config - * @param config initial webpack config - * @param isServer indicates if this is a server webpack configuration - * @param jsLoader custom js loader config - * @param content content loaded by the plugin - * @returns final/ modified webpack config - */ -export function applyConfigureWebpack( - configureWebpack: NonNullable, - config: Configuration, - isServer: boolean, - jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined, - content: unknown, -): Configuration { - // Export some utility functions - const utils: ConfigureWebpackUtils = { - getStyleLoaders, - getJSLoader: getCustomizableJSLoader(jsLoader), - }; - if (typeof configureWebpack === 'function') { - const {mergeStrategy, ...res} = - configureWebpack(config, isServer, utils, content) ?? {}; - const customizeRules = mergeStrategy ?? {}; - return mergeWithCustomize({ - customizeArray: customizeArray(customizeRules), - customizeObject: customizeObject(customizeRules), - })(config, res); - } - return config; -} - -export function applyConfigurePostCss( - configurePostCss: NonNullable, - config: Configuration, -): Configuration { - type LocalPostCSSLoader = object & { - options: {postcssOptions: PostCssOptions}; - }; - - // Not ideal heuristic but good enough for our use-case? - function isPostCssLoader(loader: unknown): loader is LocalPostCSSLoader { - return !!(loader as LocalPostCSSLoader)?.options?.postcssOptions; - } - - // Does not handle all edge cases, but good enough for now - function overridePostCssOptions(entry: RuleSetRule) { - if (isPostCssLoader(entry)) { - entry.options.postcssOptions = configurePostCss( - entry.options.postcssOptions, - ); - } else if (Array.isArray(entry.oneOf)) { - entry.oneOf.forEach((r) => { - if (r) { - overridePostCssOptions(r); - } - }); - } else if (Array.isArray(entry.use)) { - entry.use - .filter((u) => typeof u === 'object') - .forEach((rule) => overridePostCssOptions(rule as RuleSetRule)); - } - } - - config.module?.rules?.forEach((rule) => - overridePostCssOptions(rule as RuleSetRule), - ); - - return config; -} - -// Plugin Lifecycle - configurePostCss() -export function executePluginsConfigurePostCss({ - plugins, - config, -}: { - plugins: LoadedPlugin[]; - config: Configuration; -}): Configuration { - let resultConfig = config; - plugins.forEach((plugin) => { - const {configurePostCss} = plugin; - if (configurePostCss) { - resultConfig = applyConfigurePostCss( - configurePostCss.bind(plugin), - resultConfig, - ); - } - }); - return resultConfig; -} - -// Plugin Lifecycle - configureWebpack() -export function executePluginsConfigureWebpack({ - plugins, - config, - isServer, - jsLoader, -}: { - plugins: LoadedPlugin[]; - config: Configuration; - isServer: boolean; - jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined; -}): Configuration { - let resultConfig = config; - - plugins.forEach((plugin) => { - const {configureWebpack} = plugin; - if (configureWebpack) { - resultConfig = applyConfigureWebpack( - configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`. - resultConfig, - isServer, - jsLoader, - plugin.content, - ); - } - }); - - return resultConfig; -} - declare global { interface Error { /** @see https://webpack.js.org/api/node/#error-handling */