@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,55 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { DuxtResolvedSource } from './sources-resolve';
|
|
4
|
+
import type { DuxtSectionTypes } from './sections-resolve';
|
|
5
|
+
import {
|
|
6
|
+
duxtSectionTypes,
|
|
7
|
+
missingSectionArtefact,
|
|
8
|
+
sectionPages
|
|
9
|
+
} from './sections-resolve';
|
|
10
|
+
import { repositoryRoot } from './repository-root';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fill in what each LOCAL artefact had to say, for the manifest a module holds.
|
|
14
|
+
*
|
|
15
|
+
* The reports `sections.ts` produces while it declares the collections are
|
|
16
|
+
* written onto the manifest `content.config.ts` built, and that is not the
|
|
17
|
+
* manifest anything else reads:
|
|
18
|
+
* a config file and a Nuxt module are loaded through two different loaders, so
|
|
19
|
+
* the duxt modules build one of their own — see `duxtSectionTypes` for the same
|
|
20
|
+
* problem stated about the registry. Rather than smuggle the first manifest
|
|
21
|
+
* across, the artefacts are read again here: a file read and a parse, at build
|
|
22
|
+
* time, over a file the build has already opened once.
|
|
23
|
+
*
|
|
24
|
+
* LOCAL ONLY, and the limit is real rather than chosen. A remote artefact lives
|
|
25
|
+
* wherever Content's hash-cached checkout put it, which is a directory known
|
|
26
|
+
* only inside the collection that declared it — recomputing it here is exactly
|
|
27
|
+
* the second implementation `remoteCollection` refuses to write. A remote
|
|
28
|
+
* section therefore reaches the report with no `report` at all, and
|
|
29
|
+
* `sectionFindings` answers from the page count instead, saying less because
|
|
30
|
+
* less is known.
|
|
31
|
+
*/
|
|
32
|
+
export function readSectionReports(
|
|
33
|
+
entries: DuxtResolvedSource[],
|
|
34
|
+
types: DuxtSectionTypes = duxtSectionTypes()
|
|
35
|
+
): DuxtResolvedSource[] {
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const type = entry.generated && types[entry.generated.type];
|
|
38
|
+
|
|
39
|
+
if (!type || entry.generated!.remote) continue;
|
|
40
|
+
|
|
41
|
+
const file = join(repositoryRoot(), entry.path);
|
|
42
|
+
|
|
43
|
+
// Both calls record the report on the entry; the return value is the
|
|
44
|
+
// collection's business, not this one's. A missing local artefact throws
|
|
45
|
+
// here exactly as it throws while the collections are declared — the same
|
|
46
|
+
// error, from whichever of the two runs first.
|
|
47
|
+
if (existsSync(file)) {
|
|
48
|
+
sectionPages(entry, type, readFileSync(file, 'utf8'));
|
|
49
|
+
} else {
|
|
50
|
+
missingSectionArtefact(entry, file);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return entries;
|
|
55
|
+
}
|
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `changelog` type: a release log, published as pages.
|
|
3
|
+
*
|
|
4
|
+
* The first type in the registry, and the reason the registry is not
|
|
5
|
+
* generalisation without a user: a scaffold with nothing plugged into it is
|
|
6
|
+
* never touched by the build, which is where this repository's real failures
|
|
7
|
+
* have always shown up.
|
|
8
|
+
*
|
|
9
|
+
* What it reads is the file release-please writes — `## [0.2.0](compare) (date)`
|
|
10
|
+
* for a release, `### [0.2.1](compare) (date)` for a patch — and it is
|
|
11
|
+
* deliberately tolerant beyond that: a heading is a release when it begins with
|
|
12
|
+
* something that looks like a version, so a hand-kept `## 1.4.0` or a
|
|
13
|
+
* Keep-a-Changelog file reads too.
|
|
14
|
+
*
|
|
15
|
+
* TWO GRANULARITIES, and `split` is the default. Split turns each release into
|
|
16
|
+
* a page of its own — a deep link, a search hit, a feed item and an `llms.txt`
|
|
17
|
+
* entry per release — under a generated overview; `flat` renders the file as
|
|
18
|
+
* the one page it was written as, for a project that just wants it shown. The
|
|
19
|
+
* cost of two rendering paths was weighed and accepted, and the difference runs
|
|
20
|
+
* further than the page count: a flat changelog is an ORDINARY docs page and
|
|
21
|
+
* keeps the docs chrome, which is why `layout` is answered per declaration.
|
|
22
|
+
*
|
|
23
|
+
* Pure text in, files out. What the split history LOOKS like — the timeline,
|
|
24
|
+
* the group badges, the filters — belongs to `layouts/changelog.vue` and the
|
|
25
|
+
* two components this file writes calls to.
|
|
26
|
+
*/
|
|
27
|
+
import { stringify as stringifyYaml } from 'yaml';
|
|
28
|
+
import type {
|
|
29
|
+
DuxtSectionContext,
|
|
30
|
+
DuxtSectionOptions,
|
|
31
|
+
DuxtSectionPage,
|
|
32
|
+
DuxtSectionType
|
|
33
|
+
} from './sections-resolve';
|
|
34
|
+
import { slugify } from './sources-resolve';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The names this type binds — PUBLIC SURFACE, so renaming one is a `feat!:`.
|
|
38
|
+
*
|
|
39
|
+
* A layout and two MDC components, because that is the whole of what a
|
|
40
|
+
* consumer can override: dropping a `ChangelogGroup.vue` of their own into
|
|
41
|
+
* `app/components/content/` replaces the layer's, exactly as it does for a
|
|
42
|
+
* callout, and a `layouts/changelog.vue` replaces the chrome around it.
|
|
43
|
+
*/
|
|
44
|
+
export const DUXT_CHANGELOG_LAYOUT = 'changelog';
|
|
45
|
+
export const DUXT_CHANGELOG_RELEASES = 'changelog-releases';
|
|
46
|
+
export const DUXT_CHANGELOG_GROUP = 'changelog-group';
|
|
47
|
+
|
|
48
|
+
/** A heading, at any level. */
|
|
49
|
+
const HEADING = /^(#{1,6})[ \t]+(.+?)[ \t]*$/;
|
|
50
|
+
|
|
51
|
+
/** A fence opening or closing a code block, so a `#` inside one is text. */
|
|
52
|
+
const FENCE = /^\s*(```|~~~)/;
|
|
53
|
+
|
|
54
|
+
/** An inline link, reduced to the text it shows. */
|
|
55
|
+
const LINK = /\[([^\]]*)\]\([^)]*\)/g;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A link the release heading OPENS on — where release-please puts the compare
|
|
59
|
+
* URL of the release against the one before it.
|
|
60
|
+
*
|
|
61
|
+
* Anchored at the front, and only that link: a heading matches `RELEASE` once
|
|
62
|
+
* its links are reduced to their text, so a link standing first IS the version
|
|
63
|
+
* and nothing else can be. A link further along is prose about the release
|
|
64
|
+
* rather than a diff of it.
|
|
65
|
+
*
|
|
66
|
+
* `https?` ONLY. A relative href in a changelog resolves against the site that
|
|
67
|
+
* renders it rather than the repository the file came from, so it points at a
|
|
68
|
+
* page that does not exist — and a `javascript:` one is a script this layer
|
|
69
|
+
* would be putting on the page of a site that only asked for someone else's
|
|
70
|
+
* changelog.
|
|
71
|
+
*/
|
|
72
|
+
const RELEASE_LINK = /^\[[^\]]*\]\((https?:\/\/[^)\s]+)(?:[ \t]+"[^"]*")?\)/;
|
|
73
|
+
|
|
74
|
+
/** What is left of a release heading once its link is gone: version, date. */
|
|
75
|
+
const RELEASE = /^(v?\d[\w.+-]*)(?:[ \t]+\((\d{4}-\d{2}-\d{2})\))?$/;
|
|
76
|
+
|
|
77
|
+
/** A list item at the top level of a group — one entry of the release. */
|
|
78
|
+
const ENTRY = /^(?:[-*+]|\d+[.)])[ \t]+\S/;
|
|
79
|
+
|
|
80
|
+
/** How the file is turned into pages. */
|
|
81
|
+
const GRANULARITIES = ['split', 'flat'] as const;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The options this type reads, and the whole of what it answers to.
|
|
85
|
+
*
|
|
86
|
+
* Named rather than ignored, for the same reason an unknown VALUE is: a
|
|
87
|
+
* misspelled `granularty: 'flat'` is one character from the key that works, the
|
|
88
|
+
* site silently gets the default instead of what it configured, and nothing
|
|
89
|
+
* downstream ever mentions it again. `options` is opaque to the scaffold on
|
|
90
|
+
* purpose — `DuxtGeneratedSection.options` carries the bag rather than reading
|
|
91
|
+
* it — so the type that reads a key is the only place left that can say a key
|
|
92
|
+
* is not one it knows.
|
|
93
|
+
*/
|
|
94
|
+
const OPTIONS = ['granularity'] as const;
|
|
95
|
+
|
|
96
|
+
type Granularity = (typeof GRANULARITIES)[number];
|
|
97
|
+
|
|
98
|
+
interface Release {
|
|
99
|
+
/** The version exactly as the changelog wrote it. */
|
|
100
|
+
version: string;
|
|
101
|
+
/** The release date, when the heading carried one. */
|
|
102
|
+
date?: string;
|
|
103
|
+
/** The diff against the release before it, when the heading linked one. */
|
|
104
|
+
compare?: string;
|
|
105
|
+
/** The lines under the heading, up to the next release. */
|
|
106
|
+
body: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** One `###` block of a release, named by its own heading. */
|
|
110
|
+
interface Group {
|
|
111
|
+
/** The heading, VERBATIM — see `groupsOf`. */
|
|
112
|
+
name: string;
|
|
113
|
+
/** How many entries it lists, for the badge on the overview. */
|
|
114
|
+
count: number;
|
|
115
|
+
/** The lines under it. */
|
|
116
|
+
lines: string[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const changelogSectionType: DuxtSectionType = {
|
|
120
|
+
parse: parseChangelog,
|
|
121
|
+
/**
|
|
122
|
+
* ONE GLOBAL HISTORY, read from the default version.
|
|
123
|
+
*
|
|
124
|
+
* A changelog is not a per-version document that happens to mention other
|
|
125
|
+
* versions — it is the list OF the versions, and building one copy per
|
|
126
|
+
* version would publish the same file under three URLs, each of them missing
|
|
127
|
+
* the releases that came after it.
|
|
128
|
+
*/
|
|
129
|
+
versioning: 'global',
|
|
130
|
+
/**
|
|
131
|
+
* The original, in every language.
|
|
132
|
+
*
|
|
133
|
+
* A release log is written once, by the release tool, in whatever language
|
|
134
|
+
* the project commits in. A localised site therefore serves the original and
|
|
135
|
+
* says so with the translation banner it already draws for an untranslated
|
|
136
|
+
* page — which is the honest answer, and needs no new component.
|
|
137
|
+
*/
|
|
138
|
+
localisation: 'original',
|
|
139
|
+
/**
|
|
140
|
+
* A layout of its own for the split history, and none for the flat file.
|
|
141
|
+
*
|
|
142
|
+
* The split history is a timeline: the releases in the sidebar, a page per
|
|
143
|
+
* release, no table of contents over four bullet points. The flat file is a
|
|
144
|
+
* long ordinary page, and the docs chrome is exactly what it wants — a
|
|
145
|
+
* contents column listing the releases most of all. Same type, two products,
|
|
146
|
+
* so the question is answered from the declaration's own options.
|
|
147
|
+
*/
|
|
148
|
+
layout: (options) =>
|
|
149
|
+
granularityOf(options) === 'split' ? DUXT_CHANGELOG_LAYOUT : undefined,
|
|
150
|
+
icon: 'lucide:tag'
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The granularity this declaration asked for, and the declaration checked.
|
|
155
|
+
*
|
|
156
|
+
* Named rather than ignored, for the reason an unknown type is: a misspelled
|
|
157
|
+
* `granularity: 'splitt'` that silently means `split` is a site quietly not
|
|
158
|
+
* getting what it configured, and this build is the last place that can say so.
|
|
159
|
+
* A misspelled KEY is that same failure one character away, and is named in the
|
|
160
|
+
* same words — see `OPTIONS`.
|
|
161
|
+
*/
|
|
162
|
+
function granularityOf(options: DuxtSectionOptions): Granularity {
|
|
163
|
+
// Here rather than beside the caller because this is the ONE place the type
|
|
164
|
+
// reads its options at all: both the layout question and the parser come
|
|
165
|
+
// through it, so a section that is never built still names the typo.
|
|
166
|
+
const unknown = Object.keys(options).find(
|
|
167
|
+
(key) => !OPTIONS.includes(key as (typeof OPTIONS)[number])
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (unknown) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`duxt: a changelog section names the option "${unknown}", which is not ` +
|
|
173
|
+
`one of ${OPTIONS.map((name) => `"${name}"`).join(' or ')}.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const value = options.granularity ?? 'split';
|
|
178
|
+
|
|
179
|
+
if (!GRANULARITIES.includes(value as Granularity)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`duxt: a changelog section asks for the granularity ` +
|
|
182
|
+
`"${String(value)}", which is not one of ` +
|
|
183
|
+
`${GRANULARITIES.map((name) => `"${name}"`).join(' or ')}.`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return value as Granularity;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parseChangelog(
|
|
191
|
+
artefact: string,
|
|
192
|
+
context: DuxtSectionContext
|
|
193
|
+
): DuxtSectionPage[] {
|
|
194
|
+
const lines = artefact.split(/\r?\n/);
|
|
195
|
+
|
|
196
|
+
if (granularityOf(context.options) === 'flat') return [flat(lines, context)];
|
|
197
|
+
|
|
198
|
+
const headings = headingsOf(lines);
|
|
199
|
+
|
|
200
|
+
const starts = headings.filter((heading) => release(heading.text));
|
|
201
|
+
|
|
202
|
+
reportNearMisses(headings, starts, context);
|
|
203
|
+
const releases: Release[] = starts.map((heading, index) => {
|
|
204
|
+
const parsed = release(heading.text)!;
|
|
205
|
+
const end = starts[index + 1]?.line ?? lines.length;
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
version: parsed.version,
|
|
209
|
+
date: parsed.date,
|
|
210
|
+
compare: parsed.compare,
|
|
211
|
+
body: lines.slice(heading.line + 1, end)
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const preamble = lines.slice(0, starts[0]?.line ?? lines.length);
|
|
216
|
+
|
|
217
|
+
// Zero-padded to the number of releases, so ten of them still sort the way
|
|
218
|
+
// they read. Content strips the prefix from the URL, which is what makes an
|
|
219
|
+
// ordering scheme affordable at all — see `DuxtSectionPage.file`.
|
|
220
|
+
const width = String(releases.length).length;
|
|
221
|
+
|
|
222
|
+
return [
|
|
223
|
+
index(preamble, releases, context),
|
|
224
|
+
...releases.map((entry, position) =>
|
|
225
|
+
page(entry, String(position + 1).padStart(width, '0'))
|
|
226
|
+
)
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The file as it stands, as one page.
|
|
232
|
+
*
|
|
233
|
+
* Nothing is rewritten but the title: the file's own `# Changelog` goes,
|
|
234
|
+
* because the docs page draws its heading from `title` and a second `<h1>` in
|
|
235
|
+
* the body is both a duplicate and an axe finding. Every release heading stays
|
|
236
|
+
* exactly where the release tool put it — which is what "unchanged" has to
|
|
237
|
+
* mean, or the mode is not the escape hatch it exists to be.
|
|
238
|
+
*/
|
|
239
|
+
function flat(lines: string[], context: DuxtSectionContext): DuxtSectionPage {
|
|
240
|
+
return {
|
|
241
|
+
file: 'index.md',
|
|
242
|
+
body: [
|
|
243
|
+
frontmatter({ title: context.label }),
|
|
244
|
+
'',
|
|
245
|
+
...trim(withoutTitle(lines)),
|
|
246
|
+
''
|
|
247
|
+
].join('\n')
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Every heading outside a fenced code block, with the line it sits on. */
|
|
252
|
+
function headingsOf(
|
|
253
|
+
lines: string[]
|
|
254
|
+
): { line: number; level: number; text: string }[] {
|
|
255
|
+
const headings: { line: number; level: number; text: string }[] = [];
|
|
256
|
+
|
|
257
|
+
scan(lines, (line, number, fenced) => {
|
|
258
|
+
if (fenced) return;
|
|
259
|
+
|
|
260
|
+
const match = HEADING.exec(line);
|
|
261
|
+
if (match) {
|
|
262
|
+
headings.push({
|
|
263
|
+
line: number,
|
|
264
|
+
level: match[1]!.length,
|
|
265
|
+
text: match[2]!
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return headings;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Every line, with whether it sits inside a fenced block.
|
|
275
|
+
*
|
|
276
|
+
* One walk, three readers: the heading list, the entry count and the promotion
|
|
277
|
+
* pass all have to agree about what is code and what is content, and three
|
|
278
|
+
* copies of the fence rule is three places for them to stop agreeing. A fence
|
|
279
|
+
* closes only on its own kind, so a ``` inside a ~~~ block is content rather
|
|
280
|
+
* than the end of it.
|
|
281
|
+
*/
|
|
282
|
+
function scan(
|
|
283
|
+
lines: string[],
|
|
284
|
+
visit: (line: string, number: number, fenced: boolean) => void
|
|
285
|
+
): void {
|
|
286
|
+
let fence: string | undefined;
|
|
287
|
+
|
|
288
|
+
lines.forEach((line, number) => {
|
|
289
|
+
const opened = FENCE.exec(line);
|
|
290
|
+
|
|
291
|
+
if (opened) {
|
|
292
|
+
if (!fence) fence = opened[1];
|
|
293
|
+
else if (fence === opened[1]) fence = undefined;
|
|
294
|
+
|
|
295
|
+
visit(line, number, true);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
visit(line, number, Boolean(fence));
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Is this heading a release, and which one? */
|
|
304
|
+
/**
|
|
305
|
+
* A heading that reads like a release but was not taken for one.
|
|
306
|
+
*
|
|
307
|
+
* The parser is deliberately tolerant, and tolerance has a silent edge: a
|
|
308
|
+
* heading `RELEASE` does not match is not an error, it is PROSE — its lines are
|
|
309
|
+
* folded into the release above it, or into the preamble, and the release it
|
|
310
|
+
* was meant to be never becomes a page. Nothing said so. This says so.
|
|
311
|
+
*
|
|
312
|
+
* The test is a dotted number at the front, so `## 1.4 (Feb 2024)` and the
|
|
313
|
+
* Keep a Changelog form `## [1.2.3] - 2024-01-01` — which carries no link and
|
|
314
|
+
* therefore survives `LINK` with its brackets on — are reported, while a group
|
|
315
|
+
* heading like `### 3 breaking changes` is not. It is a warning rather than a
|
|
316
|
+
* throw for the reason every other content finding is one: the site renders,
|
|
317
|
+
* and a changelog this layer did not write is not the build's to reject.
|
|
318
|
+
*/
|
|
319
|
+
const NEAR_RELEASE = /^\[?v?\d+\.\d/;
|
|
320
|
+
|
|
321
|
+
function reportNearMisses(
|
|
322
|
+
headings: { line: number; text: string }[],
|
|
323
|
+
starts: { line: number }[],
|
|
324
|
+
context: DuxtSectionContext
|
|
325
|
+
): void {
|
|
326
|
+
if (!context.warn) return;
|
|
327
|
+
|
|
328
|
+
const taken = new Set(starts.map((heading) => heading.line));
|
|
329
|
+
|
|
330
|
+
for (const heading of headings) {
|
|
331
|
+
if (taken.has(heading.line)) continue;
|
|
332
|
+
|
|
333
|
+
const text = heading.text.replace(LINK, '$1').trim();
|
|
334
|
+
if (!NEAR_RELEASE.test(text)) continue;
|
|
335
|
+
|
|
336
|
+
context.warn(
|
|
337
|
+
`the heading "${heading.text}" on line ${heading.line + 1} reads like a ` +
|
|
338
|
+
'release but does not parse as one, so it became part of the release ' +
|
|
339
|
+
'above it instead of a page of its own. A release heading is a version, ' +
|
|
340
|
+
'optionally followed by a date in parentheses: `## 1.4.0 (2024-02-01)`.'
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function release(
|
|
346
|
+
text: string
|
|
347
|
+
): { version: string; date?: string; compare?: string } | undefined {
|
|
348
|
+
const trimmed = text.trim();
|
|
349
|
+
|
|
350
|
+
const match = RELEASE.exec(trimmed.replace(LINK, '$1').trim());
|
|
351
|
+
if (!match) return undefined;
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
version: match[1]!,
|
|
355
|
+
date: match[2],
|
|
356
|
+
compare: RELEASE_LINK.exec(trimmed)?.[1]
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* A release body, split into the groups its own headings name.
|
|
362
|
+
*
|
|
363
|
+
* THE GROUPS ARE THE FILE'S HEADINGS, TAKEN VERBATIM. No fixed taxonomy and no
|
|
364
|
+
* mapping: release-please writes "Features" and "Bug Fixes", changesets and
|
|
365
|
+
* Keep a Changelog write their own, and every one of them writes them in the
|
|
366
|
+
* language the project is kept in. A hard-wired list would fail silently on the
|
|
367
|
+
* first heading it did not know — which is the failure mode this repository
|
|
368
|
+
* least wants, and the reason the filters on the overview are built from
|
|
369
|
+
* whatever came out of the file.
|
|
370
|
+
*
|
|
371
|
+
* The LEVEL comes off the body rather than being assumed: release-please writes
|
|
372
|
+
* a patch release at `###` and its groups at `###` as well, so "the shallowest
|
|
373
|
+
* heading in this release" is the only rule that reads both.
|
|
374
|
+
*/
|
|
375
|
+
function groupsOf(lines: string[]): { intro: string[]; groups: Group[] } {
|
|
376
|
+
const headings = headingsOf(lines);
|
|
377
|
+
if (!headings.length) return { intro: lines, groups: [] };
|
|
378
|
+
|
|
379
|
+
const level = Math.min(...headings.map((heading) => heading.level));
|
|
380
|
+
const starts = headings.filter((heading) => heading.level === level);
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
intro: trim(lines.slice(0, starts[0]!.line)),
|
|
384
|
+
groups: starts.map((heading, index) => {
|
|
385
|
+
const end = starts[index + 1]?.line ?? lines.length;
|
|
386
|
+
const body = trim(lines.slice(heading.line + 1, end));
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
name: heading.text.trim(),
|
|
390
|
+
count: entries(body),
|
|
391
|
+
lines: body
|
|
392
|
+
};
|
|
393
|
+
})
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** How many entries a group lists — its top-level list items. */
|
|
398
|
+
function entries(lines: string[]): number {
|
|
399
|
+
let count = 0;
|
|
400
|
+
|
|
401
|
+
scan(lines, (line, _number, fenced) => {
|
|
402
|
+
if (!fenced && ENTRY.test(line)) count += 1;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return count;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The section's own page: the preamble, then the releases as a timeline.
|
|
410
|
+
*
|
|
411
|
+
* The release LIST travels as props rather than as a Markdown list, the way
|
|
412
|
+
* `sections-openapi.ts` hands its tags over: what the component needs is the
|
|
413
|
+
* date and the groups beside each version, and a bullet list carrying that
|
|
414
|
+
* would be a data structure spelled as prose. The entries themselves stay on
|
|
415
|
+
* the release pages — repeating them here would put the whole changelog twice
|
|
416
|
+
* into the search index, `llms-full.txt` and the feed.
|
|
417
|
+
*/
|
|
418
|
+
function index(
|
|
419
|
+
preamble: string[],
|
|
420
|
+
releases: Release[],
|
|
421
|
+
context: DuxtSectionContext
|
|
422
|
+
): DuxtSectionPage {
|
|
423
|
+
const body = trim(withoutTitle(preamble));
|
|
424
|
+
|
|
425
|
+
const props = {
|
|
426
|
+
releases: releases.map((entry) => ({
|
|
427
|
+
version: entry.version,
|
|
428
|
+
date: entry.date,
|
|
429
|
+
to: `${context.prefix}/${segment(entry.version)}`,
|
|
430
|
+
groups: groupsOf(trim(entry.body)).groups.map((group) => ({
|
|
431
|
+
name: group.name,
|
|
432
|
+
count: group.count
|
|
433
|
+
}))
|
|
434
|
+
}))
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
file: 'index.md',
|
|
439
|
+
body: [
|
|
440
|
+
frontmatter({ title: context.label }),
|
|
441
|
+
'',
|
|
442
|
+
// NO `<h1>` OF ITS OWN. The page draws the docs header — breadcrumb,
|
|
443
|
+
// title, description, the copy control beside it — for exactly the
|
|
444
|
+
// generated pages whose body opens on no heading, and a release history
|
|
445
|
+
// is one: its pages have a title and a trail like any other page, and the
|
|
446
|
+
// only reason they ever drew their own was that the layout drew none.
|
|
447
|
+
...(body.length ? [...body, ''] : []),
|
|
448
|
+
...(releases.length
|
|
449
|
+
? [component(DUXT_CHANGELOG_RELEASES, props), '']
|
|
450
|
+
: [])
|
|
451
|
+
].join('\n')
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* One release, as a page: its groups, and in the frontmatter what the release
|
|
457
|
+
* IS.
|
|
458
|
+
*
|
|
459
|
+
* `date` and `compare` are FRONTMATTER rather than a row the body opens on, and
|
|
460
|
+
* that is the difference between a release page and a dashboard: a release note
|
|
461
|
+
* is Markdown and should read as the rest of the site's Markdown does. Both
|
|
462
|
+
* facts are provenance — when this was cut, and what went into it — so they
|
|
463
|
+
* belong where the site already answers that, in the column beside the contents
|
|
464
|
+
* where "Edit this page" and the last commit sit. `DuxtPageInfo` draws them.
|
|
465
|
+
*/
|
|
466
|
+
function page(entry: Release, order: string): DuxtSectionPage {
|
|
467
|
+
const { intro, groups } = groupsOf(trim(entry.body));
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
file: `${order}.${segment(entry.version)}.md`,
|
|
471
|
+
body: [
|
|
472
|
+
frontmatter({
|
|
473
|
+
title: entry.version,
|
|
474
|
+
date: entry.date,
|
|
475
|
+
compare: entry.compare
|
|
476
|
+
}),
|
|
477
|
+
'',
|
|
478
|
+
// The version is the page's title, and the page draws it — see `index`.
|
|
479
|
+
...(intro.length ? [...promote(intro), ''] : []),
|
|
480
|
+
...groups.flatMap((group) => [
|
|
481
|
+
// The entries stay MARKDOWN, in the component's slot: they are the
|
|
482
|
+
// release, and what a reader searches for, what `llms-full.txt` carries
|
|
483
|
+
// and what the copy button hands a model is prose either way. Only the
|
|
484
|
+
// name and the count — which the badge and the filters need as data —
|
|
485
|
+
// travel as props.
|
|
486
|
+
component(
|
|
487
|
+
DUXT_CHANGELOG_GROUP,
|
|
488
|
+
{ name: group.name, count: group.count },
|
|
489
|
+
promote(group.lines).join('\n')
|
|
490
|
+
),
|
|
491
|
+
''
|
|
492
|
+
])
|
|
493
|
+
].join('\n')
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* The URL segment one release is served at.
|
|
499
|
+
*
|
|
500
|
+
* A `v` is added to a bare number, and that is not decoration: Content reads a
|
|
501
|
+
* file name made only of digits and dots as a version and stops refining it —
|
|
502
|
+
* which would leave the `NN.` ordering prefix in the URL, so `01.0.2.0.md`
|
|
503
|
+
* would serve `/releases/01.0.2.0`. With the `v` the prefix is stripped as it
|
|
504
|
+
* is everywhere else in this repository, and the segment matches the tag the
|
|
505
|
+
* release was cut as (`include-v-in-tag`).
|
|
506
|
+
*/
|
|
507
|
+
function segment(version: string): string {
|
|
508
|
+
return slugify(/^\d/.test(version) ? `v${version}` : version);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Every heading below the second level, one level up.
|
|
513
|
+
*
|
|
514
|
+
* A release body starts at `###` — release-please puts "Features" and "Bug
|
|
515
|
+
* Fixes" there, under the `##` the release itself occupies. On a page of its
|
|
516
|
+
* own that release is the `<h1>`, and the groups it was split into are the
|
|
517
|
+
* `<h2>`s their component draws — so what was written under a group has to come
|
|
518
|
+
* up one level too, or it skips one: a broken outline for a screen reader, and
|
|
519
|
+
* an axe `heading-order` failure on the built page.
|
|
520
|
+
*/
|
|
521
|
+
function promote(lines: string[]): string[] {
|
|
522
|
+
const promoted: string[] = [];
|
|
523
|
+
|
|
524
|
+
scan(lines, (line, _number, fenced) => {
|
|
525
|
+
const match = fenced ? undefined : HEADING.exec(line);
|
|
526
|
+
|
|
527
|
+
promoted.push(
|
|
528
|
+
match && match[1]!.length >= 3
|
|
529
|
+
? `${'#'.repeat(match[1]!.length - 1)} ${match[2]}`
|
|
530
|
+
: line
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
return promoted;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* The lines without the file's own title.
|
|
539
|
+
*
|
|
540
|
+
* A page draws its `<h1>` from `title`, so the `# Changelog` at the top of the
|
|
541
|
+
* file is a second one — in the docs shell and in a layout of its own alike.
|
|
542
|
+
*
|
|
543
|
+
* THE FIRST ONE ONLY, and only ahead of the first release. Dropping every `#`
|
|
544
|
+
* reads correctly on the file release-please writes, where there is exactly
|
|
545
|
+
* one — and is silent content loss on a hand-kept changelog that puts its
|
|
546
|
+
* releases at the top level: every release heading would go, and `flat`, the
|
|
547
|
+
* mode that exists to render the file untouched, would print one unbroken run
|
|
548
|
+
* of bullets with no release boundaries in it. `split` reads that same file
|
|
549
|
+
* release by release, which is what makes the general rule an asymmetry rather
|
|
550
|
+
* than a policy.
|
|
551
|
+
*/
|
|
552
|
+
function withoutTitle(lines: string[]): string[] {
|
|
553
|
+
const title = titleOf(lines);
|
|
554
|
+
if (title === undefined) return lines;
|
|
555
|
+
|
|
556
|
+
return [...lines.slice(0, title), ...lines.slice(title + 1)];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* The line the file's own title sits on, where it has one.
|
|
561
|
+
*
|
|
562
|
+
* A heading is the title when it is the first `#` in the file and no release
|
|
563
|
+
* came before it — a file that opens on a release has no title to take, and
|
|
564
|
+
* `index()` reads the same rule scoped to the preamble it is handed, where
|
|
565
|
+
* "before the first release" is the whole of what it holds. A heading inside a
|
|
566
|
+
* fenced block is text, which `headingsOf` already settles.
|
|
567
|
+
*/
|
|
568
|
+
function titleOf(lines: string[]): number | undefined {
|
|
569
|
+
for (const heading of headingsOf(lines)) {
|
|
570
|
+
if (release(heading.text)) return undefined;
|
|
571
|
+
if (heading.level === 1) return heading.line;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** The lines with the blank ones at either end dropped. */
|
|
578
|
+
function trim(lines: string[]): string[] {
|
|
579
|
+
let start = 0;
|
|
580
|
+
let end = lines.length;
|
|
581
|
+
|
|
582
|
+
while (start < end && !lines[start]!.trim()) start += 1;
|
|
583
|
+
while (end > start && !lines[end - 1]!.trim()) end -= 1;
|
|
584
|
+
|
|
585
|
+
return lines.slice(start, end);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* An MDC block component with YAML props and Markdown inside it.
|
|
590
|
+
*
|
|
591
|
+
* The fence LENGTH is computed rather than fixed, exactly as in
|
|
592
|
+
* `sections-openapi.ts`: a release note containing a `::callout` would
|
|
593
|
+
* otherwise close the component early and spill its props into the page.
|
|
594
|
+
*
|
|
595
|
+
* EVERY STRING IS QUOTED, and that is the same defence the frontmatter helper
|
|
596
|
+
* below makes for the same reason one level up. A plain `2026-02-01` is a
|
|
597
|
+
* timestamp under the schema the props are read back with, so an unquoted date
|
|
598
|
+
* reaches the component as a `Date` and `<time :datetime>` prints the reader's
|
|
599
|
+
* own timezone rather than the day the release was cut.
|
|
600
|
+
*/
|
|
601
|
+
function component(
|
|
602
|
+
name: string,
|
|
603
|
+
props: Record<string, unknown>,
|
|
604
|
+
slot?: string
|
|
605
|
+
): string {
|
|
606
|
+
const body = slot?.trim() ?? '';
|
|
607
|
+
|
|
608
|
+
const longest = Math.max(
|
|
609
|
+
2,
|
|
610
|
+
...body.split('\n').map((line) => /^\s*(:+)/.exec(line)?.[1]?.length ?? 0)
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
const fence = ':'.repeat(Math.max(3, longest + 1));
|
|
614
|
+
|
|
615
|
+
return [
|
|
616
|
+
`${fence}${name}`,
|
|
617
|
+
'---',
|
|
618
|
+
stringifyYaml(props, {
|
|
619
|
+
defaultStringType: 'QUOTE_DOUBLE',
|
|
620
|
+
defaultKeyType: 'PLAIN',
|
|
621
|
+
lineWidth: 0
|
|
622
|
+
}).trimEnd(),
|
|
623
|
+
'---',
|
|
624
|
+
...(body ? [body] : []),
|
|
625
|
+
fence
|
|
626
|
+
].join('\n');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* A frontmatter block YAML can read back.
|
|
631
|
+
*
|
|
632
|
+
* Every value is written as a JSON string, which is also a YAML double-quoted
|
|
633
|
+
* scalar — so a release title carrying a colon cannot end the mapping early,
|
|
634
|
+
* which is the exact failure `tests/frontmatter-yaml.test.ts` exists over.
|
|
635
|
+
*/
|
|
636
|
+
function frontmatter(fields: Record<string, string | undefined>): string {
|
|
637
|
+
return [
|
|
638
|
+
'---',
|
|
639
|
+
...Object.entries(fields)
|
|
640
|
+
.filter(([, value]) => value !== undefined)
|
|
641
|
+
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`),
|
|
642
|
+
'---'
|
|
643
|
+
].join('\n');
|
|
644
|
+
}
|