@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,318 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3
|
+
<title>duxt — Versions</title><style>
|
|
4
|
+
:root {
|
|
5
|
+
color-scheme: light dark;
|
|
6
|
+
--bg: #fff;
|
|
7
|
+
--fg: #12141a;
|
|
8
|
+
--muted: #6b7280;
|
|
9
|
+
--faint: #9ca3af;
|
|
10
|
+
--line: #e8e8ea;
|
|
11
|
+
--raised: #f6f6f7;
|
|
12
|
+
--accent: #00a86b;
|
|
13
|
+
--accent-soft: #e6f7f0;
|
|
14
|
+
--warn: #b45309;
|
|
15
|
+
--error: #b91c1c;
|
|
16
|
+
--ok: #15803d;
|
|
17
|
+
--radius: 7px;
|
|
18
|
+
}
|
|
19
|
+
@media (prefers-color-scheme: dark) {
|
|
20
|
+
:root:not([data-theme='light']) {
|
|
21
|
+
--bg: #0f1115;
|
|
22
|
+
--fg: #e8e8ea;
|
|
23
|
+
--muted: #9099a8;
|
|
24
|
+
--faint: #5b6472;
|
|
25
|
+
--line: #23262d;
|
|
26
|
+
--raised: #171a20;
|
|
27
|
+
--accent: #00dc82;
|
|
28
|
+
--accent-soft: #10291f;
|
|
29
|
+
--warn: #f59e0b;
|
|
30
|
+
--error: #f87171;
|
|
31
|
+
--ok: #4ade80;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
:root[data-theme='dark'] {
|
|
35
|
+
--bg: #0f1115;
|
|
36
|
+
--fg: #e8e8ea;
|
|
37
|
+
--muted: #9099a8;
|
|
38
|
+
--faint: #5b6472;
|
|
39
|
+
--line: #23262d;
|
|
40
|
+
--raised: #171a20;
|
|
41
|
+
--accent: #00dc82;
|
|
42
|
+
--accent-soft: #10291f;
|
|
43
|
+
--warn: #f59e0b;
|
|
44
|
+
--error: #f87171;
|
|
45
|
+
--ok: #4ade80;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
* { box-sizing: border-box; }
|
|
49
|
+
body {
|
|
50
|
+
margin: 0;
|
|
51
|
+
padding: 0 0 40px;
|
|
52
|
+
font: 13px/1.55 ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
53
|
+
color: var(--fg);
|
|
54
|
+
background: var(--bg);
|
|
55
|
+
-webkit-font-smoothing: antialiased;
|
|
56
|
+
}
|
|
57
|
+
a { color: inherit; }
|
|
58
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
|
|
59
|
+
|
|
60
|
+
header.bar {
|
|
61
|
+
position: sticky; top: 0; z-index: 2;
|
|
62
|
+
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
|
63
|
+
backdrop-filter: blur(8px);
|
|
64
|
+
border-bottom: 1px solid var(--line);
|
|
65
|
+
padding: 10px 16px 0;
|
|
66
|
+
}
|
|
67
|
+
nav { display: flex; gap: 1px; flex-wrap: wrap; }
|
|
68
|
+
nav a {
|
|
69
|
+
padding: 5px 10px; border-radius: var(--radius) var(--radius) 0 0;
|
|
70
|
+
text-decoration: none; color: var(--muted); font-weight: 500;
|
|
71
|
+
border-bottom: 2px solid transparent; line-height: 1.4;
|
|
72
|
+
}
|
|
73
|
+
nav a:hover { color: var(--fg); background: var(--raised); }
|
|
74
|
+
nav a[aria-current] { color: var(--fg); border-bottom-color: var(--accent); font-weight: 600; }
|
|
75
|
+
|
|
76
|
+
main { padding: 18px 16px 0; max-width: 1400px; }
|
|
77
|
+
h1 { margin: 0 0 3px; font-size: 15px; font-weight: 650; letter-spacing: -0.01em; }
|
|
78
|
+
h2 {
|
|
79
|
+
margin: 26px 0 8px; font-size: 12px; font-weight: 600;
|
|
80
|
+
text-transform: uppercase; letter-spacing: .05em; color: var(--muted);
|
|
81
|
+
}
|
|
82
|
+
p.hint { margin: 0 0 16px; color: var(--muted); max-width: 78ch; }
|
|
83
|
+
|
|
84
|
+
.stats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
|
|
85
|
+
.stat {
|
|
86
|
+
display: flex; flex-direction: column; gap: 1px;
|
|
87
|
+
padding: 7px 12px; min-width: 78px;
|
|
88
|
+
border: 1px solid var(--line); border-radius: var(--radius); background: var(--raised);
|
|
89
|
+
}
|
|
90
|
+
.stat b { font-size: 16px; font-weight: 650; font-variant-numeric: tabular-nums; line-height: 1.2; }
|
|
91
|
+
.stat span { font-size: 10.5px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
|
|
92
|
+
.stat.ok b { color: var(--ok); }
|
|
93
|
+
.stat.warn b { color: var(--warn); }
|
|
94
|
+
.stat.error b { color: var(--error); }
|
|
95
|
+
|
|
96
|
+
.scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: 10px; }
|
|
97
|
+
table { width: 100%; border-collapse: collapse; }
|
|
98
|
+
th, td { padding: 7px 11px; text-align: left; vertical-align: top; }
|
|
99
|
+
/* Offset zero, not the height of the tab bar. .scroll sets overflow-x, which
|
|
100
|
+
computes overflow-y to auto as well, so the box — not the page — is what a
|
|
101
|
+
sticky heading offsets against. At 41px the heading sat 41px down from the
|
|
102
|
+
box's top edge, leaving a blank band above it AND covering the first row:
|
|
103
|
+
the Redirects panel counted eight rules and showed seven. */
|
|
104
|
+
thead th {
|
|
105
|
+
position: sticky; top: 0; z-index: 1;
|
|
106
|
+
background: var(--raised);
|
|
107
|
+
font-weight: 600; color: var(--muted);
|
|
108
|
+
font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em;
|
|
109
|
+
white-space: nowrap; border-bottom: 1px solid var(--line);
|
|
110
|
+
}
|
|
111
|
+
tbody tr { border-top: 1px solid var(--line); }
|
|
112
|
+
tbody tr:first-child { border-top: 0; }
|
|
113
|
+
tbody tr:hover { background: var(--raised); }
|
|
114
|
+
td.empty { color: var(--muted); padding: 16px 11px; text-align: center; }
|
|
115
|
+
|
|
116
|
+
code {
|
|
117
|
+
font: 11.5px/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
|
|
118
|
+
background: var(--raised); border: 1px solid var(--line);
|
|
119
|
+
padding: 0 4px; border-radius: 4px; white-space: nowrap;
|
|
120
|
+
}
|
|
121
|
+
a.file { text-decoration: none; border-bottom: 1px dashed var(--faint); }
|
|
122
|
+
a.file:hover { border-bottom-style: solid; color: var(--accent); }
|
|
123
|
+
.dim { color: var(--faint); }
|
|
124
|
+
.trunc {
|
|
125
|
+
display: inline-block; max-width: 34ch; overflow: hidden;
|
|
126
|
+
text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;
|
|
127
|
+
}
|
|
128
|
+
td.wide { min-width: 32ch; }
|
|
129
|
+
|
|
130
|
+
.tag {
|
|
131
|
+
display: inline-block; border-radius: 999px; padding: 1px 7px;
|
|
132
|
+
font-size: 10px; font-weight: 600; white-space: nowrap;
|
|
133
|
+
background: var(--accent-soft); color: var(--accent);
|
|
134
|
+
border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
|
|
135
|
+
}
|
|
136
|
+
.tag.warn { background: transparent; color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
|
|
137
|
+
.tag.error { background: transparent; color: var(--error); border-color: color-mix(in srgb, var(--error) 40%, transparent); }
|
|
138
|
+
.tag.ok { background: transparent; color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
|
|
139
|
+
.tag.muted { background: var(--raised); color: var(--muted); border-color: var(--line); }
|
|
140
|
+
|
|
141
|
+
form { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; align-items: center; }
|
|
142
|
+
input[type=text], input[type=search] {
|
|
143
|
+
flex: 1; min-width: 240px; padding: 6px 11px;
|
|
144
|
+
border: 1px solid var(--line); border-radius: var(--radius);
|
|
145
|
+
font: inherit; background: var(--bg); color: inherit;
|
|
146
|
+
}
|
|
147
|
+
.filterbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
|
148
|
+
.filterbar .count {
|
|
149
|
+
color: var(--muted); font-size: 11.5px;
|
|
150
|
+
font-variant-numeric: tabular-nums; white-space: nowrap;
|
|
151
|
+
}
|
|
152
|
+
input.filter {
|
|
153
|
+
flex: 0 1 340px; min-width: 0; margin: 0; padding-left: 31px;
|
|
154
|
+
background: var(--bg) no-repeat 10px 50% / 14px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239099a8' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.6-3.6'/%3E%3C/svg%3E");
|
|
155
|
+
}
|
|
156
|
+
input.filter:focus { border-color: var(--accent); }
|
|
157
|
+
input.filter::-webkit-search-cancel-button { cursor: pointer; }
|
|
158
|
+
input::placeholder { color: var(--faint); }
|
|
159
|
+
button {
|
|
160
|
+
padding: 6px 14px; border: 1px solid var(--line); border-radius: var(--radius);
|
|
161
|
+
background: var(--raised); font: inherit; font-weight: 500; cursor: pointer; color: inherit;
|
|
162
|
+
}
|
|
163
|
+
button:hover { border-color: var(--accent); color: var(--accent); }
|
|
164
|
+
|
|
165
|
+
ol.steps { margin: 0 0 18px; padding: 0; list-style: none; counter-reset: step; }
|
|
166
|
+
ol.steps li {
|
|
167
|
+
position: relative; counter-increment: step;
|
|
168
|
+
padding: 8px 0 8px 30px; border-left: 2px solid var(--line); margin-left: 9px;
|
|
169
|
+
}
|
|
170
|
+
ol.steps li::before {
|
|
171
|
+
content: counter(step);
|
|
172
|
+
position: absolute; left: -10px; top: 8px;
|
|
173
|
+
width: 18px; height: 18px; border-radius: 999px;
|
|
174
|
+
background: var(--raised); border: 1px solid var(--line);
|
|
175
|
+
font-size: 10px; font-weight: 600; color: var(--muted);
|
|
176
|
+
display: flex; align-items: center; justify-content: center;
|
|
177
|
+
}
|
|
178
|
+
ol.steps li:last-child { border-left-color: transparent; }
|
|
179
|
+
|
|
180
|
+
.note {
|
|
181
|
+
padding: 10px 13px; border-radius: var(--radius);
|
|
182
|
+
background: var(--raised); border: 1px solid var(--line);
|
|
183
|
+
color: var(--muted); margin-bottom: 16px; max-width: 90ch;
|
|
184
|
+
}
|
|
185
|
+
.note code { background: var(--bg); }
|
|
186
|
+
|
|
187
|
+
details { margin-bottom: 10px; }
|
|
188
|
+
details > summary {
|
|
189
|
+
cursor: pointer; padding: 7px 11px; border-radius: var(--radius);
|
|
190
|
+
background: var(--raised); border: 1px solid var(--line);
|
|
191
|
+
font-weight: 600; list-style-position: inside;
|
|
192
|
+
}
|
|
193
|
+
details > summary:hover { border-color: var(--accent); }
|
|
194
|
+
details[open] > summary { border-radius: var(--radius) var(--radius) 0 0; margin-bottom: -1px; }
|
|
195
|
+
details[open] > summary + .scroll { border-radius: 0 0 var(--radius) var(--radius); }
|
|
196
|
+
details[open] > summary + .filterbar {
|
|
197
|
+
margin: 0; padding: 8px 11px;
|
|
198
|
+
background: var(--raised); border: 1px solid var(--line); border-top: 0;
|
|
199
|
+
}
|
|
200
|
+
details[open] > .filterbar + .scroll {
|
|
201
|
+
border-radius: 0 0 var(--radius) var(--radius); margin-top: -1px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* A disclosure inside a table cell is not the panel-sized one above: no bar,
|
|
205
|
+
no border, no weight — just a triangle in front of the preview. */
|
|
206
|
+
details.json { margin: 0; }
|
|
207
|
+
details.json > summary {
|
|
208
|
+
padding: 0; background: none; border: 0; border-radius: 0; font-weight: inherit;
|
|
209
|
+
list-style-position: outside; margin-left: 12px;
|
|
210
|
+
}
|
|
211
|
+
details.json > summary::marker { color: var(--faint); }
|
|
212
|
+
details.json > summary:hover { border: 0; }
|
|
213
|
+
details.json[open] > summary { margin-bottom: 6px; border-radius: 0; }
|
|
214
|
+
details.json[open] > summary .peek { display: none; }
|
|
215
|
+
pre.json {
|
|
216
|
+
margin: 0 0 2px; padding: 9px 11px; max-height: 320px; overflow: auto;
|
|
217
|
+
background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
|
|
218
|
+
font: 11.5px/1.6 ui-monospace, 'SF Mono', Menlo, monospace;
|
|
219
|
+
white-space: pre; tab-size: 2;
|
|
220
|
+
}
|
|
221
|
+
.j-key { color: var(--accent); }
|
|
222
|
+
.j-num { color: var(--warn); }
|
|
223
|
+
.j-word { color: var(--muted); font-style: italic; }
|
|
224
|
+
|
|
225
|
+
.matrix th:first-child, .matrix td:first-child {
|
|
226
|
+
position: sticky; left: 0; background: var(--bg);
|
|
227
|
+
border-right: 1px solid var(--line);
|
|
228
|
+
}
|
|
229
|
+
.matrix thead th:first-child { background: var(--raised); }
|
|
230
|
+
.matrix tbody tr:hover td:first-child { background: var(--raised); }
|
|
231
|
+
.matrix td.mark { text-align: center; }
|
|
232
|
+
.matrix td.yes { color: var(--ok); font-weight: 700; }
|
|
233
|
+
.matrix td.no { color: var(--faint); }
|
|
234
|
+
</style></head><body>
|
|
235
|
+
|
|
236
|
+
<main>
|
|
237
|
+
<h1>Versions</h1>
|
|
238
|
+
<p class="hint">Which versions carry which page. The view the fallback is debugged from.</p>
|
|
239
|
+
<h2>acme/sdk <span class="tag muted">en</span></h2><div class="scroll"><table class="matrix">
|
|
240
|
+
<thead><tr><th scope="col">Page</th><th scope="col">v2 *</th><th scope="col">v1.9</th></tr></thead>
|
|
241
|
+
<tbody><tr><td><code>/</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/changelog</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/getting-started</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/getting-started/install</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/guides/caching</code></td><td class="mark yes">✓</td><td class="mark no">·</td></tr><tr><td><code>/guides/deploying</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/client</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/errors</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr></tbody>
|
|
242
|
+
</table></div><p class="hint">* the version served without a version segment.</p><h2>acme/sdk <span class="tag muted">de</span></h2><div class="scroll"><table class="matrix">
|
|
243
|
+
<thead><tr><th scope="col">Page</th><th scope="col">v2 *</th><th scope="col">v1.9</th></tr></thead>
|
|
244
|
+
<tbody><tr><td><code>/</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/getting-started</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/getting-started/install</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/guides/deploying</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/client</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/errors</code></td><td class="mark yes">✓</td><td class="mark no">·</td></tr></tbody>
|
|
245
|
+
</table></div><p class="hint">* the version served without a version segment.</p><h2>acme/sdk <span class="tag muted">v2</span></h2><div class="scroll"><table class="matrix">
|
|
246
|
+
<thead><tr><th scope="col">Page</th><th scope="col">en *</th><th scope="col">de</th></tr></thead>
|
|
247
|
+
<tbody><tr><td><code>/</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/changelog</code></td><td class="mark yes">✓</td><td class="mark no">·</td></tr><tr><td><code>/getting-started</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/getting-started/install</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/guides/caching</code></td><td class="mark yes">✓</td><td class="mark no">·</td></tr><tr><td><code>/guides/deploying</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/client</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/errors</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr></tbody>
|
|
248
|
+
</table></div><p class="hint">* the language served from the source path itself, without a folder.</p><h2>acme/sdk <span class="tag muted">v1.9</span></h2><div class="scroll"><table class="matrix">
|
|
249
|
+
<thead><tr><th scope="col">Page</th><th scope="col">en *</th><th scope="col">de</th></tr></thead>
|
|
250
|
+
<tbody><tr><td><code>/</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/changelog</code></td><td class="mark yes">✓</td><td class="mark no">·</td></tr><tr><td><code>/getting-started</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/getting-started/install</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/guides/deploying</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/client</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/errors</code></td><td class="mark yes">✓</td><td class="mark no">·</td></tr></tbody>
|
|
251
|
+
</table></div><p class="hint">* the language served from the source path itself, without a folder.</p>
|
|
252
|
+
</main>
|
|
253
|
+
<script>
|
|
254
|
+
document.addEventListener('input', (event) => {
|
|
255
|
+
const input = event.target;
|
|
256
|
+
if (!input.matches('[data-filter]')) return;
|
|
257
|
+
|
|
258
|
+
const term = input.value.trim().toLowerCase();
|
|
259
|
+
const scope = input.closest('details') ?? document;
|
|
260
|
+
const rows = scope.querySelectorAll('tbody tr');
|
|
261
|
+
let shown = 0;
|
|
262
|
+
|
|
263
|
+
for (const row of rows) {
|
|
264
|
+
row.hidden = Boolean(term) && !row.textContent.toLowerCase().includes(term);
|
|
265
|
+
if (!row.hidden) shown += 1;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const count = input.parentElement.querySelector('[data-filter-count]');
|
|
269
|
+
if (count) {
|
|
270
|
+
count.textContent = term
|
|
271
|
+
? shown ? shown + ' of ' + rows.length : 'no matches'
|
|
272
|
+
: '';
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// EVERY link and button, with no exception for the tab row — a preview has
|
|
277
|
+
// no tab row. Embedded in a documentation page, a reader who clicked one
|
|
278
|
+
// would land on another panel inside the frame while the prose around it went
|
|
279
|
+
// on explaining this one.
|
|
280
|
+
document.addEventListener('click', (event) => {
|
|
281
|
+
if (event.target.closest('a, button')) event.preventDefault();
|
|
282
|
+
});
|
|
283
|
+
document.addEventListener('submit', (event) => event.preventDefault());
|
|
284
|
+
|
|
285
|
+
// The page that framed this one owns the theme and the height: it knows
|
|
286
|
+
// which mode the reader picked, and only this document knows how tall it
|
|
287
|
+
// turned out. Two messages, one each way.
|
|
288
|
+
const theme = new URLSearchParams(location.search).get('theme');
|
|
289
|
+
if (theme) document.documentElement.dataset.theme = theme;
|
|
290
|
+
|
|
291
|
+
// Only from the frame that embedded this one, and only from its own origin.
|
|
292
|
+
// Both halves are needed: the origin alone would still accept a message from
|
|
293
|
+
// a sibling frame on the same site, and `event.source` alone would accept one
|
|
294
|
+
// from a parent on another origin. This document is served from `public/`, so
|
|
295
|
+
// the page framing it is always same-origin — a message from anywhere else is
|
|
296
|
+
// not a theme change, it is somebody else talking.
|
|
297
|
+
addEventListener('message', (event) => {
|
|
298
|
+
if (event.origin !== location.origin || event.source !== parent) return;
|
|
299
|
+
|
|
300
|
+
if (event.data && typeof event.data.duxtTheme === 'string') {
|
|
301
|
+
document.documentElement.dataset.theme = event.data.duxtTheme;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Addressed rather than broadcast, for the same reason. `'*'` hands the
|
|
306
|
+
// height to whatever ends up framing this page, which on a consumer's site is
|
|
307
|
+
// not necessarily the page that asked for it.
|
|
308
|
+
const measure = () => parent.postMessage(
|
|
309
|
+
{ duxtHeight: document.documentElement.scrollHeight },
|
|
310
|
+
location.origin
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
addEventListener('load', measure);
|
|
314
|
+
addEventListener('resize', measure);
|
|
315
|
+
document.addEventListener('toggle', measure, true);
|
|
316
|
+
document.addEventListener('input', () => setTimeout(measure, 0));
|
|
317
|
+
</script>
|
|
318
|
+
</body></html>
|
package/report.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { contentCacheFile, readContentCacheAt } from './content-cache';
|
|
3
|
+
import { readDuxtBuildConfig } from './duxt-app-config';
|
|
4
|
+
import { duxtManifest, duxtSectionTypes } from './sections-resolve';
|
|
5
|
+
import { readSectionReports } from './section-reports';
|
|
6
|
+
import { resolveLatestRefs } from './sources-git';
|
|
7
|
+
import { redirectRules } from './modules/redirects';
|
|
8
|
+
import type { Collected, PageRecord } from './validate-report';
|
|
9
|
+
import { report, walk } from './validate-report';
|
|
10
|
+
import type { DuxtResolvedSource } from './sources-resolve';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The devtools tab's answers, as text a terminal — or a model — can read.
|
|
14
|
+
*
|
|
15
|
+
* The panels are HTML behind a route that exists only inside a dev server, and
|
|
16
|
+
* the validator's findings are lines in a build log that has scrolled away.
|
|
17
|
+
* Neither form survives being handed to somebody: "what is wrong with my
|
|
18
|
+
* documentation" had no answer that fits in a paste. This is that answer, and
|
|
19
|
+
* it is deliberately the SAME data rather than a second opinion — the manifest
|
|
20
|
+
* comes from `duxtManifest`, the findings from `report`, the redirects from
|
|
21
|
+
* `redirectRules`, exactly as the build and the panels take them.
|
|
22
|
+
*
|
|
23
|
+
* NO SERVER, and that is what makes it usable in CI and in a pipe. Everything
|
|
24
|
+
* here is a file read: the site's `app.config.ts` through jiti, the artefacts
|
|
25
|
+
* off disk, and the pages out of Content's parse cache — which is on disk after
|
|
26
|
+
* any build or dev run, remote sources included.
|
|
27
|
+
*
|
|
28
|
+
* What it therefore CANNOT show is the three panels that ask a running server:
|
|
29
|
+
* the search index, the path debugger and the cache directory's own listing.
|
|
30
|
+
* Those are questions about a process, and a command that answered them from
|
|
31
|
+
* the outside would be guessing.
|
|
32
|
+
*/
|
|
33
|
+
export interface DuxtReportOptions {
|
|
34
|
+
/** The site being reported on — where `app.config.ts` and `.data/` live. */
|
|
35
|
+
rootDir?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DuxtReport {
|
|
39
|
+
rootDir: string;
|
|
40
|
+
sources: DuxtResolvedSource[];
|
|
41
|
+
/** Undefined when nothing has parsed this site yet — see `pages`. */
|
|
42
|
+
pages: PageRecord[] | undefined;
|
|
43
|
+
findings: { errors: string[]; warnings: string[]; notes: string[] };
|
|
44
|
+
redirects: { from: string; to: string }[];
|
|
45
|
+
locales: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Everything the report says, before anything decides how to say it. */
|
|
49
|
+
export function duxtReport(options: DuxtReportOptions = {}): DuxtReport {
|
|
50
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
51
|
+
|
|
52
|
+
const config = readDuxtBuildConfig([rootDir, join(rootDir, 'app')]);
|
|
53
|
+
const types = duxtSectionTypes(config?.sectionTypes);
|
|
54
|
+
|
|
55
|
+
const sources = readSectionReports(
|
|
56
|
+
duxtManifest(
|
|
57
|
+
resolveLatestRefs(config?.sources ?? [{ path: 'docs' }]),
|
|
58
|
+
config?.sourceOptions ?? {},
|
|
59
|
+
types
|
|
60
|
+
),
|
|
61
|
+
types
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const locales = [
|
|
65
|
+
...new Set(
|
|
66
|
+
sources
|
|
67
|
+
.map((source) => source.locale)
|
|
68
|
+
.filter((locale): locale is string => Boolean(locale))
|
|
69
|
+
)
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const cached = readContentCacheAt(
|
|
73
|
+
contentCacheFile(rootDir),
|
|
74
|
+
sources.map((source) => source.collection)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const pages = cached?.map((entry) => {
|
|
78
|
+
const collected: Collected = {
|
|
79
|
+
anchors: new Set<string>(),
|
|
80
|
+
links: [],
|
|
81
|
+
commands: []
|
|
82
|
+
};
|
|
83
|
+
walk(entry.content.body, collected);
|
|
84
|
+
|
|
85
|
+
const text = (key: string) =>
|
|
86
|
+
typeof entry.content[key] === 'string'
|
|
87
|
+
? (entry.content[key] as string)
|
|
88
|
+
: undefined;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
collection: entry.collection,
|
|
92
|
+
path: entry.path,
|
|
93
|
+
file: entry.file,
|
|
94
|
+
title: text('title'),
|
|
95
|
+
description: text('description'),
|
|
96
|
+
anchors: collected.anchors,
|
|
97
|
+
links: collected.links,
|
|
98
|
+
commands: collected.commands,
|
|
99
|
+
lastUpdated: text('lastUpdated')
|
|
100
|
+
} satisfies PageRecord;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const byCollection = new Map(
|
|
104
|
+
sources.map((source) => [source.collection, source.prefix])
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const rules = redirectRules(
|
|
108
|
+
(cached ?? [])
|
|
109
|
+
.filter((entry) => Array.isArray(entry.content.redirectFrom))
|
|
110
|
+
.map((entry) => ({
|
|
111
|
+
path: entry.path,
|
|
112
|
+
prefix: byCollection.get(entry.collection) ?? '',
|
|
113
|
+
redirectFrom: entry.content.redirectFrom as string[]
|
|
114
|
+
})),
|
|
115
|
+
locales
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
rootDir,
|
|
120
|
+
sources,
|
|
121
|
+
pages,
|
|
122
|
+
// Over the pages there ARE. A site nobody has built yet is reported as
|
|
123
|
+
// exactly that further down, rather than as a site whose every collection
|
|
124
|
+
// is empty — which is what running the checks over nothing would say.
|
|
125
|
+
findings: pages
|
|
126
|
+
? report(sources, pages)
|
|
127
|
+
: { errors: [], warnings: [], notes: [] },
|
|
128
|
+
redirects: Object.entries(rules).map(([from, rule]) => ({
|
|
129
|
+
from,
|
|
130
|
+
to: rule.redirect.to
|
|
131
|
+
})),
|
|
132
|
+
locales
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* -------------------------------------------------------------------------- */
|
|
137
|
+
/* Markdown */
|
|
138
|
+
/* -------------------------------------------------------------------------- */
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The report as Markdown.
|
|
142
|
+
*
|
|
143
|
+
* Markdown rather than a drawn table, because the reader this exists for is as
|
|
144
|
+
* likely to be a model as a person, and a pipe-table is the one tabular form
|
|
145
|
+
* both read without ceremony. Nothing is truncated: a report that hides its
|
|
146
|
+
* long tail is a report you cannot act on, and the whole point is to paste it
|
|
147
|
+
* somewhere and ask.
|
|
148
|
+
*/
|
|
149
|
+
/**
|
|
150
|
+
* The ref a source was read at.
|
|
151
|
+
*
|
|
152
|
+
* The kind is only printed when there IS one: a source that names its origin
|
|
153
|
+
* without saying whether it is a branch or a tag reaches the manifest with a
|
|
154
|
+
* `ref` and no `refKind`, and the obvious template then prints
|
|
155
|
+
* `main (undefined)`.
|
|
156
|
+
*/
|
|
157
|
+
/**
|
|
158
|
+
* A figure and the thing it counts, in the number the figure asks for.
|
|
159
|
+
*
|
|
160
|
+
* Written inline four times it read `1 collections, 0 versions, 1 languages` —
|
|
161
|
+
* the kind of wrongness a reader stops trusting the rest of a report over.
|
|
162
|
+
*/
|
|
163
|
+
const count = (many: number, noun: string) =>
|
|
164
|
+
`${many} ${noun}${many === 1 ? '' : 's'}`;
|
|
165
|
+
|
|
166
|
+
const ref = (source: DuxtResolvedSource) =>
|
|
167
|
+
source.ref
|
|
168
|
+
? `${source.ref}${source.refKind ? ` (${source.refKind})` : ''}`
|
|
169
|
+
: 'checkout';
|
|
170
|
+
|
|
171
|
+
export function duxtReportMarkdown(data: DuxtReport): string {
|
|
172
|
+
const { sources, pages, findings } = data;
|
|
173
|
+
const out: string[] = [`# duxt report`, '', `Site: \`${data.rootDir}\``, ''];
|
|
174
|
+
|
|
175
|
+
const generated = sources.filter((source) => source.generated);
|
|
176
|
+
const versions = new Set(
|
|
177
|
+
sources
|
|
178
|
+
.filter((source) => source.version)
|
|
179
|
+
.map((source) => `${source.repo ?? ''}\u0000${source.version}`)
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// A site with no `locales` has ONE language — the one it is written in — and
|
|
183
|
+
// reaches here with an empty list rather than a list of one.
|
|
184
|
+
const languages = data.locales.length || 1;
|
|
185
|
+
|
|
186
|
+
out.push(
|
|
187
|
+
'## Summary',
|
|
188
|
+
'',
|
|
189
|
+
`- ${count(sources.length, 'collection')}, ` +
|
|
190
|
+
`${count(versions.size, 'version')}, ${count(languages, 'language')}`,
|
|
191
|
+
`- ${count(generated.length, 'generated section')}`,
|
|
192
|
+
pages
|
|
193
|
+
? `- ${pages.length} pages parsed`
|
|
194
|
+
: '- **no parse cache found** — run a build or a dev server first; ' +
|
|
195
|
+
'everything below that needs pages is missing, not empty',
|
|
196
|
+
`- ${findings.errors.length} errors, ${findings.warnings.length} warnings`,
|
|
197
|
+
`- ${data.redirects.length} redirects from \`redirectFrom\``,
|
|
198
|
+
''
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
out.push(
|
|
202
|
+
'## Sources',
|
|
203
|
+
'',
|
|
204
|
+
'| Prefix | Collection | Kind | Repository | Folder / artefact | Ref | Version | Status | Pages |',
|
|
205
|
+
'| :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | --: |'
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const counted = new Map<string, number>();
|
|
209
|
+
for (const page of pages ?? []) {
|
|
210
|
+
counted.set(page.collection, (counted.get(page.collection) ?? 0) + 1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const source of sources) {
|
|
214
|
+
out.push(
|
|
215
|
+
`| \`${source.prefix || '/'}\` | \`${source.collection}\` | ` +
|
|
216
|
+
`${source.generated ? source.generated.type : 'docs'}` +
|
|
217
|
+
`${source.locale ? ` · ${source.locale}` : ''} | ` +
|
|
218
|
+
`${source.repository ?? 'this repository'} | \`${source.path}\` | ` +
|
|
219
|
+
`${ref(source)} | ` +
|
|
220
|
+
`${source.version ?? '—'}${source.isDefault ? ' *default*' : ''} | ` +
|
|
221
|
+
`${source.status} | ${pages ? (counted.get(source.collection) ?? 0) : '?'} |`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
out.push('');
|
|
226
|
+
|
|
227
|
+
for (const [heading, list] of [
|
|
228
|
+
['Errors', findings.errors],
|
|
229
|
+
['Warnings', findings.warnings]
|
|
230
|
+
] as const) {
|
|
231
|
+
if (!list.length) continue;
|
|
232
|
+
|
|
233
|
+
out.push(`## ${heading}`, '');
|
|
234
|
+
for (const finding of list) out.push(`- ${finding}`);
|
|
235
|
+
out.push('');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!findings.errors.length && !findings.warnings.length && pages) {
|
|
239
|
+
out.push('## Findings', '', 'None.', '');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (findings.notes.length) {
|
|
243
|
+
out.push('## Translations', '');
|
|
244
|
+
for (const note of findings.notes) {
|
|
245
|
+
// A note indented by two spaces belongs under the line above it — the
|
|
246
|
+
// report is one summary per language, then the files behind the figure.
|
|
247
|
+
out.push(note.startsWith(' ') ? ` - ${note.trim()}` : `- ${note}`);
|
|
248
|
+
}
|
|
249
|
+
out.push('');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (data.redirects.length) {
|
|
253
|
+
out.push('## Redirects', '', '| From | To |', '| :-- | :-- |');
|
|
254
|
+
for (const rule of data.redirects) {
|
|
255
|
+
out.push(`| \`${rule.from}\` | \`${rule.to}\` |`);
|
|
256
|
+
}
|
|
257
|
+
out.push('');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return out.join('\n');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* -------------------------------------------------------------------------- */
|
|
264
|
+
/* The command */
|
|
265
|
+
/* -------------------------------------------------------------------------- */
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The command, as a function of its arguments and its data.
|
|
269
|
+
*
|
|
270
|
+
* HERE rather than in the bin, and that is not tidiness. The bin has to be
|
|
271
|
+
* plain JavaScript — Node refuses to strip types from a file under
|
|
272
|
+
* `node_modules`, so a `.ts` entry point works in this workspace, where the
|
|
273
|
+
* package is a symlink, and fails on the first real install with
|
|
274
|
+
* `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. Everything the command decides
|
|
275
|
+
* therefore lives on this side, where it is typed and tested, and the bin is
|
|
276
|
+
* left with nothing to get wrong.
|
|
277
|
+
*
|
|
278
|
+
* Returns the exit code rather than setting it: warnings do not fail, because
|
|
279
|
+
* they are what somebody else's repository going stale looks like, and a check
|
|
280
|
+
* that cannot survive that is a check that gets switched off.
|
|
281
|
+
*/
|
|
282
|
+
export function runDuxtReportCli(
|
|
283
|
+
argv: string[],
|
|
284
|
+
data: DuxtReport
|
|
285
|
+
): { output: string; exitCode: number } {
|
|
286
|
+
const output = argv.includes('--json')
|
|
287
|
+
? JSON.stringify(
|
|
288
|
+
{
|
|
289
|
+
...data,
|
|
290
|
+
// A Set does not survive `JSON.stringify` — it serialises as `{}`,
|
|
291
|
+
// which is a silently empty anchor list rather than an error.
|
|
292
|
+
pages: data.pages?.map((page) => ({
|
|
293
|
+
...page,
|
|
294
|
+
anchors: [...page.anchors]
|
|
295
|
+
}))
|
|
296
|
+
},
|
|
297
|
+
undefined,
|
|
298
|
+
2
|
|
299
|
+
)
|
|
300
|
+
: duxtReportMarkdown(data);
|
|
301
|
+
|
|
302
|
+
return { output, exitCode: data.findings.errors.length ? 1 : 0 };
|
|
303
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Walk up to the repository root, so `docs/` resolves there and not in a
|
|
6
|
+
* subfolder.
|
|
7
|
+
*
|
|
8
|
+
* A FILE OF ITS OWN, for eight lines, because of what importing it drags along.
|
|
9
|
+
* It lived in `sources.ts`, which imports `@nuxt/content` and `@nuxtjs/sitemap`
|
|
10
|
+
* — types that resolve inside the Content config and not inside a Nuxt module,
|
|
11
|
+
* so a module reaching for this function pulled that whole graph into a
|
|
12
|
+
* typecheck it does not belong to. Node-only either way, and therefore still
|
|
13
|
+
* not beside the resolver: that half is read by `app.config.ts` and bundled for
|
|
14
|
+
* the browser.
|
|
15
|
+
*/
|
|
16
|
+
export function repositoryRoot(): string {
|
|
17
|
+
let dir = process.cwd();
|
|
18
|
+
|
|
19
|
+
for (;;) {
|
|
20
|
+
if (existsSync(join(dir, '.git'))) return dir;
|
|
21
|
+
|
|
22
|
+
const parent = dirname(dir);
|
|
23
|
+
if (parent === dir) return process.cwd();
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
}
|