@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,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The checks themselves, over data rather than over a build.
|
|
3
|
+
*
|
|
4
|
+
* Split from `modules/validate.ts` for the reason `sources-resolve.ts` is split
|
|
5
|
+
* from `sources.ts`: the module half imports `node:url`, `@nuxt/schema` and the
|
|
6
|
+
* jiti-backed config reader, none of which belong in a bundle. This half is
|
|
7
|
+
* plain data in, findings out — so a test can run it, and so the devtools panel
|
|
8
|
+
* can run the SAME checks against the pages the site is actually serving,
|
|
9
|
+
* rather than reprinting a build log that has already scrolled away.
|
|
10
|
+
*/
|
|
11
|
+
import { reservedSegments } from './sources-resolve';
|
|
12
|
+
import { packageCommandIssues } from './app/utils/package-command';
|
|
13
|
+
|
|
14
|
+
export interface PageRecord {
|
|
15
|
+
collection: string;
|
|
16
|
+
path: string;
|
|
17
|
+
file: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
anchors: Set<string>;
|
|
21
|
+
links: { href: string }[];
|
|
22
|
+
/**
|
|
23
|
+
* When the file last changed, as `git-meta` read it off the history.
|
|
24
|
+
*
|
|
25
|
+
* Absent for a source that did not ask for its history, and for one whose
|
|
26
|
+
* clone could not be deepened — which is why the staleness half of the
|
|
27
|
+
* translation report is skipped rather than guessed when it is missing.
|
|
28
|
+
*/
|
|
29
|
+
lastUpdated?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The `command` of every `::package-managers` block on the page.
|
|
32
|
+
*
|
|
33
|
+
* Read off the parsed body rather than the source, because that is where the
|
|
34
|
+
* prop has already been resolved — see `walk`.
|
|
35
|
+
*/
|
|
36
|
+
commands?: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The manifest, as much of it as the checks read. */
|
|
40
|
+
export interface SourceRecord {
|
|
41
|
+
collection: string;
|
|
42
|
+
prefix: string;
|
|
43
|
+
locale?: string;
|
|
44
|
+
isDefaultLocale?: boolean;
|
|
45
|
+
/** The artefact a generated section was read from, for its findings. */
|
|
46
|
+
path?: string;
|
|
47
|
+
/** Where that artefact lives, for a finding that has to name it. */
|
|
48
|
+
repository?: string;
|
|
49
|
+
repositoryUrl?: string;
|
|
50
|
+
ref?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Set when this collection is a GENERATED SECTION rather than a docs tree.
|
|
53
|
+
*
|
|
54
|
+
* Checked DIFFERENTLY rather than skipped. An empty section is a finding
|
|
55
|
+
* about the artefact and not about a docs folder, so it is worded and graded
|
|
56
|
+
* from `report` instead of from the collection rule; a page split out of a
|
|
57
|
+
* changelog has no field a `description` could come from, so it is not asked
|
|
58
|
+
* for one; and what the type could not read is a finding no other check could
|
|
59
|
+
* ever have produced.
|
|
60
|
+
*/
|
|
61
|
+
generated?: GeneratedRecord;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What a generated section carries into the checks. */
|
|
65
|
+
export interface GeneratedRecord {
|
|
66
|
+
type: string;
|
|
67
|
+
label: string;
|
|
68
|
+
remote?: boolean;
|
|
69
|
+
report?: {
|
|
70
|
+
pages: number;
|
|
71
|
+
warnings: string[];
|
|
72
|
+
missing?: boolean;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** What one pass over a parsed MDC body picks up. */
|
|
77
|
+
export interface Collected {
|
|
78
|
+
anchors: Set<string>;
|
|
79
|
+
links: { href: string }[];
|
|
80
|
+
commands: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Collect anchor ids, internal links and command blocks out of a parsed MDC
|
|
85
|
+
* body.
|
|
86
|
+
*
|
|
87
|
+
* ONE ACCUMULATOR rather than a parameter per kind: the third thing to collect
|
|
88
|
+
* is the point at which a positional list stops reading as a signature and
|
|
89
|
+
* starts reading as an argument order to get wrong.
|
|
90
|
+
*/
|
|
91
|
+
export function walk(node: unknown, into: Collected): void {
|
|
92
|
+
if (Array.isArray(node)) {
|
|
93
|
+
const [tag, props] = node as [unknown, Record<string, unknown> | undefined];
|
|
94
|
+
|
|
95
|
+
if (typeof tag === 'string' && props && typeof props === 'object') {
|
|
96
|
+
if (typeof props.id === 'string') into.anchors.add(props.id);
|
|
97
|
+
|
|
98
|
+
if (tag === 'a' && typeof props.href === 'string') {
|
|
99
|
+
into.links.push({ href: props.href });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (tag === 'package-managers' && typeof props.command === 'string') {
|
|
103
|
+
into.commands.push(props.command);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const child of node) walk(child, into);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (node && typeof node === 'object') {
|
|
112
|
+
for (const value of Object.values(node as Record<string, unknown>)) {
|
|
113
|
+
walk(value, into);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function report(
|
|
119
|
+
sources: SourceRecord[],
|
|
120
|
+
pages: PageRecord[]
|
|
121
|
+
): { errors: string[]; warnings: string[]; notes: string[] } {
|
|
122
|
+
const errors: string[] = [];
|
|
123
|
+
const warnings: string[] = [];
|
|
124
|
+
|
|
125
|
+
const byCollection = new Map<string, PageRecord[]>();
|
|
126
|
+
for (const page of pages) {
|
|
127
|
+
const list = byCollection.get(page.collection) ?? [];
|
|
128
|
+
list.push(page);
|
|
129
|
+
byCollection.set(page.collection, list);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 1. A collection with nothing in it. The symptom is an empty sidebar and a
|
|
133
|
+
// 404 on every page of one version — never a message.
|
|
134
|
+
//
|
|
135
|
+
// A GENERATED SECTION answers the same question from its own artefact, at
|
|
136
|
+
// its own severity and in its own words — see `sectionFindings`. It used
|
|
137
|
+
// to be skipped here and reported by a `console.warn` where the file is
|
|
138
|
+
// read, which put half of this layer's findings in a channel that has
|
|
139
|
+
// scrolled away by the time anyone looks at the other half.
|
|
140
|
+
for (const source of sources) {
|
|
141
|
+
if (source.generated) {
|
|
142
|
+
warnings.push(
|
|
143
|
+
...sectionFindings(source, byCollection.get(source.collection)?.length)
|
|
144
|
+
);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!byCollection.get(source.collection)?.length) {
|
|
149
|
+
errors.push(
|
|
150
|
+
`collection "${source.collection}" (serving "${source.prefix || '/'}") ` +
|
|
151
|
+
"contains no pages. Check the source's `path` and `refs`."
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 2. A docs folder named like a repository or a version segment. The prefix
|
|
157
|
+
// wins, so the folder is simply unreachable.
|
|
158
|
+
const reserved = reservedSegments(sources as never);
|
|
159
|
+
|
|
160
|
+
for (const source of sources) {
|
|
161
|
+
const claimed = reserved.get(source.collection);
|
|
162
|
+
if (!claimed?.size) continue;
|
|
163
|
+
|
|
164
|
+
for (const page of byCollection.get(source.collection) ?? []) {
|
|
165
|
+
const rest = source.prefix
|
|
166
|
+
? page.path.slice(source.prefix.length)
|
|
167
|
+
: page.path;
|
|
168
|
+
const segment = rest.split('/')[1];
|
|
169
|
+
|
|
170
|
+
if (segment && claimed.has(segment)) {
|
|
171
|
+
errors.push(
|
|
172
|
+
`"${page.file}" sits in a folder called "${segment}", which is ` +
|
|
173
|
+
`already a repository or version segment under "${source.prefix || '/'}". ` +
|
|
174
|
+
'Rename the folder, or give the source a `slug`.'
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 3. Frontmatter. Neither field breaks a page; both quietly degrade the
|
|
181
|
+
// table of contents, the OG image and llms.txt.
|
|
182
|
+
//
|
|
183
|
+
// A generated page is asked for a title and not for a description: the
|
|
184
|
+
// title is the type's to produce and a missing one is a bug in the parser,
|
|
185
|
+
// where a description would have to be invented — a changelog entry has no
|
|
186
|
+
// field that could carry one.
|
|
187
|
+
const isGenerated = new Set(
|
|
188
|
+
sources.filter((source) => source.generated).map((s) => s.collection)
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
for (const page of pages) {
|
|
192
|
+
const missing = [
|
|
193
|
+
!page.title && 'title',
|
|
194
|
+
!page.description && !isGenerated.has(page.collection) && 'description'
|
|
195
|
+
].filter(Boolean);
|
|
196
|
+
|
|
197
|
+
if (missing.length) {
|
|
198
|
+
warnings.push(`"${page.file}" has no ${missing.join(' and no ')}.`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 4. Links. Internal ones only — an external URL is not this build's to
|
|
203
|
+
// verify, and checking it would put the network in the build.
|
|
204
|
+
//
|
|
205
|
+
// Resolved exactly as `ProseA` resolves them at render time: an absolute
|
|
206
|
+
// path written in a page is relative to that page's OWN source, so it is
|
|
207
|
+
// tried under the source's prefix first and bare second. Checking only the
|
|
208
|
+
// bare form reports every correct link on a prefixed site.
|
|
209
|
+
//
|
|
210
|
+
// PER LANGUAGE, and that is not a refinement. Every language of one source
|
|
211
|
+
// serves IDENTICAL content paths — the locale lives in front of the URL,
|
|
212
|
+
// not in the content tree — so a map keyed by path alone holds one page per
|
|
213
|
+
// path and whichever language was parsed last wins. Anchors are derived
|
|
214
|
+
// from heading TEXT, so that map would check a German link against English
|
|
215
|
+
// headings and report every correct anchor on a translated site. A link is
|
|
216
|
+
// therefore resolved in the linking page's OWN collection first, and in the
|
|
217
|
+
// untranslated original second — which is where the site itself falls back
|
|
218
|
+
// when a language does not carry the page.
|
|
219
|
+
const known = new Map<string, PageRecord[]>();
|
|
220
|
+
for (const page of pages) {
|
|
221
|
+
const list = known.get(page.path) ?? [];
|
|
222
|
+
list.push(page);
|
|
223
|
+
known.set(page.path, list);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const prefixOf = new Map(
|
|
227
|
+
sources.map((source) => [source.collection, source.prefix])
|
|
228
|
+
);
|
|
229
|
+
const originals = new Set(
|
|
230
|
+
sources
|
|
231
|
+
.filter((source) => !source.locale || source.isDefaultLocale)
|
|
232
|
+
.map((source) => source.collection)
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const resolve = (path: string, from: PageRecord) => {
|
|
236
|
+
const candidates = known.get(stripTrailingSlash(path));
|
|
237
|
+
if (!candidates) return undefined;
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
candidates.find((page) => page.collection === from.collection) ??
|
|
241
|
+
candidates.find((page) => originals.has(page.collection)) ??
|
|
242
|
+
candidates[0]
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/** A percent-encoded fragment, or the fragment itself where it is not one. */
|
|
247
|
+
const decodeAnchor = (fragment: string) => {
|
|
248
|
+
try {
|
|
249
|
+
return decodeURIComponent(fragment);
|
|
250
|
+
} catch {
|
|
251
|
+
// A stray `%` is a link nobody can follow either way, and the anchor
|
|
252
|
+
// check is not the place to say so.
|
|
253
|
+
return fragment;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
for (const page of pages) {
|
|
258
|
+
const prefix = prefixOf.get(page.collection) ?? '';
|
|
259
|
+
|
|
260
|
+
for (const link of page.links) {
|
|
261
|
+
const { href } = link;
|
|
262
|
+
if (!href.startsWith('/') && !href.startsWith('#')) continue;
|
|
263
|
+
|
|
264
|
+
const [target, fragment] = href.split('#');
|
|
265
|
+
|
|
266
|
+
// DECODED, because the two halves are written in different alphabets. A
|
|
267
|
+
// heading's id is the text as it stands — `icônes` — while the link
|
|
268
|
+
// arrives percent-encoded, so comparing them as written reported every
|
|
269
|
+
// accented anchor on the site as missing and told the author to point at
|
|
270
|
+
// a heading that was already there.
|
|
271
|
+
const anchor = fragment && decodeAnchor(fragment);
|
|
272
|
+
const destination = target
|
|
273
|
+
? (resolve(`${prefix}${target}`, page) ?? resolve(target, page))
|
|
274
|
+
: page;
|
|
275
|
+
|
|
276
|
+
if (target && !destination) {
|
|
277
|
+
warnings.push(
|
|
278
|
+
`"${page.file}" links to "${href}", which no page serves.`
|
|
279
|
+
);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (anchor && destination && !destination.anchors.has(anchor)) {
|
|
284
|
+
warnings.push(
|
|
285
|
+
`"${page.file}" links to "${href}", but that page has no "${anchor}" heading.`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
commandWarnings(pages, warnings);
|
|
292
|
+
|
|
293
|
+
return { errors, warnings, notes: translationNotes(sources, byCollection) };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* What a `::package-managers` block asks for that a manager cannot say.
|
|
298
|
+
*
|
|
299
|
+
* NEITHER FAULT FAILS ANYTHING, and both are otherwise invisible. A manager with
|
|
300
|
+
* no equivalent silently loses its tab, so a page written as
|
|
301
|
+
* `command="outdated"` shows three tabs where the site offers four and nothing
|
|
302
|
+
* anywhere says why. An unknown verb is printed as written for all four, which is
|
|
303
|
+
* usually right and occasionally a typo — `outdatd` renders four plausible
|
|
304
|
+
* commands that none of them accept.
|
|
305
|
+
*
|
|
306
|
+
* Warnings rather than errors, because both are legitimate: a command only three
|
|
307
|
+
* managers have is a fine thing to document, and this table will always be behind
|
|
308
|
+
* some manager's newest subcommand.
|
|
309
|
+
*/
|
|
310
|
+
function commandWarnings(pages: PageRecord[], warnings: string[]): void {
|
|
311
|
+
for (const page of pages) {
|
|
312
|
+
for (const command of page.commands ?? []) {
|
|
313
|
+
const { unknownVerb, unavailable } = packageCommandIssues(command);
|
|
314
|
+
|
|
315
|
+
if (unknownVerb) {
|
|
316
|
+
warnings.push(
|
|
317
|
+
`"${page.file}": "${command}" starts with "${unknownVerb}", which duxt ` +
|
|
318
|
+
'does not translate — every manager shows it as written.'
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (unavailable.length) {
|
|
323
|
+
warnings.push(
|
|
324
|
+
`"${page.file}": "${command}" has no equivalent in ` +
|
|
325
|
+
`${unavailable.join(' and ')}, so that tab is not drawn.`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const stripTrailingSlash = (path: string) =>
|
|
333
|
+
path.length > 1 ? path.replace(/\/+$/, '') : path;
|
|
334
|
+
|
|
335
|
+
/** The repository and ref an artefact was looked for in. */
|
|
336
|
+
const sectionOrigin = (source: SourceRecord) =>
|
|
337
|
+
`${source.repository ?? source.repositoryUrl ?? 'the source'}${
|
|
338
|
+
source.ref ? `@${source.ref}` : ''
|
|
339
|
+
}`;
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* What reading one generated section's artefact had to say.
|
|
343
|
+
*
|
|
344
|
+
* WARNINGS, all of them, and the severity is not a compromise. Everything a
|
|
345
|
+
* local artefact can get wrong — a path that is not there, a file the type
|
|
346
|
+
* cannot read at all, an option it does not know — has already thrown while the
|
|
347
|
+
* config was loading, long before this report exists: the site's own
|
|
348
|
+
* configuration is a mistake the build must not carry. What is left to report
|
|
349
|
+
* here is therefore either a REMOTE artefact, which may legitimately have gone
|
|
350
|
+
* stale between releases and is not this build's to reject, or a page that
|
|
351
|
+
* rendered with something missing from it. Neither is an error.
|
|
352
|
+
*
|
|
353
|
+
* `report` absent is not "nothing to report" — it is "nobody read the artefact
|
|
354
|
+
* on this side". A remote one is read where Content put the checkout, which is
|
|
355
|
+
* a directory only the config loader knows, so the manifest a module holds
|
|
356
|
+
* carries no report for it at all. The page count answers for that case, and
|
|
357
|
+
* says less because less is known: a section serving nothing is still visible
|
|
358
|
+
* from the collection, only the reason for it is not.
|
|
359
|
+
*/
|
|
360
|
+
function sectionFindings(
|
|
361
|
+
source: SourceRecord,
|
|
362
|
+
pages: number | undefined
|
|
363
|
+
): string[] {
|
|
364
|
+
const meta = source.generated!;
|
|
365
|
+
const report = meta.report;
|
|
366
|
+
const where = `the generated section "${meta.label}"`;
|
|
367
|
+
const findings: string[] = [];
|
|
368
|
+
|
|
369
|
+
// Quoted only where the file is actually in this checkout. The Checks panel
|
|
370
|
+
// turns the first quoted `.md` in a finding into an editor link, and a remote
|
|
371
|
+
// `CHANGELOG.md` resolved against the local root is a link to nothing.
|
|
372
|
+
const artefact = meta.remote ? source.path : `"${source.path}"`;
|
|
373
|
+
|
|
374
|
+
if (report?.missing) {
|
|
375
|
+
findings.push(
|
|
376
|
+
`${where} declares ${artefact}, which ${sectionOrigin(source)} does not ` +
|
|
377
|
+
'have. The section is not built.'
|
|
378
|
+
);
|
|
379
|
+
} else if (report && !report.pages) {
|
|
380
|
+
findings.push(
|
|
381
|
+
`${where} holds nothing the "${meta.type}" type can read in ` +
|
|
382
|
+
`${artefact}, so it has no pages.`
|
|
383
|
+
);
|
|
384
|
+
} else if (!pages) {
|
|
385
|
+
findings.push(
|
|
386
|
+
report
|
|
387
|
+
? `${where} produced ${report.pages} pages out of ${artefact}, but the ` +
|
|
388
|
+
`collection "${source.collection}" serves none — Content dropped it.`
|
|
389
|
+
: `${where} produced no pages out of ${artefact} in ` +
|
|
390
|
+
`${sectionOrigin(source)}. Either the artefact is not there at that ` +
|
|
391
|
+
`ref, or it holds nothing the "${meta.type}" type can read.`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Prefixed with the section, never with a page: a generated page's name is
|
|
396
|
+
// this layer's own invention, so pointing at one would send a reader to a
|
|
397
|
+
// file that does not exist. The artefact is the thing to open.
|
|
398
|
+
for (const warning of report?.warnings ?? []) {
|
|
399
|
+
findings.push(`${where} (${artefact}): ${warning}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return findings;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* What each language carries, and what has stood still.
|
|
407
|
+
*
|
|
408
|
+
* Notes rather than warnings: an untranslated page is not a defect, it is a
|
|
409
|
+
* state — and one nothing else in this layer ever says out loud. The layer
|
|
410
|
+
* knows, per page, which languages carry it and when each file last changed;
|
|
411
|
+
* without a report that knowledge sits in the manifest and reaches nobody.
|
|
412
|
+
*
|
|
413
|
+
* OpenCode is the cautionary case, and the reason this is a build report rather
|
|
414
|
+
* than a dashboard: seventeen languages, an agent that kept them in sync, the
|
|
415
|
+
* workflow switched off, and nothing anywhere saying the translations had
|
|
416
|
+
* stopped moving. A line per language in every build is what makes that
|
|
417
|
+
* visible on the day it happens instead of a year later.
|
|
418
|
+
*
|
|
419
|
+
* Two findings, and the second is skipped rather than guessed when the data is
|
|
420
|
+
* missing: a page the original has and a translation does not, and a
|
|
421
|
+
* translation whose file has not moved since the original's did. `lastUpdated`
|
|
422
|
+
* comes from `git-meta`, which a source without `history: true` never gets.
|
|
423
|
+
*/
|
|
424
|
+
function translationNotes(
|
|
425
|
+
sources: SourceRecord[],
|
|
426
|
+
byCollection: Map<string, PageRecord[]>
|
|
427
|
+
): string[] {
|
|
428
|
+
const notes: string[] = [];
|
|
429
|
+
|
|
430
|
+
// Grouped by PREFIX, because that is what a translation shares with its
|
|
431
|
+
// original — the locale is never part of a content path.
|
|
432
|
+
const byPrefix = new Map<string, SourceRecord[]>();
|
|
433
|
+
for (const source of sources) {
|
|
434
|
+
const list = byPrefix.get(source.prefix) ?? [];
|
|
435
|
+
list.push(source);
|
|
436
|
+
byPrefix.set(source.prefix, list);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
for (const [prefix, group] of byPrefix) {
|
|
440
|
+
const original = group.find(
|
|
441
|
+
(source) => !source.locale || source.isDefaultLocale
|
|
442
|
+
);
|
|
443
|
+
const translations = group.filter((source) => source !== original);
|
|
444
|
+
|
|
445
|
+
if (!original || !translations.length) continue;
|
|
446
|
+
|
|
447
|
+
const base = byCollection.get(original.collection) ?? [];
|
|
448
|
+
const dates = new Map(
|
|
449
|
+
base.map((page) => [page.path, page.lastUpdated] as const)
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
for (const translation of translations) {
|
|
453
|
+
const pages = byCollection.get(translation.collection) ?? [];
|
|
454
|
+
const have = new Map(pages.map((page) => [page.path, page] as const));
|
|
455
|
+
|
|
456
|
+
const missing = base.filter((page) => !have.has(page.path));
|
|
457
|
+
|
|
458
|
+
const stale = pages.filter((page) => {
|
|
459
|
+
const source = dates.get(page.path);
|
|
460
|
+
return (
|
|
461
|
+
source &&
|
|
462
|
+
page.lastUpdated &&
|
|
463
|
+
Date.parse(page.lastUpdated) < Date.parse(source)
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const where = prefix ? ` under "${prefix}"` : '';
|
|
468
|
+
|
|
469
|
+
notes.push(
|
|
470
|
+
`${translation.locale}${where}: ${have.size}/${base.length} pages` +
|
|
471
|
+
(stale.length ? `, ${stale.length} behind the original` : '')
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
// Named, not just counted — a figure says a translation has drifted and
|
|
475
|
+
// a file name says where to start. Capped, because a language nobody has
|
|
476
|
+
// begun would otherwise print the whole tree.
|
|
477
|
+
for (const page of [...missing, ...stale].slice(0, NAMED)) {
|
|
478
|
+
notes.push(
|
|
479
|
+
missing.includes(page)
|
|
480
|
+
? ` "${page.file}" has no ${translation.locale} translation.`
|
|
481
|
+
: ` "${page.file}" has not moved since the original changed.`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const rest = missing.length + stale.length - NAMED;
|
|
486
|
+
if (rest > 0) notes.push(` … and ${rest} more.`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return notes;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** How many files a language names before the report starts counting instead. */
|
|
494
|
+
const NAMED = 10;
|