@kirchdev/duxt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/app/app.config.ts +13 -0
- package/app/app.vue +122 -0
- package/app/assets/css/duxt.css +555 -0
- package/app/assets/css/typeset.css +498 -0
- package/app/components/DuxtBrand.vue +53 -0
- package/app/components/DuxtBreadcrumb.vue +37 -0
- package/app/components/DuxtCodeBlock.vue +143 -0
- package/app/components/DuxtCopyPage.vue +117 -0
- package/app/components/DuxtFooter.vue +86 -0
- package/app/components/DuxtHeader.vue +496 -0
- package/app/components/DuxtJsonEditor.vue +359 -0
- package/app/components/DuxtLandingProse.vue +90 -0
- package/app/components/DuxtLandingShowcase.vue +295 -0
- package/app/components/DuxtLiveWindow.vue +343 -0
- package/app/components/DuxtLocale.vue +86 -0
- package/app/components/DuxtMetaList.vue +76 -0
- package/app/components/DuxtNavigation.vue +94 -0
- package/app/components/DuxtNavigationLink.vue +41 -0
- package/app/components/DuxtOpenApiCallbacks.vue +92 -0
- package/app/components/DuxtOpenApiClient.vue +1339 -0
- package/app/components/DuxtOpenApiMedia.vue +108 -0
- package/app/components/DuxtOpenApiMethod.vue +59 -0
- package/app/components/DuxtOpenApiParameters.vue +114 -0
- package/app/components/DuxtOpenApiResponses.vue +109 -0
- package/app/components/DuxtOpenApiSchema.vue +365 -0
- package/app/components/DuxtOpenApiSecurity.vue +115 -0
- package/app/components/DuxtPageFeedback.vue +56 -0
- package/app/components/DuxtPageInfo.vue +194 -0
- package/app/components/DuxtPageNav.vue +93 -0
- package/app/components/DuxtPreviewSkeleton.vue +262 -0
- package/app/components/DuxtProgress.vue +45 -0
- package/app/components/DuxtSearch.vue +271 -0
- package/app/components/DuxtSections.vue +65 -0
- package/app/components/DuxtShortcuts.vue +46 -0
- package/app/components/DuxtSkipLink.vue +34 -0
- package/app/components/DuxtToc.vue +86 -0
- package/app/components/DuxtTranslationBanner.vue +64 -0
- package/app/components/DuxtVersion.vue +168 -0
- package/app/components/DuxtVersionBanner.vue +109 -0
- package/app/components/OgImage/Duxt.satori.vue +82 -0
- package/app/components/content/Accordion.vue +37 -0
- package/app/components/content/AccordionItem.vue +46 -0
- package/app/components/content/Callout.vue +58 -0
- package/app/components/content/ChangelogGroup.vue +91 -0
- package/app/components/content/ChangelogReleases.vue +520 -0
- package/app/components/content/CodeGroup.vue +183 -0
- package/app/components/content/DevtoolsPanel.vue +107 -0
- package/app/components/content/Field.vue +50 -0
- package/app/components/content/FieldGroup.vue +8 -0
- package/app/components/content/FileTree.vue +92 -0
- package/app/components/content/Mermaid.vue +70 -0
- package/app/components/content/OpenApiOperation.vue +185 -0
- package/app/components/content/OpenApiOperations.vue +72 -0
- package/app/components/content/OpenApiOverview.vue +302 -0
- package/app/components/content/PackageManagers.vue +187 -0
- package/app/components/content/PageCards.vue +48 -0
- package/app/components/content/Partial.vue +65 -0
- package/app/components/content/Preview.vue +173 -0
- package/app/components/content/ProseA.vue +55 -0
- package/app/components/content/ProseH2.vue +53 -0
- package/app/components/content/ProseH3.vue +53 -0
- package/app/components/content/ProseH4.vue +53 -0
- package/app/components/content/ProseImg.vue +87 -0
- package/app/components/content/ProsePre.vue +58 -0
- package/app/components/content/ProseTable.vue +11 -0
- package/app/components/content/Since.vue +21 -0
- package/app/components/content/Steps.vue +20 -0
- package/app/components/content/Tab.vue +23 -0
- package/app/components/content/TabGroup.vue +58 -0
- package/app/components/ui/alert/Alert.vue +21 -0
- package/app/components/ui/alert/AlertDescription.vue +22 -0
- package/app/components/ui/alert/AlertTitle.vue +22 -0
- package/app/components/ui/alert/index.ts +24 -0
- package/app/components/ui/badge/Badge.vue +28 -0
- package/app/components/ui/badge/index.ts +52 -0
- package/app/components/ui/breadcrumb/Breadcrumb.vue +13 -0
- package/app/components/ui/breadcrumb/BreadcrumbEllipsis.vue +22 -0
- package/app/components/ui/breadcrumb/BreadcrumbItem.vue +17 -0
- package/app/components/ui/breadcrumb/BreadcrumbLink.vue +24 -0
- package/app/components/ui/breadcrumb/BreadcrumbList.vue +22 -0
- package/app/components/ui/breadcrumb/BreadcrumbPage.vue +20 -0
- package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +21 -0
- package/app/components/ui/breadcrumb/index.ts +7 -0
- package/app/components/ui/button/Button.vue +31 -0
- package/app/components/ui/button/index.ts +39 -0
- package/app/components/ui/card/Card.vue +22 -0
- package/app/components/ui/card/CardAction.vue +22 -0
- package/app/components/ui/card/CardContent.vue +14 -0
- package/app/components/ui/card/CardDescription.vue +17 -0
- package/app/components/ui/card/CardFooter.vue +17 -0
- package/app/components/ui/card/CardHeader.vue +22 -0
- package/app/components/ui/card/CardTitle.vue +17 -0
- package/app/components/ui/card/index.ts +7 -0
- package/app/components/ui/collapsible/Collapsible.vue +19 -0
- package/app/components/ui/collapsible/CollapsibleContent.vue +12 -0
- package/app/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
- package/app/components/ui/collapsible/index.ts +3 -0
- package/app/components/ui/command/Command.vue +98 -0
- package/app/components/ui/command/CommandDialog.vue +42 -0
- package/app/components/ui/command/CommandEmpty.vue +31 -0
- package/app/components/ui/command/CommandGroup.vue +52 -0
- package/app/components/ui/command/CommandInput.vue +45 -0
- package/app/components/ui/command/CommandItem.vue +85 -0
- package/app/components/ui/command/CommandList.vue +32 -0
- package/app/components/ui/command/CommandSeparator.vue +23 -0
- package/app/components/ui/command/CommandShortcut.vue +19 -0
- package/app/components/ui/command/index.ts +29 -0
- package/app/components/ui/dialog/Dialog.vue +15 -0
- package/app/components/ui/dialog/DialogClose.vue +12 -0
- package/app/components/ui/dialog/DialogContent.vue +61 -0
- package/app/components/ui/dialog/DialogDescription.vue +25 -0
- package/app/components/ui/dialog/DialogFooter.vue +30 -0
- package/app/components/ui/dialog/DialogHeader.vue +17 -0
- package/app/components/ui/dialog/DialogOverlay.vue +28 -0
- package/app/components/ui/dialog/DialogScrollContent.vue +65 -0
- package/app/components/ui/dialog/DialogTitle.vue +25 -0
- package/app/components/ui/dialog/DialogTrigger.vue +12 -0
- package/app/components/ui/dialog/index.ts +10 -0
- package/app/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +47 -0
- package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +47 -0
- package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +12 -0
- package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +41 -0
- package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +27 -0
- package/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +48 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +19 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +34 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +34 -0
- package/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
- package/app/components/ui/dropdown-menu/index.ts +16 -0
- package/app/components/ui/input/Input.vue +35 -0
- package/app/components/ui/input/index.ts +1 -0
- package/app/components/ui/navigation-menu/NavigationMenu.vue +42 -0
- package/app/components/ui/navigation-menu/NavigationMenuContent.vue +35 -0
- package/app/components/ui/navigation-menu/NavigationMenuIndicator.vue +32 -0
- package/app/components/ui/navigation-menu/NavigationMenuItem.vue +23 -0
- package/app/components/ui/navigation-menu/NavigationMenuLink.vue +30 -0
- package/app/components/ui/navigation-menu/NavigationMenuList.vue +30 -0
- package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +31 -0
- package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +30 -0
- package/app/components/ui/navigation-menu/index.ts +14 -0
- package/app/components/ui/scroll-area/ScrollArea.vue +31 -0
- package/app/components/ui/scroll-area/ScrollBar.vue +38 -0
- package/app/components/ui/scroll-area/index.ts +2 -0
- package/app/components/ui/select/Select.vue +15 -0
- package/app/components/ui/select/SelectContent.vue +59 -0
- package/app/components/ui/select/SelectGroup.vue +19 -0
- package/app/components/ui/select/SelectItem.vue +42 -0
- package/app/components/ui/select/SelectItemText.vue +12 -0
- package/app/components/ui/select/SelectLabel.vue +16 -0
- package/app/components/ui/select/SelectScrollDownButton.vue +28 -0
- package/app/components/ui/select/SelectScrollUpButton.vue +28 -0
- package/app/components/ui/select/SelectSeparator.vue +20 -0
- package/app/components/ui/select/SelectTrigger.vue +32 -0
- package/app/components/ui/select/SelectValue.vue +12 -0
- package/app/components/ui/select/index.ts +11 -0
- package/app/components/ui/separator/Separator.vue +30 -0
- package/app/components/ui/separator/index.ts +1 -0
- package/app/components/ui/sheet/Sheet.vue +15 -0
- package/app/components/ui/sheet/SheetClose.vue +12 -0
- package/app/components/ui/sheet/SheetContent.vue +68 -0
- package/app/components/ui/sheet/SheetDescription.vue +23 -0
- package/app/components/ui/sheet/SheetFooter.vue +15 -0
- package/app/components/ui/sheet/SheetHeader.vue +15 -0
- package/app/components/ui/sheet/SheetOverlay.vue +28 -0
- package/app/components/ui/sheet/SheetTitle.vue +23 -0
- package/app/components/ui/sheet/SheetTrigger.vue +12 -0
- package/app/components/ui/sheet/index.ts +8 -0
- package/app/components/ui/sidebar/Sidebar.vue +110 -0
- package/app/components/ui/sidebar/SidebarContent.vue +23 -0
- package/app/components/ui/sidebar/SidebarFooter.vue +18 -0
- package/app/components/ui/sidebar/SidebarGroup.vue +18 -0
- package/app/components/ui/sidebar/SidebarGroupAction.vue +31 -0
- package/app/components/ui/sidebar/SidebarGroupContent.vue +18 -0
- package/app/components/ui/sidebar/SidebarGroupLabel.vue +30 -0
- package/app/components/ui/sidebar/SidebarHeader.vue +18 -0
- package/app/components/ui/sidebar/SidebarInput.vue +19 -0
- package/app/components/ui/sidebar/SidebarInset.vue +23 -0
- package/app/components/ui/sidebar/SidebarMenu.vue +18 -0
- package/app/components/ui/sidebar/SidebarMenuAction.vue +42 -0
- package/app/components/ui/sidebar/SidebarMenuBadge.vue +28 -0
- package/app/components/ui/sidebar/SidebarMenuButton.vue +60 -0
- package/app/components/ui/sidebar/SidebarMenuButtonChild.vue +36 -0
- package/app/components/ui/sidebar/SidebarMenuItem.vue +18 -0
- package/app/components/ui/sidebar/SidebarMenuSkeleton.vue +35 -0
- package/app/components/ui/sidebar/SidebarMenuSub.vue +24 -0
- package/app/components/ui/sidebar/SidebarMenuSubButton.vue +43 -0
- package/app/components/ui/sidebar/SidebarMenuSubItem.vue +18 -0
- package/app/components/ui/sidebar/SidebarProvider.vue +109 -0
- package/app/components/ui/sidebar/SidebarRail.vue +35 -0
- package/app/components/ui/sidebar/SidebarSeparator.vue +19 -0
- package/app/components/ui/sidebar/SidebarTrigger.vue +26 -0
- package/app/components/ui/sidebar/index.ts +62 -0
- package/app/components/ui/sidebar/utils.ts +19 -0
- package/app/components/ui/skeleton/Skeleton.vue +17 -0
- package/app/components/ui/skeleton/index.ts +1 -0
- package/app/components/ui/sonner/Sonner.vue +67 -0
- package/app/components/ui/sonner/index.ts +1 -0
- package/app/components/ui/tabs/Tabs.vue +26 -0
- package/app/components/ui/tabs/TabsContent.vue +23 -0
- package/app/components/ui/tabs/TabsList.vue +28 -0
- package/app/components/ui/tabs/TabsTrigger.vue +30 -0
- package/app/components/ui/tabs/index.ts +4 -0
- package/app/components/ui/textarea/Textarea.vue +32 -0
- package/app/components/ui/textarea/index.ts +1 -0
- package/app/components/ui/tooltip/Tooltip.vue +15 -0
- package/app/components/ui/tooltip/TooltipContent.vue +49 -0
- package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
- package/app/components/ui/tooltip/TooltipTrigger.vue +12 -0
- package/app/components/ui/tooltip/index.ts +4 -0
- package/app/composables/useActiveHeading.ts +90 -0
- package/app/composables/useDuxtAnimatedHeight.ts +155 -0
- package/app/composables/useDuxtBreadcrumb.ts +60 -0
- package/app/composables/useDuxtChoice.ts +28 -0
- package/app/composables/useDuxtCollection.ts +89 -0
- package/app/composables/useDuxtConfig.ts +47 -0
- package/app/composables/useDuxtNavigation.ts +87 -0
- package/app/composables/useDuxtPageFocus.ts +30 -0
- package/app/composables/useDuxtPath.ts +42 -0
- package/app/composables/useDuxtSearch.ts +208 -0
- package/app/composables/useDuxtSection.ts +44 -0
- package/app/composables/useDuxtSectionRow.ts +49 -0
- package/app/composables/useDuxtShortcuts.ts +58 -0
- package/app/composables/useDuxtSiteUrl.ts +27 -0
- package/app/composables/useDuxtToast.ts +25 -0
- package/app/composables/useDuxtVersion.ts +138 -0
- package/app/composables/useFuzzySearch.ts +83 -0
- package/app/composables/usePackageManager.ts +4 -0
- package/app/composables/useRecentPages.ts +52 -0
- package/app/error.vue +133 -0
- package/app/layouts/changelog.vue +85 -0
- package/app/layouts/default.vue +23 -0
- package/app/layouts/docs.vue +62 -0
- package/app/layouts/reference.vue +85 -0
- package/app/lib/utils.ts +7 -0
- package/app/middleware/duxt-section-layout.global.ts +57 -0
- package/app/pages/[...slug].vue +441 -0
- package/app/pages/index.vue +479 -0
- package/app/types/duxt.d.ts +955 -0
- package/app/utils/aside.ts +16 -0
- package/app/utils/changelog.ts +182 -0
- package/app/utils/code-highlight.ts +116 -0
- package/app/utils/duxt-config.ts +185 -0
- package/app/utils/duxt-text.ts +135 -0
- package/app/utils/file-icons.ts +62 -0
- package/app/utils/generated-sections.ts +190 -0
- package/app/utils/generated-toc.ts +216 -0
- package/app/utils/heading-link.ts +32 -0
- package/app/utils/locale-path.ts +45 -0
- package/app/utils/navigation-tree.ts +206 -0
- package/app/utils/nearest-page.ts +72 -0
- package/app/utils/openapi.ts +1028 -0
- package/app/utils/package-command.ts +162 -0
- package/app/utils/request-samples.ts +635 -0
- package/app/utils/section-scope.ts +97 -0
- package/app/utils/sheet-navigation.ts +100 -0
- package/app/utils/source-file.ts +84 -0
- package/app/utils/version-choices.ts +68 -0
- package/app/utils/version-paths.ts +65 -0
- package/bin/duxt-report.mjs +45 -0
- package/components.json +19 -0
- package/content-cache.ts +193 -0
- package/content.config.ts +38 -0
- package/duxt-app-config.ts +124 -0
- package/highlight-langs.ts +68 -0
- package/i18n/i18n.config.ts +16 -0
- package/i18n/locales/de/duxt/changelog.json +17 -0
- package/i18n/locales/de/duxt/code.json +13 -0
- package/i18n/locales/de/duxt/defaults.json +22 -0
- package/i18n/locales/de/duxt/devtools.json +7 -0
- package/i18n/locales/de/duxt/error.json +10 -0
- package/i18n/locales/de/duxt/footer.json +7 -0
- package/i18n/locales/de/duxt/locale.json +7 -0
- package/i18n/locales/de/duxt/nav.json +17 -0
- package/i18n/locales/de/duxt/openapi.json +89 -0
- package/i18n/locales/de/duxt/page.json +34 -0
- package/i18n/locales/de/duxt/search.json +12 -0
- package/i18n/locales/de/duxt/shortcuts.json +11 -0
- package/i18n/locales/de/duxt/theme.json +7 -0
- package/i18n/locales/de/duxt/toc.json +7 -0
- package/i18n/locales/de/duxt/version.json +25 -0
- package/i18n/locales/en/duxt/changelog.json +17 -0
- package/i18n/locales/en/duxt/code.json +13 -0
- package/i18n/locales/en/duxt/defaults.json +22 -0
- package/i18n/locales/en/duxt/devtools.json +7 -0
- package/i18n/locales/en/duxt/error.json +10 -0
- package/i18n/locales/en/duxt/footer.json +7 -0
- package/i18n/locales/en/duxt/locale.json +7 -0
- package/i18n/locales/en/duxt/nav.json +17 -0
- package/i18n/locales/en/duxt/openapi.json +89 -0
- package/i18n/locales/en/duxt/page.json +34 -0
- package/i18n/locales/en/duxt/search.json +12 -0
- package/i18n/locales/en/duxt/shortcuts.json +11 -0
- package/i18n/locales/en/duxt/theme.json +7 -0
- package/i18n/locales/en/duxt/toc.json +7 -0
- package/i18n/locales/en/duxt/version.json +25 -0
- package/i18n/locales/es/duxt/changelog.json +17 -0
- package/i18n/locales/es/duxt/code.json +13 -0
- package/i18n/locales/es/duxt/defaults.json +22 -0
- package/i18n/locales/es/duxt/devtools.json +7 -0
- package/i18n/locales/es/duxt/error.json +10 -0
- package/i18n/locales/es/duxt/footer.json +7 -0
- package/i18n/locales/es/duxt/locale.json +7 -0
- package/i18n/locales/es/duxt/nav.json +17 -0
- package/i18n/locales/es/duxt/openapi.json +89 -0
- package/i18n/locales/es/duxt/page.json +34 -0
- package/i18n/locales/es/duxt/search.json +12 -0
- package/i18n/locales/es/duxt/shortcuts.json +11 -0
- package/i18n/locales/es/duxt/theme.json +7 -0
- package/i18n/locales/es/duxt/toc.json +7 -0
- package/i18n/locales/es/duxt/version.json +25 -0
- package/i18n/locales/fr/duxt/changelog.json +17 -0
- package/i18n/locales/fr/duxt/code.json +13 -0
- package/i18n/locales/fr/duxt/defaults.json +22 -0
- package/i18n/locales/fr/duxt/devtools.json +7 -0
- package/i18n/locales/fr/duxt/error.json +10 -0
- package/i18n/locales/fr/duxt/footer.json +7 -0
- package/i18n/locales/fr/duxt/locale.json +7 -0
- package/i18n/locales/fr/duxt/nav.json +17 -0
- package/i18n/locales/fr/duxt/openapi.json +89 -0
- package/i18n/locales/fr/duxt/page.json +34 -0
- package/i18n/locales/fr/duxt/search.json +12 -0
- package/i18n/locales/fr/duxt/shortcuts.json +11 -0
- package/i18n/locales/fr/duxt/theme.json +7 -0
- package/i18n/locales/fr/duxt/toc.json +7 -0
- package/i18n/locales/fr/duxt/version.json +25 -0
- package/i18n/locales/pt/duxt/changelog.json +17 -0
- package/i18n/locales/pt/duxt/code.json +13 -0
- package/i18n/locales/pt/duxt/defaults.json +22 -0
- package/i18n/locales/pt/duxt/devtools.json +7 -0
- package/i18n/locales/pt/duxt/error.json +10 -0
- package/i18n/locales/pt/duxt/footer.json +7 -0
- package/i18n/locales/pt/duxt/locale.json +7 -0
- package/i18n/locales/pt/duxt/nav.json +17 -0
- package/i18n/locales/pt/duxt/openapi.json +89 -0
- package/i18n/locales/pt/duxt/page.json +34 -0
- package/i18n/locales/pt/duxt/search.json +12 -0
- package/i18n/locales/pt/duxt/shortcuts.json +11 -0
- package/i18n/locales/pt/duxt/theme.json +7 -0
- package/i18n/locales/pt/duxt/toc.json +7 -0
- package/i18n/locales/pt/duxt/version.json +25 -0
- package/i18n/locales/pt-BR/duxt/error.json +8 -0
- package/i18n/locales/pt-BR/duxt/nav.json +10 -0
- package/i18n/locales/pt-BR/duxt/page.json +10 -0
- package/i18n/locales/pt-BR/duxt/search.json +11 -0
- package/i18n/locales/pt-BR/duxt/version.json +13 -0
- package/mdc.config.ts +28 -0
- package/modules/config.ts +440 -0
- package/modules/devtools.ts +126 -0
- package/modules/git-meta.ts +349 -0
- package/modules/redirects.ts +152 -0
- package/modules/validate.ts +137 -0
- package/nuxt.config.ts +530 -0
- package/openapi-model.ts +358 -0
- package/openapi-parse.ts +1052 -0
- package/package.json +161 -0
- package/public/devtools/cache.html +318 -0
- package/public/devtools/checks.html +309 -0
- package/public/devtools/config.html +461 -0
- package/public/devtools/i18n.html +309 -0
- package/public/devtools/pages.html +348 -0
- package/public/devtools/paths.html +315 -0
- package/public/devtools/redirects.html +309 -0
- package/public/devtools/search.html +324 -0
- package/public/devtools/sources.html +309 -0
- package/public/devtools/versions.html +318 -0
- package/report.ts +303 -0
- package/repository-root.ts +26 -0
- package/section-reports.ts +55 -0
- package/sections-changelog.ts +644 -0
- package/sections-openapi.ts +450 -0
- package/sections-resolve.ts +711 -0
- package/sections.ts +157 -0
- package/server/devtools/content.ts +120 -0
- package/server/devtools/context.ts +38 -0
- package/server/devtools/entry-path.ts +30 -0
- package/server/devtools/handler.ts +104 -0
- package/server/devtools/preview.ts +461 -0
- package/server/devtools/render/content.ts +233 -0
- package/server/devtools/render/sources.ts +414 -0
- package/server/devtools/render/system.ts +352 -0
- package/server/devtools/shell.ts +611 -0
- package/server/devtools/sources.ts +122 -0
- package/server/devtools/system.ts +178 -0
- package/server/mcp/tools/list-pages.ts +52 -0
- package/server/mcp/tools/read-page.ts +57 -0
- package/server/mcp/tools/search-docs.ts +74 -0
- package/server/middleware/raw-markdown.ts +86 -0
- package/server/routes/llms-full.txt.get.ts +75 -0
- package/server/routes/llms.txt.get.ts +63 -0
- package/server/routes/rss.xml.get.ts +125 -0
- package/server/utils/duxt-server-text.ts +46 -0
- package/sources-git.ts +113 -0
- package/sources-resolve.ts +908 -0
- package/sources.ts +287 -0
- package/validate-report.ts +494 -0
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
// Type-only, and therefore erased: the section resolver imports the functions
|
|
2
|
+
// below, so a value import here would be a cycle between the two files.
|
|
3
|
+
import type {
|
|
4
|
+
DuxtGeneratedMeta,
|
|
5
|
+
DuxtGeneratedSection
|
|
6
|
+
} from './sections-resolve';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A documentation source: a folder, in this repository or another, at the
|
|
10
|
+
* current checkout or at named refs.
|
|
11
|
+
*/
|
|
12
|
+
export interface DuxtSource {
|
|
13
|
+
/** Folder holding the Markdown, relative to the repository root. */
|
|
14
|
+
path?: string;
|
|
15
|
+
/** `owner/name` or a full git URL. Omitted means this repository. */
|
|
16
|
+
repo?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Refs to publish as versions. Omitted means the current checkout.
|
|
19
|
+
*
|
|
20
|
+
* A bare string is a branch. A tag has to say so — git keeps the two in
|
|
21
|
+
* separate namespaces, and asking for a tag under refs/heads fails the
|
|
22
|
+
* build with "Could not find refs/heads/…".
|
|
23
|
+
*/
|
|
24
|
+
refs?: DuxtRef[];
|
|
25
|
+
/**
|
|
26
|
+
* Languages this source is available in, beyond the one written in `path`.
|
|
27
|
+
*
|
|
28
|
+
* A string is the folder inside `path`: `'de-DE'` reads `docs/de-DE/`. An
|
|
29
|
+
* object overrides where that language lives — its own folder, its own
|
|
30
|
+
* repository, its own ref — which is what lets a translation be maintained by
|
|
31
|
+
* other people, at another pace, in a repository of their own.
|
|
32
|
+
*
|
|
33
|
+
* The DEFAULT locale is the exception: it is the tree in `path` itself, with
|
|
34
|
+
* no folder, so listing it changes nothing. That is what keeps this key
|
|
35
|
+
* additive — a site that adds `locales` does not move the pages it already
|
|
36
|
+
* serves. Which one is the default comes from `defaultLocale`.
|
|
37
|
+
*/
|
|
38
|
+
locales?: DuxtSourceLocale[];
|
|
39
|
+
/**
|
|
40
|
+
* The version THIS source is, where no ref names one.
|
|
41
|
+
*
|
|
42
|
+
* A version is normally a checkout — list `refs` and each becomes one. An API
|
|
43
|
+
* is usually not versioned that way: `openapi/v1.yaml` sits beside
|
|
44
|
+
* `openapi/v2.yaml` in one repository, and without this key the two are two
|
|
45
|
+
* unrelated sections rather than two versions of one document.
|
|
46
|
+
*
|
|
47
|
+
* Named here, everything else follows the ref path exactly: the URL segment,
|
|
48
|
+
* the switcher entry (scoped to the same artefact), the banner and the
|
|
49
|
+
* canonical. `sourceOptions.defaultRef` names which of them is served without
|
|
50
|
+
* a prefix, whether it is a ref or one of these.
|
|
51
|
+
*
|
|
52
|
+
* Never beside `refs` — a source with both would have to be served at two
|
|
53
|
+
* prefixes at once, and the resolver says so rather than picking one.
|
|
54
|
+
*/
|
|
55
|
+
version?: string;
|
|
56
|
+
/** Shown in the version switcher and used in the URL; defaults to the ref. */
|
|
57
|
+
label?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Segment used in the URL for this source; defaults to the repository name.
|
|
60
|
+
*
|
|
61
|
+
* NAMING ONE IS A CLAIM ON A SEGMENT, and that is what makes it more than a
|
|
62
|
+
* spelling: the automatic rule adds a segment to every source once the list
|
|
63
|
+
* holds more than one repository, which cannot express "the documentation at
|
|
64
|
+
* the root, one thing beside it". A source that names a slug gets its segment
|
|
65
|
+
* whether or not the rule fires, and the sources that name none are untouched
|
|
66
|
+
* — so a site adding a second source keeps every URL it already serves.
|
|
67
|
+
*/
|
|
68
|
+
slug?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Lifecycle of every version this entry publishes, unless a ref says
|
|
71
|
+
* otherwise. See `DuxtSourceStatus`.
|
|
72
|
+
*/
|
|
73
|
+
status?: DuxtSourceStatus;
|
|
74
|
+
/**
|
|
75
|
+
* The repository these pages live in, for a source read off disk.
|
|
76
|
+
*
|
|
77
|
+
* Only ever used to link back — "Edit this page", the contributor list.
|
|
78
|
+
* Writing `repo` instead would be the obvious move and the wrong one: `repo`
|
|
79
|
+
* is what makes Content DOWNLOAD a source, so naming this repository there
|
|
80
|
+
* would have the build clone the checkout it is already standing in.
|
|
81
|
+
*/
|
|
82
|
+
origin?: { repo: string; ref?: string };
|
|
83
|
+
/**
|
|
84
|
+
* Read this source's git history for "Last updated" and the contributors.
|
|
85
|
+
*
|
|
86
|
+
* Off by default for a REMOTE source, and the reason is a cost, not a
|
|
87
|
+
* limitation: Content clones a repository with `--depth 1`, so the checkout
|
|
88
|
+
* on disk holds exactly one commit and every file appears to have been
|
|
89
|
+
* written by whoever cut the tip — wrong data rather than missing data.
|
|
90
|
+
* Turning this on has the build unshallow that clone once, which downloads
|
|
91
|
+
* the repository's whole history. Worth it for a docs repo, a real cost for
|
|
92
|
+
* a monorepo, and the consumer is the one who knows which they have.
|
|
93
|
+
*
|
|
94
|
+
* A source read off disk is already a full checkout, so its history is read
|
|
95
|
+
* whether or not this is set.
|
|
96
|
+
*/
|
|
97
|
+
history?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Artefacts beside this source's Markdown, published as pages of the site.
|
|
100
|
+
*
|
|
101
|
+
* Off until declared, the same rule as `feed.path`. A changelog, an OpenAPI
|
|
102
|
+
* document, whatever a registered type can read — see `DuxtGeneratedSection`.
|
|
103
|
+
*/
|
|
104
|
+
generated?: DuxtGeneratedSection[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Where a version sits in its life.
|
|
109
|
+
*
|
|
110
|
+
* Not the same question as "is this the default". A site can publish v2 as the
|
|
111
|
+
* default while v1 is merely older and v0 is genuinely dead, and the reader has
|
|
112
|
+
* to be told which of the three they are in — a badge in the switcher, a banner
|
|
113
|
+
* on the page, and for `eol` no place in the sitemap at all. Driven by the
|
|
114
|
+
* config rather than derived, because only the maintainer knows.
|
|
115
|
+
*
|
|
116
|
+
* `upcoming` is the one that is not about age. A version can be off the default
|
|
117
|
+
* because it has not happened yet, and telling that reader to "upgrade" is
|
|
118
|
+
* exactly backwards — what they need told is that what they are reading may
|
|
119
|
+
* still change.
|
|
120
|
+
*/
|
|
121
|
+
export type DuxtSourceStatus =
|
|
122
|
+
| 'upcoming'
|
|
123
|
+
| 'current'
|
|
124
|
+
| 'maintained'
|
|
125
|
+
| 'deprecated'
|
|
126
|
+
| 'eol';
|
|
127
|
+
|
|
128
|
+
/** A branch by name, or a tag stated as one. */
|
|
129
|
+
export type DuxtRef =
|
|
130
|
+
| string
|
|
131
|
+
| ({ branch: string } & DuxtRefOptions)
|
|
132
|
+
| ({ tag: string } & DuxtRefOptions);
|
|
133
|
+
|
|
134
|
+
interface DuxtRefOptions {
|
|
135
|
+
/** Shown in the switcher and used in the URL; defaults to the ref name. */
|
|
136
|
+
label?: string;
|
|
137
|
+
/** This one version's lifecycle, overriding the source's. */
|
|
138
|
+
status?: DuxtSourceStatus;
|
|
139
|
+
/**
|
|
140
|
+
* The languages THIS version is available in, overriding the source's.
|
|
141
|
+
*
|
|
142
|
+
* Resolved exactly as `status` is (`ref.locales ?? source.locales`), because
|
|
143
|
+
* a translation is usually kept for the current version and not for the two
|
|
144
|
+
* behind it — and a version whose translation nobody maintains is better
|
|
145
|
+
* declared untranslated than served stale.
|
|
146
|
+
*/
|
|
147
|
+
locales?: DuxtSourceLocale[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* One language of a source: the folder it lives in, or where else it lives.
|
|
152
|
+
*
|
|
153
|
+
* The string form is the whole of it for a repository that translates in
|
|
154
|
+
* place. The object form is what the big projects do instead — React keeps
|
|
155
|
+
* `de.react.dev` as its own repository, Vue an entire `vuejs-translations`
|
|
156
|
+
* org — because translators work at their own pace under their own review. A
|
|
157
|
+
* layer that already sources across repositories and refs can offer that as a
|
|
158
|
+
* source entry rather than as a second website.
|
|
159
|
+
*/
|
|
160
|
+
export type DuxtSourceLocale =
|
|
161
|
+
| string
|
|
162
|
+
| {
|
|
163
|
+
/** The locale code, matching one the site serves. */
|
|
164
|
+
locale: string;
|
|
165
|
+
/** Folder holding this language, relative to the repository root. */
|
|
166
|
+
path?: string;
|
|
167
|
+
/** A repository of its own — `owner/name` or a git URL. */
|
|
168
|
+
repo?: string;
|
|
169
|
+
/** A ref of its own, when the translation is versioned separately. */
|
|
170
|
+
ref?: DuxtRef;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** The code of a locale entry, whichever form it takes. */
|
|
174
|
+
export const localeCode = (locale: DuxtSourceLocale): string =>
|
|
175
|
+
typeof locale === 'string' ? locale : locale.locale;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The tag shorthand: the newest one, resolved at build time.
|
|
179
|
+
*
|
|
180
|
+
* Reserved as a ref NAME, so a branch genuinely called `latest` has to be
|
|
181
|
+
* written `{ branch: 'latest' }`. Worth the collision: naming a tag by hand
|
|
182
|
+
* means every release edits the consumer's source list, which is the one thing
|
|
183
|
+
* a version model should not require.
|
|
184
|
+
*/
|
|
185
|
+
export const LATEST = 'latest';
|
|
186
|
+
|
|
187
|
+
/** The name of a ref, whichever kind it is. */
|
|
188
|
+
export const refName = (ref: DuxtRef): string =>
|
|
189
|
+
typeof ref === 'string' ? ref : 'tag' in ref ? ref.tag : ref.branch;
|
|
190
|
+
|
|
191
|
+
/** Is this ref the `latest` shorthand rather than a name git knows? */
|
|
192
|
+
export const isLatestRef = (ref: DuxtRef): boolean =>
|
|
193
|
+
refName(ref) === LATEST && (typeof ref === 'string' || !('branch' in ref));
|
|
194
|
+
|
|
195
|
+
/** A tag has to say so — git keeps the two namespaces apart. */
|
|
196
|
+
export const refIsTag = (ref: DuxtRef): boolean =>
|
|
197
|
+
typeof ref !== 'string' && 'tag' in ref;
|
|
198
|
+
|
|
199
|
+
export interface DuxtSourcesOptions {
|
|
200
|
+
/**
|
|
201
|
+
* Force a repository segment even with a single repository.
|
|
202
|
+
*
|
|
203
|
+
* All or nothing, and deliberately: it answers "does this site have prefixes
|
|
204
|
+
* at all". A single source that wants one uses `slug` instead.
|
|
205
|
+
*/
|
|
206
|
+
showRepo?: boolean;
|
|
207
|
+
/** Force a version segment even with a single version. */
|
|
208
|
+
showVersion?: boolean;
|
|
209
|
+
/** The ref served without a version prefix, by name. Defaults to the first. */
|
|
210
|
+
defaultRef?: string;
|
|
211
|
+
/**
|
|
212
|
+
* The locale whose pages are the tree in `path` itself, with no folder.
|
|
213
|
+
*
|
|
214
|
+
* Defaults to the first entry of the first `locales` list, so a site that
|
|
215
|
+
* never sets it still resolves the same way on both sides. It has to be
|
|
216
|
+
* read from the config rather than from `i18n.defaultLocale`: the
|
|
217
|
+
* collections are declared in `content.config.ts`, which has no access to
|
|
218
|
+
* the Nuxt config, and the two halves must land on identical names.
|
|
219
|
+
*/
|
|
220
|
+
defaultLocale?: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** One resolved source: which collection serves which URL prefix. */
|
|
224
|
+
export interface DuxtResolvedSource {
|
|
225
|
+
/** Collection name Content will register. */
|
|
226
|
+
collection: string;
|
|
227
|
+
/** URL prefix it serves; '' for the root. */
|
|
228
|
+
prefix: string;
|
|
229
|
+
/**
|
|
230
|
+
* The source's own segment, where it has one — because the list holds more
|
|
231
|
+
* than one repository, or because the source named a `slug`.
|
|
232
|
+
*
|
|
233
|
+
* Read as an IDENTITY as much as a segment: the version switcher and the
|
|
234
|
+
* search grouping both scope themselves by it, so two sources with different
|
|
235
|
+
* segments never offer each other's versions.
|
|
236
|
+
*/
|
|
237
|
+
repo?: string;
|
|
238
|
+
/** Version label, when the list has more than one version. */
|
|
239
|
+
version?: string;
|
|
240
|
+
/** True for the version served without a prefix. */
|
|
241
|
+
isDefault: boolean;
|
|
242
|
+
/**
|
|
243
|
+
* Where the pages came from, carried through so the theme can link back to
|
|
244
|
+
* them. "Edit this page", "Last updated" and the contributor list are all
|
|
245
|
+
* this plus a file name — none of them needs config of its own.
|
|
246
|
+
*/
|
|
247
|
+
repository?: string;
|
|
248
|
+
/** The browsable URL of that repository, when there is one. */
|
|
249
|
+
repositoryUrl?: string;
|
|
250
|
+
/** The ref the pages were read at, by name. */
|
|
251
|
+
ref?: string;
|
|
252
|
+
/** Which namespace that ref lives in. */
|
|
253
|
+
refKind?: 'branch' | 'tag';
|
|
254
|
+
/** Folder inside the repository holding the Markdown. */
|
|
255
|
+
path: string;
|
|
256
|
+
/**
|
|
257
|
+
* The locale this collection serves, when the site has translations.
|
|
258
|
+
*
|
|
259
|
+
* Absent on a site with none — which keeps every existing collection name,
|
|
260
|
+
* prefix and query exactly as it was.
|
|
261
|
+
*/
|
|
262
|
+
locale?: string;
|
|
263
|
+
/** True for the locale served from `path` itself, without a folder. */
|
|
264
|
+
isDefaultLocale: boolean;
|
|
265
|
+
/** Where this version sits in its life; `current` unless stated. */
|
|
266
|
+
status: DuxtSourceStatus;
|
|
267
|
+
/** Whether the build may read this source's git history. */
|
|
268
|
+
history: boolean;
|
|
269
|
+
/**
|
|
270
|
+
* Present when this collection is a GENERATED SECTION rather than a docs
|
|
271
|
+
* tree — see `resolveGeneratedSections`.
|
|
272
|
+
*
|
|
273
|
+
* The one question anything downstream asks: the version switcher to know it
|
|
274
|
+
* has nothing to offer here, the layout slot to know which layout to set, and
|
|
275
|
+
* "edit this page" to link at the artefact instead of at a file per page.
|
|
276
|
+
*/
|
|
277
|
+
generated?: DuxtGeneratedMeta;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* A ref or a repository name, as a URL segment.
|
|
282
|
+
*
|
|
283
|
+
* The trim is a loop rather than `/^[-.]+|[-.]+$/`: an anchored `+` over a
|
|
284
|
+
* character class backtracks, so a name that is nothing but separators — which
|
|
285
|
+
* a branch name may be, and which comes from config this layer does not
|
|
286
|
+
* own — costs time quadratic in its length. Scanning from each end costs its
|
|
287
|
+
* length, once.
|
|
288
|
+
*/
|
|
289
|
+
export const slugify = (value: string) => {
|
|
290
|
+
const collapsed = value.replace(/[^a-z0-9.]+/gi, '-');
|
|
291
|
+
|
|
292
|
+
const isSeparator = (char: string | undefined) =>
|
|
293
|
+
char === '-' || char === '.';
|
|
294
|
+
|
|
295
|
+
let start = 0;
|
|
296
|
+
let end = collapsed.length;
|
|
297
|
+
while (start < end && isSeparator(collapsed[start])) start += 1;
|
|
298
|
+
while (end > start && isSeparator(collapsed[end - 1])) end -= 1;
|
|
299
|
+
|
|
300
|
+
return collapsed.slice(start, end);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Collection names are not URL segments.
|
|
305
|
+
*
|
|
306
|
+
* Content requires a valid JavaScript identifier and silently DROPS a
|
|
307
|
+
* collection whose name is not one — a warning in the build log, a version
|
|
308
|
+
* missing from the site, and nothing connecting the two. So the name is
|
|
309
|
+
* derived separately from the prefix: dashes and dots become underscores.
|
|
310
|
+
*/
|
|
311
|
+
/** The name a partial in the default language resolves against. */
|
|
312
|
+
export const PARTIALS_COLLECTION = 'duxt_partials';
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The partials collection one language reads.
|
|
316
|
+
*
|
|
317
|
+
* Named exactly as the page collections are: the default language keeps the
|
|
318
|
+
* bare name a single-language site already had, and every other language
|
|
319
|
+
* appends its code. So a site that declares no `locales` gets the one
|
|
320
|
+
* collection it always got, under the name it always had.
|
|
321
|
+
*
|
|
322
|
+
* Takes the RESOLVED SOURCE rather than a code, because "the default language"
|
|
323
|
+
* is a property of the manifest and not of the string `en`: a site whose
|
|
324
|
+
* default is `de` still calls that collection `duxt_partials`. A bare string is
|
|
325
|
+
* accepted for the build's own grouping, where the default is already
|
|
326
|
+
* `undefined`.
|
|
327
|
+
*/
|
|
328
|
+
export function partialsCollection(
|
|
329
|
+
locale?: Pick<DuxtResolvedSource, 'locale' | 'isDefaultLocale'> | string
|
|
330
|
+
): string {
|
|
331
|
+
const code =
|
|
332
|
+
typeof locale === 'string'
|
|
333
|
+
? locale
|
|
334
|
+
: locale && !locale.isDefaultLocale
|
|
335
|
+
? locale.locale
|
|
336
|
+
: undefined;
|
|
337
|
+
|
|
338
|
+
return code
|
|
339
|
+
? `${PARTIALS_COLLECTION}_${identifier(code)}`
|
|
340
|
+
: PARTIALS_COLLECTION;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* A collection name Content and TypeScript both accept.
|
|
345
|
+
*
|
|
346
|
+
* Exported because the partials collections are named from the same pieces the
|
|
347
|
+
* page collections are, and a second spelling of this rule is a name that
|
|
348
|
+
* drifts the first time a locale carries a character neither expected.
|
|
349
|
+
*/
|
|
350
|
+
export const identifier = (value: string) =>
|
|
351
|
+
value.replace(/[^a-z0-9]+/gi, '_').replace(/^_|_$/g, '');
|
|
352
|
+
|
|
353
|
+
export const repoSlug = (source: DuxtSource) =>
|
|
354
|
+
source.slug ??
|
|
355
|
+
slugify(
|
|
356
|
+
source.repo
|
|
357
|
+
?.split('/')
|
|
358
|
+
.pop()
|
|
359
|
+
?.replace(/\.git$/, '') ?? 'docs'
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* The URL a repository is cloned from.
|
|
364
|
+
*
|
|
365
|
+
* A value starting with `-` is REJECTED rather than passed on: it reaches `git`
|
|
366
|
+
* as an argument, and `git ls-remote --upload-pack=<anything>` runs that
|
|
367
|
+
* anything. The config it comes from is a file in the consumer's repository
|
|
368
|
+
* rather than user input, but a source list may be computed — from an
|
|
369
|
+
* environment variable, from a directory listing — and the cost of the check is
|
|
370
|
+
* one comparison.
|
|
371
|
+
*/
|
|
372
|
+
export const repoUrl = (repo: string) => {
|
|
373
|
+
if (repo.startsWith('-')) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`duxt: a repository may not start with "-" — got ${JSON.stringify(repo)}`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return repo.includes('://') ? repo : `https://github.com/${repo}`;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The locales one source-and-ref combination expands into.
|
|
384
|
+
*
|
|
385
|
+
* `[undefined]` — one entry, no locale — is the shape every site had before
|
|
386
|
+
* this key existed, and the one every site without it still has. That is what
|
|
387
|
+
* keeps the whole feature additive: no `locales`, no second collection, no
|
|
388
|
+
* change to a single name.
|
|
389
|
+
*/
|
|
390
|
+
function localesOf(
|
|
391
|
+
source: DuxtSource,
|
|
392
|
+
ref: DuxtRef | undefined
|
|
393
|
+
): (DuxtSourceLocale | undefined)[] {
|
|
394
|
+
const declared =
|
|
395
|
+
(ref && typeof ref === 'object' ? ref.locales : undefined) ??
|
|
396
|
+
source.locales;
|
|
397
|
+
|
|
398
|
+
return declared?.length ? declared : [undefined];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** The first locale any source declares — the default when none is configured. */
|
|
402
|
+
function firstLocale(sources: DuxtSource[]): string | undefined {
|
|
403
|
+
for (const source of sources) {
|
|
404
|
+
const declared =
|
|
405
|
+
source.locales ??
|
|
406
|
+
source.refs?.flatMap((ref) =>
|
|
407
|
+
typeof ref === 'object' && ref.locales ? ref.locales : []
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
if (declared?.length) return localeCode(declared[0]!);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Where one language of a source actually lives.
|
|
418
|
+
*
|
|
419
|
+
* The default locale is the tree in `path` itself; every other language is a
|
|
420
|
+
* folder inside it unless the entry says otherwise. An object may move the
|
|
421
|
+
* language wholesale — its own folder, repository and ref — which is the case
|
|
422
|
+
* a translation maintained by other people needs.
|
|
423
|
+
*/
|
|
424
|
+
function localeEntry(
|
|
425
|
+
source: DuxtSource,
|
|
426
|
+
locale: DuxtSourceLocale | undefined,
|
|
427
|
+
defaultLocale: string | undefined
|
|
428
|
+
): { path: string; repo?: string; ref?: DuxtRef } {
|
|
429
|
+
const base = source.path ?? 'docs';
|
|
430
|
+
if (!locale) return { path: base, repo: source.repo };
|
|
431
|
+
|
|
432
|
+
const code = localeCode(locale);
|
|
433
|
+
const override = typeof locale === 'string' ? undefined : locale;
|
|
434
|
+
|
|
435
|
+
if (code === defaultLocale) {
|
|
436
|
+
return {
|
|
437
|
+
path: override?.path ?? base,
|
|
438
|
+
repo: override?.repo ?? source.repo,
|
|
439
|
+
ref: override?.ref
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
// A folder INSIDE the source's own path, so one repository holds its
|
|
445
|
+
// translations beside the original — and an override escapes that.
|
|
446
|
+
path: override?.path ?? `${base}/${code}`,
|
|
447
|
+
repo: override?.repo ?? source.repo,
|
|
448
|
+
ref: override?.ref
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Resolve the list once: names, prefixes and labels.
|
|
454
|
+
*
|
|
455
|
+
* Both halves of the layer read this — `duxtSources` to declare the
|
|
456
|
+
* collections, `duxtSourceManifest` to tell the app which collection serves the
|
|
457
|
+
* route it is on. Computing it twice is how the two would drift.
|
|
458
|
+
*
|
|
459
|
+
* Each prefix is switched on by the SHAPE OF THE LIST, not per request: a
|
|
460
|
+
* repository segment once there is more than one repository, a version segment
|
|
461
|
+
* once there is more than one version. A single unversioned source therefore
|
|
462
|
+
* serves `/guide/deploying`, and nothing in the URL betrays that repositories
|
|
463
|
+
* or versions exist at all — which is also what keeps the scheme routable,
|
|
464
|
+
* since the shape is fixed before the first request.
|
|
465
|
+
*/
|
|
466
|
+
/** One expanded combination: a source, at a ref, in a language. */
|
|
467
|
+
export interface DuxtExpandedSource {
|
|
468
|
+
source: DuxtSource;
|
|
469
|
+
ref?: DuxtRef;
|
|
470
|
+
locale?: DuxtSourceLocale;
|
|
471
|
+
/** Where this combination's Markdown actually lives. */
|
|
472
|
+
effective: { path: string; repo?: string; ref?: DuxtRef };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* The one expansion both halves of the layer read.
|
|
477
|
+
*
|
|
478
|
+
* `content.config.ts` declares the collections and the duxt module resolves
|
|
479
|
+
* the manifest, and the two MUST agree entry for entry — the manifest tells
|
|
480
|
+
* the theme which collection serves a route, and a name computed twice is a
|
|
481
|
+
* name that drifts once. Exported for exactly that reason.
|
|
482
|
+
*/
|
|
483
|
+
export function expandSources(
|
|
484
|
+
sources: DuxtSource[],
|
|
485
|
+
options: DuxtSourcesOptions = {}
|
|
486
|
+
): DuxtExpandedSource[] {
|
|
487
|
+
const defaultLocale = options.defaultLocale ?? firstLocale(sources);
|
|
488
|
+
|
|
489
|
+
return sources.flatMap((source) =>
|
|
490
|
+
(source.refs?.length ? source.refs : [undefined]).flatMap((ref) =>
|
|
491
|
+
localesOf(source, ref).map((locale) => ({
|
|
492
|
+
source,
|
|
493
|
+
ref,
|
|
494
|
+
locale,
|
|
495
|
+
effective: localeEntry(source, locale, defaultLocale)
|
|
496
|
+
}))
|
|
497
|
+
)
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function resolveSources(
|
|
502
|
+
sources: DuxtSource[],
|
|
503
|
+
options: DuxtSourcesOptions = {}
|
|
504
|
+
): DuxtResolvedSource[] {
|
|
505
|
+
const defaultLocale = options.defaultLocale ?? firstLocale(sources);
|
|
506
|
+
const expanded = expandSources(sources, options);
|
|
507
|
+
|
|
508
|
+
const repos = new Set(sources.map((source) => source.repo ?? ''));
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* What NAMES a version — a ref, or a source that declares one.
|
|
512
|
+
*
|
|
513
|
+
* A ref is the usual answer and the only one this resolver had: a version was
|
|
514
|
+
* a checkout, so a repository that does not tag its API had no versions at
|
|
515
|
+
* all. That is the common shape for an API — `openapi/v1.yaml` beside
|
|
516
|
+
* `openapi/v2.yaml` in one checkout, versioned by FILE — and it produced two
|
|
517
|
+
* unrelated sections rather than two versions of one document.
|
|
518
|
+
*
|
|
519
|
+
* So a source may name its own version. Everything downstream is unchanged,
|
|
520
|
+
* because everything downstream reads this name: the URL segment, the
|
|
521
|
+
* switcher entry, the banner, the canonical.
|
|
522
|
+
*/
|
|
523
|
+
const names = new Set(
|
|
524
|
+
expanded
|
|
525
|
+
.map((entry) => (entry.ref ? refName(entry.ref) : entry.source.version))
|
|
526
|
+
.filter(Boolean) as string[]
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
const withRepo = options.showRepo ?? repos.size > 1;
|
|
530
|
+
const withVersion = options.showVersion ?? names.size > 1;
|
|
531
|
+
const defaultRef = options.defaultRef ?? [...names][0];
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Does THIS source get a segment of its own?
|
|
535
|
+
*
|
|
536
|
+
* The list-wide rule above, or the source's own `slug` — see there for why a
|
|
537
|
+
* slug is a claim rather than a spelling. Per source rather than per list,
|
|
538
|
+
* which is the whole difference: a site whose docs sit at the root can hang
|
|
539
|
+
* one prefixed source beside them without moving a single existing URL.
|
|
540
|
+
*/
|
|
541
|
+
const segmented = (source: DuxtSource) => withRepo || Boolean(source.slug);
|
|
542
|
+
|
|
543
|
+
const resolved: DuxtResolvedSource[] = [];
|
|
544
|
+
const taken = new Map<string, string>();
|
|
545
|
+
|
|
546
|
+
for (const { source, ref, locale, effective: entry } of expanded) {
|
|
547
|
+
const effectiveRef = entry.ref ?? ref;
|
|
548
|
+
|
|
549
|
+
// TWO TRUTHS ABOUT ONE SOURCE is not a state this resolver can be in: a
|
|
550
|
+
// source that both lists refs and names a version would have to be served
|
|
551
|
+
// at two prefixes at once, and the reader would meet the same document
|
|
552
|
+
// twice in the switcher.
|
|
553
|
+
if (effectiveRef && source.version) {
|
|
554
|
+
throw new Error(
|
|
555
|
+
`duxt: the source "${source.path ?? source.repo ?? ''}" names the ` +
|
|
556
|
+
`version "${source.version}" and lists refs. A version comes from ` +
|
|
557
|
+
'a ref or from this key, never from both.'
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const name = effectiveRef ? refName(effectiveRef) : source.version;
|
|
562
|
+
const label =
|
|
563
|
+
(ref && typeof ref === 'object' ? ref.label : undefined) ?? source.label;
|
|
564
|
+
const code = locale ? localeCode(locale) : undefined;
|
|
565
|
+
const isDefaultLocale = !code || code === defaultLocale;
|
|
566
|
+
const version = name ? slugify(label ?? name) : undefined;
|
|
567
|
+
const isDefault = !name || name === defaultRef;
|
|
568
|
+
|
|
569
|
+
const segments: string[] = [];
|
|
570
|
+
if (segmented(source)) segments.push(repoSlug(source));
|
|
571
|
+
if (withVersion && version && !isDefault) segments.push(version);
|
|
572
|
+
|
|
573
|
+
// THE LOCALE IS NOT PART OF THE PREFIX. @nuxtjs/i18n already puts it in
|
|
574
|
+
// front of the path, so a collection carrying it too would spell it twice
|
|
575
|
+
// in one URL. Original and translation therefore live under IDENTICAL
|
|
576
|
+
// content paths in different collections — which leaves every path
|
|
577
|
+
// comparison in the theme untouched and makes the fallback a second query
|
|
578
|
+
// for the same path.
|
|
579
|
+
const prefix = segments.length ? `/${segments.join('/')}` : '';
|
|
580
|
+
const collection =
|
|
581
|
+
['docs', ...(isDefaultLocale ? [] : [code!]), ...segments]
|
|
582
|
+
.map(identifier)
|
|
583
|
+
.join('_') || 'docs';
|
|
584
|
+
|
|
585
|
+
// Claimed per locale: two languages serving one prefix is not a collision,
|
|
586
|
+
// it is the point.
|
|
587
|
+
const claim = `${code ?? ''}|${prefix}`;
|
|
588
|
+
const previous = taken.get(claim);
|
|
589
|
+
if (previous) {
|
|
590
|
+
// The one ambiguity the build-time decision leaves: a docs folder named
|
|
591
|
+
// like a repository or a version. Rejected rather than resolved silently.
|
|
592
|
+
throw new Error(
|
|
593
|
+
`duxt: two sources resolve to the same URL prefix "${prefix || '/'}" ` +
|
|
594
|
+
`(${previous} and ${source.repo ?? 'this repository'}${ref ? `@${ref}` : ''}). ` +
|
|
595
|
+
'Give one of them a `slug` or a `label`.'
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
taken.set(
|
|
599
|
+
claim,
|
|
600
|
+
`${source.repo ?? 'this repository'}${name ? `@${name}` : ''}`
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
resolved.push({
|
|
604
|
+
collection,
|
|
605
|
+
prefix,
|
|
606
|
+
repo: segmented(source) ? repoSlug(source) : undefined,
|
|
607
|
+
version,
|
|
608
|
+
isDefault,
|
|
609
|
+
repository: entry.repo ?? source.origin?.repo,
|
|
610
|
+
repositoryUrl: entry.repo
|
|
611
|
+
? repoUrl(entry.repo)
|
|
612
|
+
: source.origin?.repo
|
|
613
|
+
? repoUrl(source.origin.repo)
|
|
614
|
+
: undefined,
|
|
615
|
+
ref: name ?? source.origin?.ref,
|
|
616
|
+
refKind: effectiveRef
|
|
617
|
+
? refIsTag(effectiveRef)
|
|
618
|
+
? 'tag'
|
|
619
|
+
: 'branch'
|
|
620
|
+
: undefined,
|
|
621
|
+
path: entry.path,
|
|
622
|
+
locale: code,
|
|
623
|
+
isDefaultLocale,
|
|
624
|
+
status:
|
|
625
|
+
(ref && typeof ref === 'object' ? ref.status : undefined) ??
|
|
626
|
+
source.status ??
|
|
627
|
+
'current',
|
|
628
|
+
// A local source is a full checkout already; a remote one has to be
|
|
629
|
+
// unshallowed, which is why it has to be asked for.
|
|
630
|
+
history: entry.repo ? (source.history ?? false) : true
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return resolved;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* The locales a page may be served from, best first.
|
|
639
|
+
*
|
|
640
|
+
* A translated Markdown file cannot be merged the way a locale FILE is: a page
|
|
641
|
+
* is translated whole or not at all, so this is a chain of choices rather than
|
|
642
|
+
* a deep merge. It follows the rule the layer already applies twice — the
|
|
643
|
+
* `files` arrays in `nuxt.config.ts` and `resolveDuxtText` both let the
|
|
644
|
+
* language carry the content and the region carry only its deviations:
|
|
645
|
+
*
|
|
646
|
+
* 1. the locale itself — `de-DE` reads `de-DE`;
|
|
647
|
+
* 2. its base language — `de-DE` reads a `de` tree, which is what lets one
|
|
648
|
+
* `pt/` folder serve both `pt-PT` and `pt-BR`;
|
|
649
|
+
* 3. a sibling of the same language — `pt-BR` reads `pt-PT` before it gives
|
|
650
|
+
* up on Portuguese and falls back to English;
|
|
651
|
+
* 4. `fallbackLocale`, the value vue-i18n already carries for missing keys,
|
|
652
|
+
* so the interface and the pages agree on where they fall back to;
|
|
653
|
+
* 5. the default locale, which is the untranslated original.
|
|
654
|
+
*
|
|
655
|
+
* `undefined` closes every chain: it is the collection of a site that declares
|
|
656
|
+
* no locales at all, and the one a translation ultimately falls back to.
|
|
657
|
+
*/
|
|
658
|
+
export function localeChain(
|
|
659
|
+
locale: string | undefined,
|
|
660
|
+
available: (string | undefined)[],
|
|
661
|
+
fallbackLocale?: string | string[]
|
|
662
|
+
): (string | undefined)[] {
|
|
663
|
+
const has = (code: string | undefined) => available.includes(code);
|
|
664
|
+
const chain: (string | undefined)[] = [];
|
|
665
|
+
|
|
666
|
+
const add = (code: string | undefined) => {
|
|
667
|
+
if (!chain.includes(code) && has(code)) chain.push(code);
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
if (locale) {
|
|
671
|
+
add(locale);
|
|
672
|
+
|
|
673
|
+
const base = locale.split('-')[0]!;
|
|
674
|
+
add(base);
|
|
675
|
+
|
|
676
|
+
// A sibling region of the same language, in the order the site declares
|
|
677
|
+
// them: `pt-BR` takes `pt-PT` over English, every time.
|
|
678
|
+
for (const code of available) {
|
|
679
|
+
if (code && code !== locale && code.split('-')[0] === base) add(code);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
for (const code of [fallbackLocale ?? []].flat()) add(code);
|
|
684
|
+
|
|
685
|
+
// The original. Always last, and always reachable — a page that exists in no
|
|
686
|
+
// translation still has to render.
|
|
687
|
+
if (!chain.includes(undefined)) chain.push(undefined);
|
|
688
|
+
|
|
689
|
+
return chain;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* The resolved list, as data the app can read.
|
|
694
|
+
*
|
|
695
|
+
* Kept out of `sources.ts` deliberately: that file imports `@nuxt/content` to
|
|
696
|
+
* declare collections, and importing a module's entry point from client code is
|
|
697
|
+
* rejected by the bundler. This file is plain logic, so `app.config.ts` can read
|
|
698
|
+
* it and both halves still resolve the list exactly once.
|
|
699
|
+
*
|
|
700
|
+
* THE DOCUMENTATION HALF ONLY. `duxtSources` walks this list index-for-index
|
|
701
|
+
* against `expandSources`, so a generated section appended here would put the
|
|
702
|
+
* two out of step; `duxtManifest` in `sections-resolve.ts` is the whole
|
|
703
|
+
* manifest, and what everything serving a site reads.
|
|
704
|
+
*/
|
|
705
|
+
export function duxtSourceManifest(
|
|
706
|
+
sources: DuxtSource[],
|
|
707
|
+
options: DuxtSourcesOptions = {}
|
|
708
|
+
): DuxtResolvedSource[] {
|
|
709
|
+
return resolveSources(sources, options);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Order two version-ish tag names the way a release list is ordered.
|
|
714
|
+
*
|
|
715
|
+
* Semver order, NOT tag date: a patch cut for an old line after the newest
|
|
716
|
+
* minor would otherwise become "latest" and move the whole site back a version.
|
|
717
|
+
* A `v` prefix is optional, a pre-release sorts below the release it precedes,
|
|
718
|
+
* and anything that is not a version at all sorts last so it can never win.
|
|
719
|
+
*/
|
|
720
|
+
export function compareVersionTags(a: string, b: string): number {
|
|
721
|
+
const parse = (value: string) => {
|
|
722
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(value.trim());
|
|
723
|
+
if (!match) return undefined;
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
numbers: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
727
|
+
pre: match[4]
|
|
728
|
+
};
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
const left = parse(a);
|
|
732
|
+
const right = parse(b);
|
|
733
|
+
|
|
734
|
+
if (!left || !right) return left ? -1 : right ? 1 : a.localeCompare(b);
|
|
735
|
+
|
|
736
|
+
for (let index = 0; index < 3; index++) {
|
|
737
|
+
const difference = right.numbers[index]! - left.numbers[index]!;
|
|
738
|
+
if (difference) return difference;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// 1.0.0 outranks 1.0.0-rc.1; two pre-releases fall back to their own order.
|
|
742
|
+
if (left.pre && !right.pre) return 1;
|
|
743
|
+
if (right.pre && !left.pre) return -1;
|
|
744
|
+
if (left.pre && right.pre) return right.pre.localeCompare(left.pre);
|
|
745
|
+
|
|
746
|
+
return 0;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** The newest tag in a list, by `compareVersionTags`. */
|
|
750
|
+
export function newestTag(tags: string[]): string | undefined {
|
|
751
|
+
return [...tags].sort(compareVersionTags)[0];
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* The URL segments a source's own folders must not use.
|
|
756
|
+
*
|
|
757
|
+
* With a prefix active, `/workflows` may be a repository, a version or a docs
|
|
758
|
+
* folder — and the resolver settles which at build time, so the folder is the
|
|
759
|
+
* one that loses. This names the collision rather than resolving it silently:
|
|
760
|
+
* for each source, the segments that any OTHER source's prefix claims one level
|
|
761
|
+
* below its own.
|
|
762
|
+
*/
|
|
763
|
+
export function reservedSegments(
|
|
764
|
+
resolved: DuxtResolvedSource[]
|
|
765
|
+
): Map<string, Set<string>> {
|
|
766
|
+
const reserved = new Map<string, Set<string>>();
|
|
767
|
+
|
|
768
|
+
for (const source of resolved) {
|
|
769
|
+
const claimed = new Set<string>();
|
|
770
|
+
|
|
771
|
+
for (const other of resolved) {
|
|
772
|
+
if (other === source || !other.prefix) continue;
|
|
773
|
+
if (other.prefix === source.prefix) continue;
|
|
774
|
+
|
|
775
|
+
const inside = source.prefix
|
|
776
|
+
? other.prefix.startsWith(`${source.prefix}/`) &&
|
|
777
|
+
other.prefix.slice(source.prefix.length + 1)
|
|
778
|
+
: other.prefix.slice(1);
|
|
779
|
+
|
|
780
|
+
if (!inside) continue;
|
|
781
|
+
|
|
782
|
+
const segment = inside.split('/')[0];
|
|
783
|
+
if (segment) claimed.add(segment);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
reserved.set(source.collection, claimed);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return reserved;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Is the version being read older or newer than the one served by default?
|
|
794
|
+
*
|
|
795
|
+
* The banner needs this and cannot get it from `isDefault`, which only says
|
|
796
|
+
* "not the current one" — and that covers both a release from two years ago and
|
|
797
|
+
* a branch documenting next month's. Telling the second reader to upgrade is
|
|
798
|
+
* exactly backwards.
|
|
799
|
+
*
|
|
800
|
+
* Only answered when BOTH names are versions. A branch is not comparable to a
|
|
801
|
+
* tag: `main` sorts nowhere, and guessing would have said `v0.7.0` was newer
|
|
802
|
+
* than `main` — which is how the banner came to offer a downgrade as an
|
|
803
|
+
* upgrade. Where it cannot be answered, the config's own `status` is what
|
|
804
|
+
* decides, because only the maintainer knows what a branch called `next` is.
|
|
805
|
+
*/
|
|
806
|
+
export function versionRelation(
|
|
807
|
+
version: string | undefined,
|
|
808
|
+
preferred: string | undefined
|
|
809
|
+
): 'older' | 'newer' | 'same' | 'unknown' {
|
|
810
|
+
if (!version || !preferred) return 'unknown';
|
|
811
|
+
if (version === preferred) return 'same';
|
|
812
|
+
|
|
813
|
+
const isVersion = (value: string) =>
|
|
814
|
+
/^v?\d+\.\d+\.\d+(?:-.+)?$/.test(value.trim());
|
|
815
|
+
if (!isVersion(version) || !isVersion(preferred)) return 'unknown';
|
|
816
|
+
|
|
817
|
+
const order = compareVersionTags(version, preferred);
|
|
818
|
+
|
|
819
|
+
// `compareVersionTags` sorts newest FIRST, so a negative result means this
|
|
820
|
+
// version leads the list — it is the newer of the two.
|
|
821
|
+
return order === 0 ? 'same' : order < 0 ? 'newer' : 'older';
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* The collections that serve one route, best first.
|
|
826
|
+
*
|
|
827
|
+
* Two decisions in one, because neither is complete without the other: WHICH
|
|
828
|
+
* PREFIX claims the path, and WHICH LANGUAGE of that prefix the reader gets.
|
|
829
|
+
* The locale is deliberately not part of a prefix — `@nuxtjs/i18n` already puts
|
|
830
|
+
* it in front of the path — so original and translation are two collections
|
|
831
|
+
* behind one identical prefix, and a lookup handed only the path cannot tell
|
|
832
|
+
* them apart. Whichever sorted first won, which is the bug this exists to close.
|
|
833
|
+
*
|
|
834
|
+
* `path` is the DOCUMENTATION path, locale segment already stripped, and
|
|
835
|
+
* `locale` is what was stripped off it. The rest is `localeChain`, so the theme
|
|
836
|
+
* and every view that debugs it fall back through the same order.
|
|
837
|
+
*
|
|
838
|
+
* Never empty while `sources` is not: a route whose manifest has not arrived
|
|
839
|
+
* still has to query something, and an empty chain 404s a page that exists.
|
|
840
|
+
*/
|
|
841
|
+
export function sourcesForRoute(
|
|
842
|
+
path: string,
|
|
843
|
+
locale: string | undefined,
|
|
844
|
+
sources: DuxtResolvedSource[],
|
|
845
|
+
fallbackLocale?: string | string[]
|
|
846
|
+
): DuxtResolvedSource[] {
|
|
847
|
+
if (!sources.length) return [];
|
|
848
|
+
|
|
849
|
+
// Read the prefix off the DEFAULT-language entries: every language of one
|
|
850
|
+
// source shares the prefix, so the pool only has to be free of duplicates.
|
|
851
|
+
const withDefaultLocale = sources.filter((source) => source.isDefaultLocale);
|
|
852
|
+
const pool = withDefaultLocale.length ? withDefaultLocale : sources;
|
|
853
|
+
|
|
854
|
+
const prefix =
|
|
855
|
+
(
|
|
856
|
+
[...pool]
|
|
857
|
+
.sort((a, b) => b.prefix.length - a.prefix.length)
|
|
858
|
+
.find(
|
|
859
|
+
(source) => !source.prefix || isInsidePrefix(path, source.prefix)
|
|
860
|
+
) ??
|
|
861
|
+
pool.find((source) => !source.prefix) ??
|
|
862
|
+
// The landing page matches no prefix on a site whose every source has
|
|
863
|
+
// one. It still needs a real collection for the navigation the header
|
|
864
|
+
// draws, and a literal `docs` names one such a site does not have.
|
|
865
|
+
pool[0]
|
|
866
|
+
)?.prefix ?? '';
|
|
867
|
+
|
|
868
|
+
const candidates = sources.filter((source) => source.prefix === prefix);
|
|
869
|
+
|
|
870
|
+
// Deduplicated, because two links of the chain routinely land on ONE
|
|
871
|
+
// collection: `fallbackLocale` is usually the default locale, and the chain
|
|
872
|
+
// ends at the untranslated original regardless — so `en` and `undefined` both
|
|
873
|
+
// resolve to the same entry, and the panel printed "falls back to docs → docs".
|
|
874
|
+
const ordered = [
|
|
875
|
+
...new Set(
|
|
876
|
+
localeChain(
|
|
877
|
+
locale,
|
|
878
|
+
candidates.map((source) => source.locale),
|
|
879
|
+
fallbackLocale
|
|
880
|
+
)
|
|
881
|
+
.map((code) =>
|
|
882
|
+
candidates.find((source) =>
|
|
883
|
+
code === undefined ? source.isDefaultLocale : source.locale === code
|
|
884
|
+
)
|
|
885
|
+
)
|
|
886
|
+
.filter(Boolean) as DuxtResolvedSource[]
|
|
887
|
+
)
|
|
888
|
+
];
|
|
889
|
+
|
|
890
|
+
if (ordered.length) return ordered;
|
|
891
|
+
if (candidates.length) return candidates.slice(0, 1);
|
|
892
|
+
|
|
893
|
+
return sources.slice(0, 1);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Is `path` inside `prefix`? Segment-aware — `/workflows-old` is not inside
|
|
898
|
+
* `/workflows`, though it starts with it.
|
|
899
|
+
*
|
|
900
|
+
* The same rule as `isInside` in `app/utils/version-paths.ts`, and duplicated
|
|
901
|
+
* rather than imported: that file imports from THIS one, and a cycle between
|
|
902
|
+
* them is what `app.config.ts` reading this module cannot survive.
|
|
903
|
+
*/
|
|
904
|
+
function isInsidePrefix(path: string, prefix: string): boolean {
|
|
905
|
+
if (!prefix) return true;
|
|
906
|
+
|
|
907
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
908
|
+
}
|