@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,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which section-row entries belong to the part of the site the reader is in.
|
|
3
|
+
*
|
|
4
|
+
* `duxt.sections` is one flat list, and on a site with one source that is the
|
|
5
|
+
* whole truth: every entry is a top-level part of the documentation. A site
|
|
6
|
+
* with a SECOND source has two sets of top-level parts, and rendering the list
|
|
7
|
+
* whole showed both everywhere — the demo API's own entries in the row above a
|
|
8
|
+
* getting-started page, and the documentation's six above the demo API.
|
|
9
|
+
*
|
|
10
|
+
* So an entry belongs to the source whose prefix it lies under, and the row
|
|
11
|
+
* shows the entries of the source the reader is in. A site that declares one
|
|
12
|
+
* source has one area, every entry is inside it, and nothing changes.
|
|
13
|
+
*
|
|
14
|
+
* Pure, and beside the resolver's other prefix arithmetic for the same reason
|
|
15
|
+
* that lives there: prefix work done inline in a component can only be checked
|
|
16
|
+
* by clicking.
|
|
17
|
+
*/
|
|
18
|
+
import { isInside, sourceForPath } from './version-paths';
|
|
19
|
+
|
|
20
|
+
/** The minimum a source has to carry to place a path or an entry. */
|
|
21
|
+
interface Area {
|
|
22
|
+
prefix: string;
|
|
23
|
+
generated?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The AREAS a site has — one per documentation tree, never per generated
|
|
28
|
+
* section.
|
|
29
|
+
*
|
|
30
|
+
* A reference at `/demo/api` is not a part of the site beside `/demo`; it is
|
|
31
|
+
* something published under it, and its own entry is what the row offers. Were
|
|
32
|
+
* its prefix an area of its own, an operation page would look like a third part
|
|
33
|
+
* of the site and the row would empty out on it.
|
|
34
|
+
*/
|
|
35
|
+
const areas = (sources: Area[]): string[] => [
|
|
36
|
+
...new Set(sources.filter((source) => !source.generated).map((s) => s.prefix))
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The area a path is in — the longest documentation prefix it sits under.
|
|
41
|
+
*
|
|
42
|
+
* `''` for a site whose sources are all unprefixed, which is every site that
|
|
43
|
+
* never asked for any of this.
|
|
44
|
+
*/
|
|
45
|
+
export function areaForPath(path: string, sources: Area[]): string {
|
|
46
|
+
return (
|
|
47
|
+
sourceForPath(
|
|
48
|
+
path,
|
|
49
|
+
areas(sources).map((prefix) => ({ prefix }))
|
|
50
|
+
)?.prefix ?? ''
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The entries of one area, in the order the site declared them.
|
|
56
|
+
*
|
|
57
|
+
* An entry with no `to` cannot be placed and is kept in every area: it is a
|
|
58
|
+
* link the consumer wrote for a reason the layer cannot read, and dropping it
|
|
59
|
+
* would make it disappear from a site that gained a second source.
|
|
60
|
+
*/
|
|
61
|
+
export function sectionsForPath<T extends { to?: string }>(
|
|
62
|
+
sections: T[],
|
|
63
|
+
sources: Area[],
|
|
64
|
+
path: string
|
|
65
|
+
): T[] {
|
|
66
|
+
const area = areaForPath(path, sources);
|
|
67
|
+
const all = areas(sources);
|
|
68
|
+
|
|
69
|
+
// Only when the site HAS two areas: with one, every entry is in it, and the
|
|
70
|
+
// filter is a walk over the list for an answer that cannot change.
|
|
71
|
+
if (all.length < 2) return sections;
|
|
72
|
+
|
|
73
|
+
return sections.filter(
|
|
74
|
+
(section) => !section.to || areaForPath(section.to, sources) === area
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* WHICH ONE of an area's entries is the one being read.
|
|
80
|
+
*
|
|
81
|
+
* A row of siblings answers this by itself — `/guides` and `/reference` cannot
|
|
82
|
+
* both prefix a path. An area whose parts nest can: `/demo/api` is inside
|
|
83
|
+
* `/demo`, so marking every entry that prefixes the path lit two chips at once
|
|
84
|
+
* and told the reader they were in both places.
|
|
85
|
+
*
|
|
86
|
+
* The longest match wins, which is the rule `sourceForPath` and `useDuxtSection`
|
|
87
|
+
* already follow — the deepest thing that claims the page is the thing the page
|
|
88
|
+
* is in.
|
|
89
|
+
*/
|
|
90
|
+
export function currentSection<T extends { to?: string }>(
|
|
91
|
+
sections: T[],
|
|
92
|
+
path: string
|
|
93
|
+
): T | undefined {
|
|
94
|
+
return sections
|
|
95
|
+
.filter((section) => section.to && isInside(path, section.to))
|
|
96
|
+
.sort((a, b) => (b.to?.length ?? 0) - (a.to?.length ?? 0))[0];
|
|
97
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mobile sheet's navigation, with the sections hung where they belong.
|
|
3
|
+
*
|
|
4
|
+
* The desktop header shows two rows: the navbar links up top and the sections
|
|
5
|
+
* below. The sheet has no rows, and listing the two as sibling blocks made the
|
|
6
|
+
* documentation look like a peer of its own parts — "Docs" in one list and
|
|
7
|
+
* "Guides", "ADR", "API" in another, with nothing saying the second is what the
|
|
8
|
+
* first opens.
|
|
9
|
+
*
|
|
10
|
+
* It already is, everywhere else. A navbar entry with no `to` means "the
|
|
11
|
+
* documentation" (see `DuxtHeader.linkTarget`), and `entryActive` lights it
|
|
12
|
+
* wherever ANY section is. This gives that entry the children it has always
|
|
13
|
+
* implied, so the sheet can render one tree instead of two lists.
|
|
14
|
+
*
|
|
15
|
+
* Pure and here rather than inline in the component, because the interesting
|
|
16
|
+
* part is what happens when there is nothing to hang them on — and a section
|
|
17
|
+
* that silently reaches no list at all is unreachable on a phone.
|
|
18
|
+
*/
|
|
19
|
+
import { areaForPath } from './section-scope';
|
|
20
|
+
|
|
21
|
+
/** The navbar with the sections adopted, plus whatever nobody adopted. */
|
|
22
|
+
export interface DuxtSheetNavigation {
|
|
23
|
+
entries: DuxtLink[];
|
|
24
|
+
/**
|
|
25
|
+
* Sections left over.
|
|
26
|
+
*
|
|
27
|
+
* Empty on the ordinary site, where an entry took them. A consumer who
|
|
28
|
+
* replaces `navigation` wholesale — `mergeDuxtConfig` REPLACES an array — may
|
|
29
|
+
* leave no entry that qualifies, and the sheet then renders these as their own
|
|
30
|
+
* block. Dropping them instead would take the whole documentation off the
|
|
31
|
+
* phone.
|
|
32
|
+
*/
|
|
33
|
+
sections: DuxtSection[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Which entry may adopt the sections: the first with neither a `to` nor
|
|
38
|
+
* children of its own.
|
|
39
|
+
*
|
|
40
|
+
* Both halves matter. An entry WITH a `to` names one page — a consumer wrote a
|
|
41
|
+
* destination and meant it, and appending eight children under it would
|
|
42
|
+
* repurpose their link. An entry that already has children carries a menu the
|
|
43
|
+
* consumer authored, and the sections are not more of it.
|
|
44
|
+
*/
|
|
45
|
+
function adopts(entry: DuxtLink) {
|
|
46
|
+
return !entry.to && !entry.children?.length;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* And which entry adopts ANOTHER AREA's sections: the one pointing at that
|
|
51
|
+
* area's own root.
|
|
52
|
+
*
|
|
53
|
+
* The desktop row scopes itself to the area the reader is in — see
|
|
54
|
+
* `sectionsForPath` — and the sheet has no rows to scope. So a site with a
|
|
55
|
+
* second source hangs that source's parts under the navbar entry that opens it,
|
|
56
|
+
* which is the same relationship the to-less entry has with the documentation,
|
|
57
|
+
* stated with a `to` because this one has somewhere to point.
|
|
58
|
+
*/
|
|
59
|
+
const adoptsArea = (entry: DuxtLink, area: string) =>
|
|
60
|
+
Boolean(area) && entry.to === area && !entry.children?.length;
|
|
61
|
+
|
|
62
|
+
export function buildSheetNavigation(
|
|
63
|
+
navigation: DuxtLink[] | undefined,
|
|
64
|
+
sections: DuxtSection[] | undefined,
|
|
65
|
+
sources: { prefix: string; generated?: unknown }[] = []
|
|
66
|
+
): DuxtSheetNavigation {
|
|
67
|
+
const entries = navigation ?? [];
|
|
68
|
+
const list = sections ?? [];
|
|
69
|
+
|
|
70
|
+
if (!list.length) return { entries, sections: [] };
|
|
71
|
+
|
|
72
|
+
// One bucket per area, in the order the site declared the sections.
|
|
73
|
+
const byArea = new Map<string, DuxtSection[]>();
|
|
74
|
+
for (const section of list) {
|
|
75
|
+
const area = section.to ? areaForPath(section.to, sources) : '';
|
|
76
|
+
byArea.set(area, [...(byArea.get(area) ?? []), section]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const adopted = entries.map((entry) => ({
|
|
80
|
+
entry,
|
|
81
|
+
taken: [] as DuxtSection[]
|
|
82
|
+
}));
|
|
83
|
+
const orphans: DuxtSection[] = [];
|
|
84
|
+
|
|
85
|
+
for (const [area, group] of byArea) {
|
|
86
|
+
const host = adopted.find(({ entry }) =>
|
|
87
|
+
area ? adoptsArea(entry, area) : adopts(entry)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (host) host.taken = group;
|
|
91
|
+
else orphans.push(...group);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
entries: adopted.map(({ entry, taken }) =>
|
|
96
|
+
taken.length ? { ...entry, children: taken } : entry
|
|
97
|
+
),
|
|
98
|
+
sections: orphans
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* From a page back to the file it was written in.
|
|
3
|
+
*
|
|
4
|
+
* "Edit this page" is the visible half; the same two functions answer "which
|
|
5
|
+
* file is this" for anything else that needs it. Pure, because every part of
|
|
6
|
+
* this is string arithmetic over a scheme with three moving pieces — the URL
|
|
7
|
+
* prefix, the numbered folder names and the source's own docs folder — and the
|
|
8
|
+
* only way that stays right is a test rather than a click.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The file's path inside its repository.
|
|
13
|
+
*
|
|
14
|
+
* `stem` is Content's own: the file path with the extension gone and the URL
|
|
15
|
+
* prefix in front, and — unlike `path` — with the numbered folder names still
|
|
16
|
+
* on it. `1.guides/2.add-a-body` renders at `/guides/add-a-body`, so the URL
|
|
17
|
+
* cannot be turned back into a file name; the stem can.
|
|
18
|
+
*/
|
|
19
|
+
export function sourceFilePath(
|
|
20
|
+
stem: string,
|
|
21
|
+
prefix: string,
|
|
22
|
+
folder: string
|
|
23
|
+
): string {
|
|
24
|
+
const lead = prefix.replace(/^\//, '');
|
|
25
|
+
const relative =
|
|
26
|
+
lead && stem.startsWith(`${lead}/`) ? stem.slice(lead.length + 1) : stem;
|
|
27
|
+
|
|
28
|
+
return [folder.replace(/\/+$/, ''), `${relative}.md`]
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join('/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The link back to a file: an edit form where one is possible, a view where it
|
|
35
|
+
* is not.
|
|
36
|
+
*
|
|
37
|
+
* A TAG CANNOT BE EDITED. GitHub's web editor commits to a branch, so
|
|
38
|
+
* `/edit/v0.8.0/…` is a 404 for anyone signed in — and a version served from a
|
|
39
|
+
* tag is exactly the case this layer creates. Sending that reader to the
|
|
40
|
+
* default branch instead would be worse than the 404: they would be editing
|
|
41
|
+
* today's documentation while believing they were fixing the version they were
|
|
42
|
+
* reading.
|
|
43
|
+
*
|
|
44
|
+
* So a tag gets the source, not a form. The caller draws the label the kind
|
|
45
|
+
* asks for.
|
|
46
|
+
*
|
|
47
|
+
* GitHub and GitLab spell both forms differently and every other host is left
|
|
48
|
+
* alone — a wrong link is worse than none, so no button is drawn at all.
|
|
49
|
+
*/
|
|
50
|
+
export interface SourceLink {
|
|
51
|
+
url: string;
|
|
52
|
+
/** `edit` opens a form; `view` opens the file as it stands. */
|
|
53
|
+
kind: 'edit' | 'view';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function sourceLink(
|
|
57
|
+
repositoryUrl: string | undefined,
|
|
58
|
+
ref: string | undefined,
|
|
59
|
+
refKind: 'branch' | 'tag' | undefined,
|
|
60
|
+
file: string
|
|
61
|
+
): SourceLink | undefined {
|
|
62
|
+
if (!repositoryUrl) return undefined;
|
|
63
|
+
|
|
64
|
+
const base = repositoryUrl.replace(/\.git$/, '').replace(/\/+$/, '');
|
|
65
|
+
|
|
66
|
+
// Without a ref there is nothing to open a form against either: `HEAD` is a
|
|
67
|
+
// symbolic name the editor does not accept, though it reads fine.
|
|
68
|
+
const editable = refKind !== 'tag' && Boolean(ref);
|
|
69
|
+
const at = ref || 'HEAD';
|
|
70
|
+
const kind = editable ? 'edit' : 'view';
|
|
71
|
+
|
|
72
|
+
if (base.includes('gitlab')) {
|
|
73
|
+
return {
|
|
74
|
+
url: `${base}/-/${editable ? 'edit' : 'blob'}/${at}/${file}`,
|
|
75
|
+
kind
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (base.includes('github')) {
|
|
80
|
+
return { url: `${base}/${editable ? 'edit' : 'blob'}/${at}/${file}`, kind };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which versions the switcher may offer.
|
|
3
|
+
*
|
|
4
|
+
* Lifted out of `DuxtVersion` because two components need the answer and only
|
|
5
|
+
* one of them draws the control: the mobile sheet renders a full-width select
|
|
6
|
+
* where there is something to choose and a plain badge beside the brand where
|
|
7
|
+
* there is not, and it has to know which before it decides where the version
|
|
8
|
+
* goes at all.
|
|
9
|
+
*
|
|
10
|
+
* Pure, and beside the resolver's other path arithmetic for the same reason the
|
|
11
|
+
* rest of it is: every version bug this layer has had came from prefix work
|
|
12
|
+
* done inline in a component, where it could only be checked by clicking.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Are these two collections versions of the SAME thing?
|
|
17
|
+
*
|
|
18
|
+
* A documentation tree and an API reference are both published per version, and
|
|
19
|
+
* neither is a version of the other — so a switcher that compared only the
|
|
20
|
+
* repository offered `v2 → /` and `v2 → /api` side by side, twice each label,
|
|
21
|
+
* and a canonical URL on a reference page pointed at the documentation's root.
|
|
22
|
+
*
|
|
23
|
+
* Compared by DECLARATION, which is the identity `resolveGeneratedSections`
|
|
24
|
+
* records for exactly this question. Two documentation trees answer `undefined`
|
|
25
|
+
* on both sides and are the same artefact, which is what keeps a site without a
|
|
26
|
+
* single generated section reading exactly as it did.
|
|
27
|
+
*/
|
|
28
|
+
export const sameArtefact = (
|
|
29
|
+
a: DuxtResolvedSource | undefined,
|
|
30
|
+
b: DuxtResolvedSource | undefined
|
|
31
|
+
) => a?.generated?.declaration === b?.generated?.declaration;
|
|
32
|
+
|
|
33
|
+
export function versionChoices(
|
|
34
|
+
sources: DuxtResolvedSource[],
|
|
35
|
+
current: DuxtResolvedSource | undefined,
|
|
36
|
+
configured: DuxtLink[] | undefined
|
|
37
|
+
): DuxtLink[] {
|
|
38
|
+
// A version-neutral generated section suppresses the control entirely, and
|
|
39
|
+
// that is the point of the policy rather than a tidy-up: a changelog is one
|
|
40
|
+
// global history, so every entry the switcher could offer would move the
|
|
41
|
+
// reader to a URL that section does not serve.
|
|
42
|
+
if (current?.generated?.versioning === 'global') return [];
|
|
43
|
+
|
|
44
|
+
// `versions` in the config still wins where a label needs to read differently
|
|
45
|
+
// from the URL segment.
|
|
46
|
+
if (configured?.length) return configured;
|
|
47
|
+
|
|
48
|
+
return sources
|
|
49
|
+
.filter(
|
|
50
|
+
(source) =>
|
|
51
|
+
source.version &&
|
|
52
|
+
source.repo === current?.repo &&
|
|
53
|
+
sameArtefact(source, current)
|
|
54
|
+
)
|
|
55
|
+
.map((source) => ({
|
|
56
|
+
label: source.version!,
|
|
57
|
+
to: source.prefix || '/',
|
|
58
|
+
// The lifecycle, not just "is this the default": a list where three
|
|
59
|
+
// entries look alike says nothing about which of them is still safe to
|
|
60
|
+
// read. `current` needs no word — it is what the reader assumes.
|
|
61
|
+
description:
|
|
62
|
+
source.status && source.status !== 'current'
|
|
63
|
+
? `duxt.version.status.${source.status}`
|
|
64
|
+
: source.isDefault
|
|
65
|
+
? 'default'
|
|
66
|
+
: undefined
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which source serves a path, and how to move a path between versions.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions, deliberately: every version bug this layer has had came from
|
|
5
|
+
* prefix arithmetic done inline in a component, where it could only be checked
|
|
6
|
+
* by clicking. `/workflows/v0.7.0` starts with `/workflows` as well, so taking
|
|
7
|
+
* the *first* matching prefix said "main" while reading the tag — and swapping
|
|
8
|
+
* versions then appended instead of replacing, turning /workflows/v0.7.0 into
|
|
9
|
+
* /workflows/v0.7.0/v0.7.0 on every click.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The source whose prefix the path is inside. Longest prefix wins. */
|
|
13
|
+
export function sourceForPath<T extends { prefix: string }>(
|
|
14
|
+
path: string,
|
|
15
|
+
sources: T[]
|
|
16
|
+
): T | undefined {
|
|
17
|
+
return (
|
|
18
|
+
[...sources]
|
|
19
|
+
.sort((a, b) => b.prefix.length - a.prefix.length)
|
|
20
|
+
.find((source) => !source.prefix || isInside(path, source.prefix)) ??
|
|
21
|
+
sources.find((source) => !source.prefix)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Is `path` inside `prefix`?
|
|
27
|
+
*
|
|
28
|
+
* Segment-aware: `/workflows-old` is not inside `/workflows`, though it starts
|
|
29
|
+
* with it.
|
|
30
|
+
*/
|
|
31
|
+
export function isInside(path: string, prefix: string): boolean {
|
|
32
|
+
if (!prefix) return true;
|
|
33
|
+
|
|
34
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The same page in another version.
|
|
39
|
+
*
|
|
40
|
+
* Strips the current version's prefix and applies the target's, so a page keeps
|
|
41
|
+
* its place instead of dropping the reader at the version's root.
|
|
42
|
+
*/
|
|
43
|
+
export function versionPath(
|
|
44
|
+
path: string,
|
|
45
|
+
from: string | undefined,
|
|
46
|
+
to: string
|
|
47
|
+
): string {
|
|
48
|
+
const rest =
|
|
49
|
+
from && from !== '/' && isInside(path, from)
|
|
50
|
+
? path.slice(from.length)
|
|
51
|
+
: path;
|
|
52
|
+
const target = to === '/' ? '' : to;
|
|
53
|
+
|
|
54
|
+
return `${target}${rest}` || '/';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Re-exported so the app can auto-import it.
|
|
59
|
+
*
|
|
60
|
+
* The function itself lives beside the resolver, because that is where the
|
|
61
|
+
* semver comparison it uses lives and where its tests are. `app/utils` is what
|
|
62
|
+
* Nuxt scans for auto-imports, and `sources-resolve.ts` is plain logic with no
|
|
63
|
+
* Node imports — the same property that lets `app.config.ts` read it.
|
|
64
|
+
*/
|
|
65
|
+
export { versionRelation } from '../../sources-resolve';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createJiti } from 'jiti';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The report, as a command.
|
|
6
|
+
*
|
|
7
|
+
* PLAIN JAVASCRIPT, and it has to be: Node refuses to strip types from a file
|
|
8
|
+
* under `node_modules`, so a TypeScript entry point runs in this repository —
|
|
9
|
+
* where the package is a symlink — and dies on the first real install with
|
|
10
|
+
* `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. That was measured against a
|
|
11
|
+
* packed tarball, not assumed.
|
|
12
|
+
*
|
|
13
|
+
* jiti is what loads the TypeScript behind it, the same way
|
|
14
|
+
* `duxt-app-config.ts` already reads a site's config, so the rest of the layer
|
|
15
|
+
* needs no build step to be reachable from here.
|
|
16
|
+
*
|
|
17
|
+
* Everything the command decides is in `report.ts`, where it is typed and
|
|
18
|
+
* tested. This file exists to have no logic in it.
|
|
19
|
+
*/
|
|
20
|
+
// Content's cache is read through `node:sqlite`, which announces itself as
|
|
21
|
+
// experimental on every run. True, and not this command's news to break to
|
|
22
|
+
// anybody: the report is a thing you pipe into a file or paste into a chat, and
|
|
23
|
+
// a warning about somebody else's implementation detail is noise in both.
|
|
24
|
+
process.removeAllListeners('warning');
|
|
25
|
+
|
|
26
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
27
|
+
|
|
28
|
+
const { duxtReport, runDuxtReportCli } = await jiti.import('../report');
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const { output, exitCode } = runDuxtReportCli(
|
|
32
|
+
process.argv.slice(2),
|
|
33
|
+
duxtReport()
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
console.log(output);
|
|
37
|
+
process.exitCode = exitCode;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// The message, not the stack. Everything thrown on this path is a statement
|
|
40
|
+
// about the SITE — an artefact that is not where the config says, a section
|
|
41
|
+
// option nobody knows — written to be read by whoever wrote that config, and
|
|
42
|
+
// a trace through jiti's loader helps none of them.
|
|
43
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
}
|
package/components.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://shadcn-vue.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"typescript": true,
|
|
5
|
+
"tailwind": {
|
|
6
|
+
"config": "",
|
|
7
|
+
"css": "app/assets/css/duxt.css",
|
|
8
|
+
"baseColor": "neutral",
|
|
9
|
+
"cssVariables": true
|
|
10
|
+
},
|
|
11
|
+
"aliases": {
|
|
12
|
+
"components": "@duxt/components",
|
|
13
|
+
"composables": "@duxt/composables",
|
|
14
|
+
"utils": "@duxt/lib/utils",
|
|
15
|
+
"ui": "@duxt/components/ui",
|
|
16
|
+
"lib": "@duxt/lib"
|
|
17
|
+
},
|
|
18
|
+
"iconLibrary": "lucide"
|
|
19
|
+
}
|
package/content-cache.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import type { Nuxt } from '@nuxt/schema';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Every parsed page, out of Content's own cache.
|
|
8
|
+
*
|
|
9
|
+
* The one place the build can see what Content actually produced. NOT the
|
|
10
|
+
* `content:file:afterParse` hook, which was the obvious choice and is the wrong
|
|
11
|
+
* one: Content skips the parse for every file whose checksum is unchanged, so
|
|
12
|
+
* on the second build that hook fires for nothing and a validator built on it
|
|
13
|
+
* reports an empty site. The cache holds the parsed document either way, remote
|
|
14
|
+
* sources included — Content downloads a repository into `.data/content/` and
|
|
15
|
+
* nothing else on disk describes what came back.
|
|
16
|
+
*
|
|
17
|
+
* Returns undefined rather than an empty list when there is no cache to read.
|
|
18
|
+
* A `nuxt prepare` run parses nothing, and "no pages" there is not a finding.
|
|
19
|
+
*/
|
|
20
|
+
export interface CachedPage {
|
|
21
|
+
collection: string;
|
|
22
|
+
path: string;
|
|
23
|
+
file: string;
|
|
24
|
+
content: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Where Content keeps its parse cache.
|
|
29
|
+
*
|
|
30
|
+
* Read from the runtime config where Content has already put it there, and
|
|
31
|
+
* from Content's own default where it has not — which is the case for anything
|
|
32
|
+
* running before Content's setup, `enableWriteAheadLog` included.
|
|
33
|
+
*/
|
|
34
|
+
function cacheFile(nuxt: Nuxt): string {
|
|
35
|
+
const configured = (
|
|
36
|
+
nuxt.options.runtimeConfig as {
|
|
37
|
+
content?: { localDatabase?: { filename?: string } };
|
|
38
|
+
}
|
|
39
|
+
).content?.localDatabase?.filename;
|
|
40
|
+
|
|
41
|
+
return configured && existsSync(configured)
|
|
42
|
+
? configured
|
|
43
|
+
: join(nuxt.options.rootDir, '.data/content/contents.sqlite');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function readContentCache(
|
|
47
|
+
nuxt: Nuxt,
|
|
48
|
+
collections: Iterable<string>
|
|
49
|
+
): CachedPage[] | undefined {
|
|
50
|
+
return readContentCacheAt(cacheFile(nuxt), collections);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Content's default location for that cache, for a reader with no Nuxt. */
|
|
54
|
+
export const contentCacheFile = (rootDir: string) =>
|
|
55
|
+
join(rootDir, '.data/content/contents.sqlite');
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The same read, addressed by FILE rather than by a Nuxt instance.
|
|
59
|
+
*
|
|
60
|
+
* Split out for `report.ts`, which runs as a command beside the build rather
|
|
61
|
+
* than inside it and has no `nuxt` to be handed. Everything below was already
|
|
62
|
+
* a function of the path alone; only the path was not reachable.
|
|
63
|
+
*/
|
|
64
|
+
export function readContentCacheAt(
|
|
65
|
+
file: string,
|
|
66
|
+
collections: Iterable<string>
|
|
67
|
+
): CachedPage[] | undefined {
|
|
68
|
+
if (!existsSync(file)) return undefined;
|
|
69
|
+
|
|
70
|
+
const names = new Set(collections);
|
|
71
|
+
const pages: CachedPage[] = [];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* WAIT for Content rather than failing next to it.
|
|
75
|
+
*
|
|
76
|
+
* SQLite's default journal mode locks the whole FILE while a writer holds it,
|
|
77
|
+
* so a reader gets SQLITE_BUSY immediately — `readOnly` does not help, it is
|
|
78
|
+
* the writer that excludes us. Content writes its parse cache from a
|
|
79
|
+
* `Promise.all` over the collections, and the build's own hooks read that
|
|
80
|
+
* cache; with a handful of files the write is over before anyone reads, and
|
|
81
|
+
* at six hundred it is not. The result was `database is locked` from
|
|
82
|
+
* whichever module read first, on a site whose only sin was having enough
|
|
83
|
+
* pages.
|
|
84
|
+
*
|
|
85
|
+
* A busy timeout turns the race into a wait: the reader retries internally
|
|
86
|
+
* until the writer commits. Fifteen seconds is far longer than a parse pass
|
|
87
|
+
* needs and still fails loudly if something genuinely holds the file — a
|
|
88
|
+
* stray dev server, say.
|
|
89
|
+
*/
|
|
90
|
+
const database = new DatabaseSync(file, {
|
|
91
|
+
readOnly: true,
|
|
92
|
+
timeout: 15_000
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const rows = database
|
|
97
|
+
.prepare('SELECT id, value FROM _development_cache')
|
|
98
|
+
.all() as { id: string; value: string }[];
|
|
99
|
+
|
|
100
|
+
for (const row of rows) {
|
|
101
|
+
const collection = row.id.split('/')[0] ?? '';
|
|
102
|
+
// The cache carries a version marker and, after a config change, rows of
|
|
103
|
+
// collections that no longer exist.
|
|
104
|
+
if (!names.has(collection)) continue;
|
|
105
|
+
|
|
106
|
+
let content: Record<string, unknown>;
|
|
107
|
+
try {
|
|
108
|
+
content = JSON.parse(row.value) as Record<string, unknown>;
|
|
109
|
+
} catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const path = typeof content.path === 'string' ? content.path : undefined;
|
|
114
|
+
if (!path) continue;
|
|
115
|
+
|
|
116
|
+
pages.push({
|
|
117
|
+
collection,
|
|
118
|
+
path,
|
|
119
|
+
file: typeof content.id === 'string' ? content.id : row.id,
|
|
120
|
+
content
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
} finally {
|
|
124
|
+
database.close();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return pages;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Put Content's cache database into WAL mode, before Content opens it.
|
|
132
|
+
*
|
|
133
|
+
* The failure this removes: a `nuxt build` the kernel or a Ctrl-C left behind
|
|
134
|
+
* keeps `contents.sqlite` open, and every following build then fails with
|
|
135
|
+
* `database is locked` — a message naming neither the process nor the file.
|
|
136
|
+
* The busy timeout above covers the layer's own reader; Content's cache WRITER
|
|
137
|
+
* has no such patience, and a build cannot ask it for any.
|
|
138
|
+
*
|
|
139
|
+
* SQLite's default rollback journal locks the whole FILE while a writer holds
|
|
140
|
+
* it, so any reader is excluded outright. Write-ahead logging lets readers and
|
|
141
|
+
* one writer coexist, which is the actual shape of a build: Content writing the
|
|
142
|
+
* parse cache while this layer's modules read it. Measured on this repo's own
|
|
143
|
+
* site while translating — 237 lock errors went to zero.
|
|
144
|
+
*
|
|
145
|
+
* The mode lives in the FILE HEADER, not in a connection, so setting it once
|
|
146
|
+
* holds for every process that opens the file afterwards — Content's included,
|
|
147
|
+
* which is why this can be done from a module and does not need a patch.
|
|
148
|
+
*
|
|
149
|
+
* Creates the file when it is not there yet, so a first build gets WAL too.
|
|
150
|
+
* Safe: Content opens the path either way and probes for its cache table with
|
|
151
|
+
* a `SELECT` in a try/catch, so an empty database is the same to it as no file.
|
|
152
|
+
*
|
|
153
|
+
* Best-effort throughout. A database that cannot be opened, a filesystem that
|
|
154
|
+
* does not support WAL (a network mount is the usual one) and a read-only
|
|
155
|
+
* checkout each leave the build exactly as it was before this ran.
|
|
156
|
+
*/
|
|
157
|
+
export function enableWriteAheadLog(nuxt: Nuxt): 'wal' | 'skipped' {
|
|
158
|
+
const file = cacheFile(nuxt);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* NEVER CREATE THE FILE. `new DatabaseSync` on a missing path writes an empty
|
|
162
|
+
* 4 kB database, and Content then finds a file where it expected none and
|
|
163
|
+
* never creates its schema — every query afterwards dies with `no such table:
|
|
164
|
+
* _development_cache`.
|
|
165
|
+
*
|
|
166
|
+
* Invisible on any machine that has built once, because the file is there by
|
|
167
|
+
* then; it only bites a FRESH checkout, which is to say CI and every new
|
|
168
|
+
* contributor. Skipping here costs the first build its WAL and nothing else:
|
|
169
|
+
* that build has no cache to read anyway, and the second one finds the file
|
|
170
|
+
* and switches it over.
|
|
171
|
+
*/
|
|
172
|
+
if (!existsSync(file)) return 'skipped';
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
176
|
+
|
|
177
|
+
const database = new DatabaseSync(file, { timeout: 15_000 });
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const [row] = database.prepare('PRAGMA journal_mode = WAL').all() as {
|
|
181
|
+
journal_mode?: string;
|
|
182
|
+
}[];
|
|
183
|
+
|
|
184
|
+
return row?.journal_mode?.toLowerCase() === 'wal' ? 'wal' : 'skipped';
|
|
185
|
+
} finally {
|
|
186
|
+
database.close();
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
// Nothing here is worth failing a build over: the busy timeout on the
|
|
190
|
+
// reader is what this improves upon, not what it replaces.
|
|
191
|
+
return 'skipped';
|
|
192
|
+
}
|
|
193
|
+
}
|