@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
package/openapi-parse.ts
ADDED
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An OpenAPI document, read into the shape the reference pages render.
|
|
3
|
+
*
|
|
4
|
+
* PURE, and separate from `sections-openapi.ts` for the reason
|
|
5
|
+
* `sources-resolve.ts` is separate from `sources.ts`: this half is text in and
|
|
6
|
+
* a model out, so it can be tested without a build, a file system or a Nuxt
|
|
7
|
+
* runtime — and the model it returns is the contract the theme's components
|
|
8
|
+
* read, which is the only place the spec's shape is allowed to be known.
|
|
9
|
+
*
|
|
10
|
+
* Three jobs, and they are the reason this file is not the parser call alone:
|
|
11
|
+
*
|
|
12
|
+
* - **One document out of two dialects.** 3.0 and 3.1 spell the same things
|
|
13
|
+
* differently — `nullable` against a `null` in the type list, a boolean
|
|
14
|
+
* `exclusiveMinimum` against a numeric one, a singular `example` against a
|
|
15
|
+
* list. The renderer sees ONE of those spellings, never both, because a
|
|
16
|
+
* component that branches on the document version is a component that grows a
|
|
17
|
+
* branch per keyword.
|
|
18
|
+
* - **`$ref` resolution, cycles included.** A reference is followed and the
|
|
19
|
+
* NAME it was followed through is kept: a schema drawn as `Pet` rather than
|
|
20
|
+
* as its expansion is the difference between a readable reference and a wall.
|
|
21
|
+
* A cycle — and `Pet.friends: Pet[]` is the ordinary case, not the exotic
|
|
22
|
+
* one — is cut and marked rather than expanded until the stack gives out.
|
|
23
|
+
* - **Saying what it could not read.** Every construct it declines to follow —
|
|
24
|
+
* an external `$ref`, a Swagger 2.0 document, a `$ref` pointing at nothing —
|
|
25
|
+
* lands in `warnings`, which the build prints. A reference that silently drops
|
|
26
|
+
* half a spec is worse than no reference at all.
|
|
27
|
+
*/
|
|
28
|
+
import { parse as parseYaml } from 'yaml';
|
|
29
|
+
import type {
|
|
30
|
+
DuxtOpenApiBody,
|
|
31
|
+
DuxtOpenApiCallback,
|
|
32
|
+
DuxtOpenApiCallbackOperation,
|
|
33
|
+
DuxtOpenApiCodeSample,
|
|
34
|
+
DuxtOpenApiConstraints,
|
|
35
|
+
DuxtOpenApiEncoding,
|
|
36
|
+
DuxtOpenApiExample,
|
|
37
|
+
DuxtOpenApiExternalDocs,
|
|
38
|
+
DuxtOpenApiHeader,
|
|
39
|
+
DuxtOpenApiLink,
|
|
40
|
+
DuxtOpenApiMediaType,
|
|
41
|
+
DuxtOpenApiOperation,
|
|
42
|
+
DuxtOpenApiParameter,
|
|
43
|
+
DuxtOpenApiResponse,
|
|
44
|
+
DuxtOpenApiSchema,
|
|
45
|
+
DuxtOpenApiSecurity,
|
|
46
|
+
DuxtOpenApiSecurityScheme,
|
|
47
|
+
DuxtOpenApiSecuritySchemeType,
|
|
48
|
+
DuxtOpenApiServer,
|
|
49
|
+
DuxtOpenApiSpec,
|
|
50
|
+
DuxtOpenApiTag
|
|
51
|
+
} from './openapi-model';
|
|
52
|
+
|
|
53
|
+
import {
|
|
54
|
+
DUXT_OPENAPI_DEFAULT_TAG,
|
|
55
|
+
DUXT_OPENAPI_WEBHOOKS_TAG
|
|
56
|
+
} from './openapi-model';
|
|
57
|
+
|
|
58
|
+
// Re-exported so a caller reads the model through the parser it came out of,
|
|
59
|
+
// and never has to know the split exists.
|
|
60
|
+
export type * from './openapi-model';
|
|
61
|
+
export { DUXT_OPENAPI_DEFAULT_TAG, DUXT_OPENAPI_WEBHOOKS_TAG };
|
|
62
|
+
|
|
63
|
+
/** The HTTP methods a Path Item may carry, in the order a reference lists them. */
|
|
64
|
+
const METHODS = [
|
|
65
|
+
'get',
|
|
66
|
+
'post',
|
|
67
|
+
'put',
|
|
68
|
+
'patch',
|
|
69
|
+
'delete',
|
|
70
|
+
'head',
|
|
71
|
+
'options',
|
|
72
|
+
'trace'
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
type Node = Record<string, unknown>;
|
|
76
|
+
|
|
77
|
+
const isObject = (value: unknown): value is Node =>
|
|
78
|
+
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
79
|
+
|
|
80
|
+
const str = (value: unknown): string | undefined =>
|
|
81
|
+
typeof value === 'string' ? value : undefined;
|
|
82
|
+
|
|
83
|
+
const num = (value: unknown): number | undefined =>
|
|
84
|
+
typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
85
|
+
|
|
86
|
+
const bool = (value: unknown): boolean | undefined =>
|
|
87
|
+
typeof value === 'boolean' ? value : undefined;
|
|
88
|
+
|
|
89
|
+
const list = (value: unknown): unknown[] => (Array.isArray(value) ? value : []);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The document, as text.
|
|
93
|
+
*
|
|
94
|
+
* JSON is read as YAML rather than as JSON, because YAML 1.2 is a superset of
|
|
95
|
+
* it — one parser, one error message, and a `.json` file that happens to carry
|
|
96
|
+
* a trailing comma still says which line it is on.
|
|
97
|
+
*/
|
|
98
|
+
export function parseOpenApiDocument(artefact: string): DuxtOpenApiSpec {
|
|
99
|
+
let document: unknown;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
document = parseYaml(artefact);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`the document is neither JSON nor YAML: ${(error as Error).message}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!isObject(document)) {
|
|
110
|
+
throw new Error('the document is not an object');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if ('swagger' in document) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`this is a Swagger ${str(document.swagger) ?? '2.0'} document. ` +
|
|
116
|
+
'duxt reads OpenAPI 3.0 and 3.1 — convert it before sourcing it.'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const declared = str(document.openapi);
|
|
121
|
+
|
|
122
|
+
if (!declared) {
|
|
123
|
+
throw new Error('the document declares no `openapi` version');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const dialect = declared.startsWith('3.1')
|
|
127
|
+
? '3.1'
|
|
128
|
+
: declared.startsWith('3.0')
|
|
129
|
+
? '3.0'
|
|
130
|
+
: undefined;
|
|
131
|
+
|
|
132
|
+
if (!dialect) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`the document declares OpenAPI ${declared}. duxt reads 3.0 and 3.1.`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const spec = read(document, dialect);
|
|
139
|
+
|
|
140
|
+
// The model the parser HANDS BACK is the compacted one, so there is exactly
|
|
141
|
+
// one shape: what the tests assert, what the section type writes into a
|
|
142
|
+
// page's props, and what the theme's components receive are the same object.
|
|
143
|
+
// `warnings` is rebuilt outside it, because an empty list there is a real
|
|
144
|
+
// answer — nothing went unread — rather than noise to drop.
|
|
145
|
+
return { ...compactOpenApi(spec), warnings: spec.warnings };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function read(document: Node, dialect: '3.0' | '3.1'): DuxtOpenApiSpec {
|
|
149
|
+
const warnings: string[] = [];
|
|
150
|
+
const warn = (message: string) => {
|
|
151
|
+
if (!warnings.includes(message)) warnings.push(message);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A `$ref`, followed once.
|
|
156
|
+
*
|
|
157
|
+
* Only INSIDE this document: an external reference names a file this layer
|
|
158
|
+
* was never handed — the declaration is one path, and reaching past it would
|
|
159
|
+
* reopen the build-time-network question `DuxtGeneratedSection.path` closes.
|
|
160
|
+
* So it is reported and drawn as the reference it is.
|
|
161
|
+
*/
|
|
162
|
+
const follow = (
|
|
163
|
+
node: unknown
|
|
164
|
+
): { value: unknown; name?: string; ref?: string; external?: boolean } => {
|
|
165
|
+
if (!isObject(node) || typeof node.$ref !== 'string')
|
|
166
|
+
return { value: node };
|
|
167
|
+
|
|
168
|
+
const ref = node.$ref;
|
|
169
|
+
|
|
170
|
+
if (!ref.startsWith('#/')) {
|
|
171
|
+
warn(
|
|
172
|
+
`the reference "${ref}" points outside the document, which duxt does ` +
|
|
173
|
+
'not follow. Bundle the specification before sourcing it.'
|
|
174
|
+
);
|
|
175
|
+
return { value: undefined, ref, external: true };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const segments = ref
|
|
179
|
+
.slice(2)
|
|
180
|
+
.split('/')
|
|
181
|
+
.map((segment) =>
|
|
182
|
+
decodeURIComponent(segment).replaceAll('~1', '/').replaceAll('~0', '~')
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
let current: unknown = document;
|
|
186
|
+
|
|
187
|
+
for (const segment of segments) {
|
|
188
|
+
if (Array.isArray(current)) current = current[Number(segment)];
|
|
189
|
+
else if (isObject(current)) current = current[segment];
|
|
190
|
+
else current = undefined;
|
|
191
|
+
|
|
192
|
+
if (current === undefined) break;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (current === undefined) {
|
|
196
|
+
warn(`the reference "${ref}" points at nothing in this document.`);
|
|
197
|
+
return { value: undefined, ref };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// The LAST segment is the component's own name, which is what a reader
|
|
201
|
+
// recognises — `Pet`, not `#/components/schemas/Pet`.
|
|
202
|
+
return { value: current, name: segments.at(-1), ref };
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/** The externalDocs object, wherever one may appear. */
|
|
206
|
+
const externalDocs = (node: unknown): DuxtOpenApiExternalDocs | undefined => {
|
|
207
|
+
if (!isObject(node)) return undefined;
|
|
208
|
+
const url = str(node.url);
|
|
209
|
+
return url ? { url, description: str(node.description) } : undefined;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A schema, with `$ref` followed and both dialects reduced to one spelling.
|
|
214
|
+
*
|
|
215
|
+
* `stack` holds the references currently being expanded. A schema that
|
|
216
|
+
* reaches itself is cut with `circular` rather than followed — `Pet.friends:
|
|
217
|
+
* Pet[]` is the ordinary shape of a schema, not a broken one, and expanding
|
|
218
|
+
* it is a stack overflow at build time.
|
|
219
|
+
*/
|
|
220
|
+
const schema = (
|
|
221
|
+
node: unknown,
|
|
222
|
+
stack: string[] = [],
|
|
223
|
+
inherited?: string
|
|
224
|
+
): DuxtOpenApiSchema | undefined => {
|
|
225
|
+
const followed = follow(node);
|
|
226
|
+
|
|
227
|
+
if (followed.external) {
|
|
228
|
+
return blank({ ref: followed.ref, external: true });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (followed.ref && followed.value === undefined) {
|
|
232
|
+
return blank({ ref: followed.ref });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (followed.ref && stack.includes(followed.ref)) {
|
|
236
|
+
return blank({
|
|
237
|
+
ref: followed.ref,
|
|
238
|
+
name: followed.name,
|
|
239
|
+
circular: true
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const value = followed.value;
|
|
244
|
+
|
|
245
|
+
// `true` and `false` are schemas in JSON Schema: everything and nothing.
|
|
246
|
+
if (value === true) return blank({});
|
|
247
|
+
if (value === false) return blank({ types: ['never'] });
|
|
248
|
+
if (!isObject(value)) return undefined;
|
|
249
|
+
|
|
250
|
+
const next = followed.ref ? [...stack, followed.ref] : stack;
|
|
251
|
+
const name = followed.name ?? inherited;
|
|
252
|
+
|
|
253
|
+
const types = new Set<string>();
|
|
254
|
+
const declaredType = value.type;
|
|
255
|
+
|
|
256
|
+
if (typeof declaredType === 'string') types.add(declaredType);
|
|
257
|
+
for (const entry of list(declaredType)) {
|
|
258
|
+
if (typeof entry === 'string') types.add(entry);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 3.0 says `nullable: true`; 3.1 says `null` in the type list. ONE of them
|
|
262
|
+
// reaches the renderer, so no component has to know which document it is
|
|
263
|
+
// drawing.
|
|
264
|
+
if (dialect === '3.0' && value.nullable === true) types.add('null');
|
|
265
|
+
|
|
266
|
+
const required = new Set(
|
|
267
|
+
list(value.required).filter(
|
|
268
|
+
(entry): entry is string => typeof entry === 'string'
|
|
269
|
+
)
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const properties = isObject(value.properties)
|
|
273
|
+
? Object.entries(value.properties).map(([key, property]) => ({
|
|
274
|
+
name: key,
|
|
275
|
+
required: required.has(key),
|
|
276
|
+
schema: schema(property, next) ?? blank({})
|
|
277
|
+
}))
|
|
278
|
+
: undefined;
|
|
279
|
+
|
|
280
|
+
const built: DuxtOpenApiSchema = {
|
|
281
|
+
...(name ? { name } : {}),
|
|
282
|
+
...(followed.ref ? { ref: followed.ref } : {}),
|
|
283
|
+
types: [...types],
|
|
284
|
+
format: str(value.format),
|
|
285
|
+
title: str(value.title),
|
|
286
|
+
description: str(value.description),
|
|
287
|
+
default: value.default,
|
|
288
|
+
const: value.const,
|
|
289
|
+
enum: Array.isArray(value.enum) ? value.enum : undefined,
|
|
290
|
+
examples: schemaExamples(value),
|
|
291
|
+
deprecated: bool(value.deprecated),
|
|
292
|
+
readOnly: bool(value.readOnly),
|
|
293
|
+
writeOnly: bool(value.writeOnly),
|
|
294
|
+
constraints: constraints(value),
|
|
295
|
+
properties,
|
|
296
|
+
additionalProperties: additional(value.additionalProperties, next),
|
|
297
|
+
items: schema(value.items, next),
|
|
298
|
+
prefixItems: list(value.prefixItems)
|
|
299
|
+
.map((entry) => schema(entry, next))
|
|
300
|
+
.filter((entry): entry is DuxtOpenApiSchema => Boolean(entry)),
|
|
301
|
+
oneOf: composition(value.oneOf, next),
|
|
302
|
+
anyOf: composition(value.anyOf, next),
|
|
303
|
+
allOf: composition(value.allOf, next),
|
|
304
|
+
not: schema(value.not, next),
|
|
305
|
+
discriminator: discriminator(value.discriminator),
|
|
306
|
+
externalDocs: externalDocs(value.externalDocs)
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if (!built.prefixItems?.length) delete built.prefixItems;
|
|
310
|
+
|
|
311
|
+
return mergeAllOf(prune(built));
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const composition = (
|
|
315
|
+
node: unknown,
|
|
316
|
+
stack: string[]
|
|
317
|
+
): DuxtOpenApiSchema[] | undefined => {
|
|
318
|
+
if (!Array.isArray(node) || !node.length) return undefined;
|
|
319
|
+
|
|
320
|
+
const branches = node
|
|
321
|
+
.map((entry) => schema(entry, stack))
|
|
322
|
+
.filter((entry): entry is DuxtOpenApiSchema => Boolean(entry));
|
|
323
|
+
|
|
324
|
+
return branches.length ? branches : undefined;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const additional = (
|
|
328
|
+
node: unknown,
|
|
329
|
+
stack: string[]
|
|
330
|
+
): DuxtOpenApiSchema | false | undefined => {
|
|
331
|
+
if (node === undefined) return undefined;
|
|
332
|
+
if (node === false) return false;
|
|
333
|
+
if (node === true) return blank({});
|
|
334
|
+
|
|
335
|
+
return schema(node, stack);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/** One list, whichever dialect wrote it. */
|
|
339
|
+
const schemaExamples = (value: Node): unknown[] => {
|
|
340
|
+
if (Array.isArray(value.examples)) return value.examples;
|
|
341
|
+
return 'example' in value ? [value.example] : [];
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const namedExamples = (node: Node): DuxtOpenApiExample[] => {
|
|
345
|
+
const examples = isObject(node.examples)
|
|
346
|
+
? Object.entries(node.examples).map(([key, entry]) => {
|
|
347
|
+
const followed = follow(entry);
|
|
348
|
+
const value = isObject(followed.value) ? followed.value : {};
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
name: key,
|
|
352
|
+
summary: str(value.summary),
|
|
353
|
+
description: str(value.description),
|
|
354
|
+
value: value.value,
|
|
355
|
+
externalValue: str(value.externalValue)
|
|
356
|
+
};
|
|
357
|
+
})
|
|
358
|
+
: [];
|
|
359
|
+
|
|
360
|
+
// A singular `example` is the same statement with no name, so it is
|
|
361
|
+
// promoted rather than kept as a second field every component must read.
|
|
362
|
+
if (!examples.length && 'example' in node) {
|
|
363
|
+
return [{ name: 'default', value: node.example }];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return examples;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const mediaTypes = (node: unknown): DuxtOpenApiMediaType[] => {
|
|
370
|
+
if (!isObject(node)) return [];
|
|
371
|
+
|
|
372
|
+
return Object.entries(node).map(([type, entry]) => {
|
|
373
|
+
const value = isObject(entry) ? entry : {};
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
type,
|
|
377
|
+
schema: schema(value.schema),
|
|
378
|
+
examples: namedExamples(value),
|
|
379
|
+
encoding: encodings(value.encoding)
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const encodings = (node: unknown): DuxtOpenApiEncoding[] | undefined => {
|
|
385
|
+
if (!isObject(node)) return undefined;
|
|
386
|
+
|
|
387
|
+
const entries = Object.entries(node).map(([property, entry]) => {
|
|
388
|
+
const value = isObject(entry) ? entry : {};
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
property,
|
|
392
|
+
contentType: str(value.contentType),
|
|
393
|
+
style: str(value.style),
|
|
394
|
+
explode: bool(value.explode),
|
|
395
|
+
allowReserved: bool(value.allowReserved),
|
|
396
|
+
headers: headers(value.headers)
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
return entries.length ? entries : undefined;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const headers = (node: unknown): DuxtOpenApiHeader[] => {
|
|
404
|
+
if (!isObject(node)) return [];
|
|
405
|
+
|
|
406
|
+
return Object.entries(node).map(([name, entry]) => {
|
|
407
|
+
const followed = follow(entry).value;
|
|
408
|
+
const header = isObject(followed) ? followed : {};
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
name,
|
|
412
|
+
description: str(header.description),
|
|
413
|
+
required: header.required === true,
|
|
414
|
+
deprecated: bool(header.deprecated),
|
|
415
|
+
schema: schema(header.schema),
|
|
416
|
+
examples: namedExamples(header)
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const parameters = (node: unknown): DuxtOpenApiParameter[] =>
|
|
422
|
+
list(node)
|
|
423
|
+
.map((entry): DuxtOpenApiParameter | undefined => {
|
|
424
|
+
const value = follow(entry).value;
|
|
425
|
+
if (!isObject(value)) return undefined;
|
|
426
|
+
|
|
427
|
+
const where = str(value.in);
|
|
428
|
+
const name = str(value.name);
|
|
429
|
+
|
|
430
|
+
// Both are required by the specification, and a parameter missing
|
|
431
|
+
// either cannot be rendered, sent, or matched against the path item's
|
|
432
|
+
// own list. Dropped rather than drawn as a blank row.
|
|
433
|
+
if (!where || !name) return undefined;
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
name,
|
|
437
|
+
in: where as DuxtOpenApiParameter['in'],
|
|
438
|
+
description: str(value.description),
|
|
439
|
+
// A path parameter is required by the specification whether or not
|
|
440
|
+
// the document remembered to say so.
|
|
441
|
+
required: value.required === true || where === 'path',
|
|
442
|
+
deprecated: bool(value.deprecated),
|
|
443
|
+
allowEmptyValue: bool(value.allowEmptyValue),
|
|
444
|
+
style: str(value.style),
|
|
445
|
+
explode: bool(value.explode),
|
|
446
|
+
schema: schema(value.schema),
|
|
447
|
+
examples: namedExamples(value),
|
|
448
|
+
content: isObject(value.content)
|
|
449
|
+
? mediaTypes(value.content)
|
|
450
|
+
: undefined
|
|
451
|
+
};
|
|
452
|
+
})
|
|
453
|
+
.filter((entry) => entry !== undefined);
|
|
454
|
+
|
|
455
|
+
const requestBody = (node: unknown): DuxtOpenApiBody | undefined => {
|
|
456
|
+
const value = follow(node).value;
|
|
457
|
+
if (!isObject(value)) return undefined;
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
description: str(value.description),
|
|
461
|
+
required: value.required === true,
|
|
462
|
+
content: mediaTypes(value.content)
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const links = (node: unknown): DuxtOpenApiLink[] => {
|
|
467
|
+
if (!isObject(node)) return [];
|
|
468
|
+
|
|
469
|
+
return Object.entries(node).map(([name, entry]) => {
|
|
470
|
+
const followed = follow(entry).value;
|
|
471
|
+
const value = isObject(followed) ? followed : {};
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
name,
|
|
475
|
+
operationId: str(value.operationId),
|
|
476
|
+
operationRef: str(value.operationRef),
|
|
477
|
+
description: str(value.description),
|
|
478
|
+
parameters: isObject(value.parameters)
|
|
479
|
+
? (value.parameters as Record<string, unknown>)
|
|
480
|
+
: undefined,
|
|
481
|
+
requestBody: value.requestBody,
|
|
482
|
+
server: server(value.server)
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const responses = (node: unknown): DuxtOpenApiResponse[] => {
|
|
488
|
+
if (!isObject(node)) return [];
|
|
489
|
+
|
|
490
|
+
return Object.entries(node)
|
|
491
|
+
.sort(([a], [b]) => statusOrder(a) - statusOrder(b))
|
|
492
|
+
.map(([status, entry]) => {
|
|
493
|
+
const followed = follow(entry).value;
|
|
494
|
+
const value = isObject(followed) ? followed : {};
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
status,
|
|
498
|
+
description: str(value.description),
|
|
499
|
+
headers: headers(value.headers),
|
|
500
|
+
content: mediaTypes(value.content),
|
|
501
|
+
links: links(value.links)
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const server = (node: unknown): DuxtOpenApiServer | undefined => {
|
|
507
|
+
if (!isObject(node)) return undefined;
|
|
508
|
+
const url = str(node.url);
|
|
509
|
+
if (!url) return undefined;
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
url,
|
|
513
|
+
description: str(node.description),
|
|
514
|
+
variables: isObject(node.variables)
|
|
515
|
+
? Object.entries(node.variables).map(([name, entry]) => {
|
|
516
|
+
const value = isObject(entry) ? entry : {};
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
name,
|
|
520
|
+
default: str(value.default) ?? '',
|
|
521
|
+
enum: list(value.enum).filter(
|
|
522
|
+
(option): option is string => typeof option === 'string'
|
|
523
|
+
),
|
|
524
|
+
description: str(value.description)
|
|
525
|
+
};
|
|
526
|
+
})
|
|
527
|
+
: []
|
|
528
|
+
};
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const servers = (node: unknown): DuxtOpenApiServer[] =>
|
|
532
|
+
list(node)
|
|
533
|
+
.map(server)
|
|
534
|
+
.filter((entry): entry is DuxtOpenApiServer => Boolean(entry));
|
|
535
|
+
|
|
536
|
+
const securitySchemes: DuxtOpenApiSecurityScheme[] = isObject(
|
|
537
|
+
(document.components as Node | undefined)?.securitySchemes
|
|
538
|
+
)
|
|
539
|
+
? Object.entries((document.components as Node).securitySchemes as Node).map(
|
|
540
|
+
([key, entry]) => {
|
|
541
|
+
const followed = follow(entry).value;
|
|
542
|
+
const value = isObject(followed) ? followed : {};
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
key,
|
|
546
|
+
type: (str(value.type) ??
|
|
547
|
+
'apiKey') as DuxtOpenApiSecuritySchemeType,
|
|
548
|
+
description: str(value.description),
|
|
549
|
+
name: str(value.name),
|
|
550
|
+
in: str(value.in) as DuxtOpenApiSecurityScheme['in'],
|
|
551
|
+
scheme: str(value.scheme),
|
|
552
|
+
// `x-duxt-example`, not `example`: OpenAPI defines no such field on
|
|
553
|
+
// a security scheme, so writing one would be inventing a meaning
|
|
554
|
+
// for a name the specification may yet give another.
|
|
555
|
+
example: str(value['x-duxt-example']),
|
|
556
|
+
bearerFormat: str(value.bearerFormat),
|
|
557
|
+
openIdConnectUrl: str(value.openIdConnectUrl),
|
|
558
|
+
flows: isObject(value.flows)
|
|
559
|
+
? Object.entries(value.flows).map(([name, flow]) => {
|
|
560
|
+
const it = isObject(flow) ? flow : {};
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
name,
|
|
564
|
+
authorizationUrl: str(it.authorizationUrl),
|
|
565
|
+
tokenUrl: str(it.tokenUrl),
|
|
566
|
+
refreshUrl: str(it.refreshUrl),
|
|
567
|
+
scopes: isObject(it.scopes)
|
|
568
|
+
? Object.entries(it.scopes).map(
|
|
569
|
+
([scope, description]) => ({
|
|
570
|
+
name: scope,
|
|
571
|
+
description: str(description)
|
|
572
|
+
})
|
|
573
|
+
)
|
|
574
|
+
: []
|
|
575
|
+
};
|
|
576
|
+
})
|
|
577
|
+
: []
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
)
|
|
581
|
+
: [];
|
|
582
|
+
|
|
583
|
+
const security = (node: unknown): DuxtOpenApiSecurity | undefined => {
|
|
584
|
+
if (!Array.isArray(node)) return undefined;
|
|
585
|
+
|
|
586
|
+
return node.map((entry) =>
|
|
587
|
+
isObject(entry)
|
|
588
|
+
? Object.entries(entry).map(([key, scopes]) => ({
|
|
589
|
+
key,
|
|
590
|
+
scopes: list(scopes).filter(
|
|
591
|
+
(scope): scope is string => typeof scope === 'string'
|
|
592
|
+
),
|
|
593
|
+
scheme: securitySchemes.find((candidate) => candidate.key === key)
|
|
594
|
+
}))
|
|
595
|
+
: []
|
|
596
|
+
);
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* `x-codeSamples`, under both spellings it has had.
|
|
601
|
+
*
|
|
602
|
+
* Redocly renamed `x-code-samples` to `x-codeSamples` and documents carry
|
|
603
|
+
* either, so both are read and the newer one wins where a document has both.
|
|
604
|
+
* `lang` is required — it is both the grammar and the name of the group the
|
|
605
|
+
* sample joins — and a sample with no source is nothing to show.
|
|
606
|
+
*/
|
|
607
|
+
const codeSamples = (
|
|
608
|
+
operation: Record<string, unknown>
|
|
609
|
+
): DuxtOpenApiCodeSample[] =>
|
|
610
|
+
list(operation['x-codeSamples'] ?? operation['x-code-samples'])
|
|
611
|
+
.filter(isObject)
|
|
612
|
+
.map((entry) => ({
|
|
613
|
+
lang: str(entry.lang) ?? '',
|
|
614
|
+
label: str(entry.label),
|
|
615
|
+
source: str(entry.source) ?? ''
|
|
616
|
+
}))
|
|
617
|
+
.filter((entry) => !!entry.lang && !!entry.source);
|
|
618
|
+
|
|
619
|
+
const callbacks = (node: unknown): DuxtOpenApiCallback[] => {
|
|
620
|
+
if (!isObject(node)) return [];
|
|
621
|
+
|
|
622
|
+
return Object.entries(node).map(([name, entry]) => {
|
|
623
|
+
const followed = follow(entry).value;
|
|
624
|
+
const value = isObject(followed) ? followed : {};
|
|
625
|
+
|
|
626
|
+
const operations: DuxtOpenApiCallbackOperation[] = [];
|
|
627
|
+
|
|
628
|
+
for (const [expression, item] of Object.entries(value)) {
|
|
629
|
+
if (!isObject(item)) continue;
|
|
630
|
+
|
|
631
|
+
for (const method of METHODS) {
|
|
632
|
+
const operation = item[method];
|
|
633
|
+
if (!isObject(operation)) continue;
|
|
634
|
+
|
|
635
|
+
operations.push({
|
|
636
|
+
expression,
|
|
637
|
+
method,
|
|
638
|
+
summary: str(operation.summary),
|
|
639
|
+
description: str(operation.description),
|
|
640
|
+
requestBody: requestBody(operation.requestBody),
|
|
641
|
+
responses: responses(operation.responses)
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return { name, operations };
|
|
647
|
+
});
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const operations: DuxtOpenApiOperation[] = [];
|
|
651
|
+
|
|
652
|
+
const readPathItem = (
|
|
653
|
+
path: string,
|
|
654
|
+
node: unknown,
|
|
655
|
+
kind: 'operation' | 'webhook'
|
|
656
|
+
) => {
|
|
657
|
+
const item = follow(node).value;
|
|
658
|
+
if (!isObject(item)) return;
|
|
659
|
+
|
|
660
|
+
// Declared once on the Path Item and inherited by every operation under it.
|
|
661
|
+
const shared = parameters(item.parameters);
|
|
662
|
+
|
|
663
|
+
for (const method of METHODS) {
|
|
664
|
+
const operation = item[method];
|
|
665
|
+
if (!isObject(operation)) continue;
|
|
666
|
+
|
|
667
|
+
const own = parameters(operation.parameters);
|
|
668
|
+
|
|
669
|
+
// The operation's own entry WINS over the path's, matched on name and
|
|
670
|
+
// location — which is what the specification says, and the reason a
|
|
671
|
+
// concatenation would list some parameters twice.
|
|
672
|
+
const merged = [
|
|
673
|
+
...shared.filter(
|
|
674
|
+
(entry) =>
|
|
675
|
+
!own.some(
|
|
676
|
+
(candidate) =>
|
|
677
|
+
candidate.name === entry.name && candidate.in === entry.in
|
|
678
|
+
)
|
|
679
|
+
),
|
|
680
|
+
...own
|
|
681
|
+
];
|
|
682
|
+
|
|
683
|
+
operations.push({
|
|
684
|
+
kind,
|
|
685
|
+
method,
|
|
686
|
+
path,
|
|
687
|
+
operationId: str(operation.operationId),
|
|
688
|
+
summary: str(operation.summary),
|
|
689
|
+
description: str(operation.description),
|
|
690
|
+
deprecated: operation.deprecated === true,
|
|
691
|
+
tags: list(operation.tags).filter(
|
|
692
|
+
(tag): tag is string => typeof tag === 'string'
|
|
693
|
+
),
|
|
694
|
+
externalDocs: externalDocs(operation.externalDocs),
|
|
695
|
+
servers: servers(operation.servers ?? item.servers),
|
|
696
|
+
security: security(operation.security),
|
|
697
|
+
parameters: merged,
|
|
698
|
+
requestBody: requestBody(operation.requestBody),
|
|
699
|
+
responses: responses(operation.responses),
|
|
700
|
+
callbacks: callbacks(operation.callbacks),
|
|
701
|
+
codeSamples: codeSamples(operation)
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
if (isObject(document.paths)) {
|
|
707
|
+
for (const [path, item] of Object.entries(document.paths)) {
|
|
708
|
+
readPathItem(path, item, 'operation');
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (isObject(document.webhooks)) {
|
|
713
|
+
if (dialect === '3.0') {
|
|
714
|
+
warn(
|
|
715
|
+
'the document declares `webhooks`, which OpenAPI 3.0 has no such key ' +
|
|
716
|
+
'for. They are rendered, but the document should declare 3.1.'
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
for (const [name, item] of Object.entries(document.webhooks)) {
|
|
721
|
+
readPathItem(name, item, 'webhook');
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const info = isObject(document.info) ? document.info : {};
|
|
726
|
+
|
|
727
|
+
return {
|
|
728
|
+
dialect,
|
|
729
|
+
info: {
|
|
730
|
+
title: str(info.title) ?? 'API',
|
|
731
|
+
version: str(info.version) ?? '',
|
|
732
|
+
summary: str(info.summary),
|
|
733
|
+
description: str(info.description),
|
|
734
|
+
termsOfService: str(info.termsOfService),
|
|
735
|
+
contact: isObject(info.contact)
|
|
736
|
+
? {
|
|
737
|
+
name: str(info.contact.name),
|
|
738
|
+
url: str(info.contact.url),
|
|
739
|
+
email: str(info.contact.email)
|
|
740
|
+
}
|
|
741
|
+
: undefined,
|
|
742
|
+
license: isObject(info.license)
|
|
743
|
+
? {
|
|
744
|
+
name: str(info.license.name) ?? '',
|
|
745
|
+
identifier: str(info.license.identifier),
|
|
746
|
+
url: str(info.license.url)
|
|
747
|
+
}
|
|
748
|
+
: undefined
|
|
749
|
+
},
|
|
750
|
+
servers: servers(document.servers),
|
|
751
|
+
externalDocs: externalDocs(document.externalDocs),
|
|
752
|
+
security: security(document.security),
|
|
753
|
+
securitySchemes,
|
|
754
|
+
tags: group(operations, list(document.tags)),
|
|
755
|
+
warnings
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* The operations, grouped into the tags the pages are built from.
|
|
761
|
+
*
|
|
762
|
+
* The document's own `tags` array sets the ORDER and carries the descriptions;
|
|
763
|
+
* a tag used by an operation and never declared is appended rather than
|
|
764
|
+
* dropped, because a document is allowed to omit the array entirely. An
|
|
765
|
+
* operation with no tag at all lands in one group of its own, so no endpoint is
|
|
766
|
+
* ever unreachable for want of a heading.
|
|
767
|
+
*/
|
|
768
|
+
function group(
|
|
769
|
+
operations: DuxtOpenApiOperation[],
|
|
770
|
+
declared: unknown[]
|
|
771
|
+
): DuxtOpenApiTag[] {
|
|
772
|
+
// The operations list is non-optional HERE, unlike on the model: the tags
|
|
773
|
+
// are built by pushing into it, and only `compactOpenApi` — which runs after
|
|
774
|
+
// this — is allowed to make an empty one disappear.
|
|
775
|
+
type Filled = DuxtOpenApiTag & { operations: DuxtOpenApiOperation[] };
|
|
776
|
+
|
|
777
|
+
const tags = new Map<string, Filled>();
|
|
778
|
+
|
|
779
|
+
const ensure = (name: string, webhooks = false) => {
|
|
780
|
+
const existing = tags.get(name);
|
|
781
|
+
if (existing) return existing;
|
|
782
|
+
|
|
783
|
+
const created: Filled = {
|
|
784
|
+
name,
|
|
785
|
+
...(webhooks ? { webhooks: true } : {}),
|
|
786
|
+
operations: []
|
|
787
|
+
};
|
|
788
|
+
tags.set(name, created);
|
|
789
|
+
return created;
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
for (const entry of declared) {
|
|
793
|
+
if (!isObject(entry)) continue;
|
|
794
|
+
const name = str(entry.name);
|
|
795
|
+
if (!name) continue;
|
|
796
|
+
|
|
797
|
+
const tag = ensure(name);
|
|
798
|
+
tag.description = str(entry.description);
|
|
799
|
+
|
|
800
|
+
if (isObject(entry.externalDocs)) {
|
|
801
|
+
const url = str(entry.externalDocs.url);
|
|
802
|
+
if (url) {
|
|
803
|
+
tag.externalDocs = {
|
|
804
|
+
url,
|
|
805
|
+
description: str(entry.externalDocs.description)
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
for (const operation of operations) {
|
|
812
|
+
if (operation.kind === 'webhook') {
|
|
813
|
+
ensure(DUXT_OPENAPI_WEBHOOKS_TAG, true).operations.push(operation);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const names = operation.tags?.length
|
|
818
|
+
? operation.tags
|
|
819
|
+
: [DUXT_OPENAPI_DEFAULT_TAG];
|
|
820
|
+
|
|
821
|
+
for (const name of names) ensure(name).operations.push(operation);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// A declared tag no operation uses documents nothing and would be an empty
|
|
825
|
+
// page in the sidebar.
|
|
826
|
+
return [...tags.values()].filter((tag) => tag.operations.length);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* `default` last, a wildcard at the END of its own range, everything else by
|
|
831
|
+
* code.
|
|
832
|
+
*
|
|
833
|
+
* The `X`s become nines rather than being stripped: `4XX` means "any client
|
|
834
|
+
* error", so it belongs after `404` and not before `201` — which is where
|
|
835
|
+
* parsing it as the number 4 puts it.
|
|
836
|
+
*/
|
|
837
|
+
function statusOrder(status: string): number {
|
|
838
|
+
if (status === 'default') return 10_000;
|
|
839
|
+
|
|
840
|
+
const numeric = Number.parseInt(status.replaceAll(/[Xx]/g, '9'), 10);
|
|
841
|
+
|
|
842
|
+
return Number.isNaN(numeric) ? 9_000 : numeric;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function constraints(value: Node): DuxtOpenApiConstraints {
|
|
846
|
+
const built: DuxtOpenApiConstraints = {
|
|
847
|
+
minimum: num(value.minimum),
|
|
848
|
+
maximum: num(value.maximum),
|
|
849
|
+
exclusiveMinimum: num(value.exclusiveMinimum),
|
|
850
|
+
exclusiveMaximum: num(value.exclusiveMaximum),
|
|
851
|
+
multipleOf: num(value.multipleOf),
|
|
852
|
+
minLength: num(value.minLength),
|
|
853
|
+
maxLength: num(value.maxLength),
|
|
854
|
+
pattern: str(value.pattern),
|
|
855
|
+
minItems: num(value.minItems),
|
|
856
|
+
maxItems: num(value.maxItems),
|
|
857
|
+
uniqueItems: bool(value.uniqueItems),
|
|
858
|
+
minProperties: num(value.minProperties),
|
|
859
|
+
maxProperties: num(value.maxProperties)
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
// 3.0 writes an exclusive bound as a BOOLEAN beside the inclusive one; 3.1
|
|
863
|
+
// writes the number itself. Reduced to 3.1's spelling, so the renderer draws
|
|
864
|
+
// one thing.
|
|
865
|
+
if (value.exclusiveMinimum === true && built.minimum !== undefined) {
|
|
866
|
+
built.exclusiveMinimum = built.minimum;
|
|
867
|
+
delete built.minimum;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (value.exclusiveMaximum === true && built.maximum !== undefined) {
|
|
871
|
+
built.exclusiveMaximum = built.maximum;
|
|
872
|
+
delete built.maximum;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
for (const [key, entry] of Object.entries(built)) {
|
|
876
|
+
if (entry === undefined) delete built[key as keyof DuxtOpenApiConstraints];
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return built;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function discriminator(node: unknown): DuxtOpenApiSchema['discriminator'] {
|
|
883
|
+
if (!isObject(node)) return undefined;
|
|
884
|
+
const propertyName = str(node.propertyName);
|
|
885
|
+
if (!propertyName) return undefined;
|
|
886
|
+
|
|
887
|
+
return {
|
|
888
|
+
propertyName,
|
|
889
|
+
mapping: isObject(node.mapping)
|
|
890
|
+
? (Object.fromEntries(
|
|
891
|
+
Object.entries(node.mapping).filter(
|
|
892
|
+
([, value]) => typeof value === 'string'
|
|
893
|
+
)
|
|
894
|
+
) as Record<string, string>)
|
|
895
|
+
: undefined
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* `allOf` of plain objects, folded into the schema that carries it.
|
|
901
|
+
*
|
|
902
|
+
* The overwhelmingly common shape is `allOf: [$ref, { properties: … }]` — a
|
|
903
|
+
* component plus the two fields this endpoint adds — and drawing that as a
|
|
904
|
+
* composition of two boxes is strictly harder to read than the object it
|
|
905
|
+
* describes. So object branches are merged, and anything that is NOT a plain
|
|
906
|
+
* object (a `oneOf`, a scalar, a branch with its own composition) is left in
|
|
907
|
+
* `allOf` for the renderer to draw as the composition it is.
|
|
908
|
+
*/
|
|
909
|
+
function mergeAllOf(value: DuxtOpenApiSchema): DuxtOpenApiSchema {
|
|
910
|
+
if (!value.allOf?.length) return value;
|
|
911
|
+
|
|
912
|
+
const mergeable = (branch: DuxtOpenApiSchema) =>
|
|
913
|
+
!branch.circular &&
|
|
914
|
+
!branch.external &&
|
|
915
|
+
!branch.oneOf &&
|
|
916
|
+
!branch.anyOf &&
|
|
917
|
+
!branch.allOf &&
|
|
918
|
+
!branch.not &&
|
|
919
|
+
!branch.items &&
|
|
920
|
+
!branch.enum &&
|
|
921
|
+
(branch.types ?? []).every((type) => type === 'object');
|
|
922
|
+
|
|
923
|
+
const merged = value.allOf.filter(mergeable);
|
|
924
|
+
const kept = value.allOf.filter((branch) => !mergeable(branch));
|
|
925
|
+
|
|
926
|
+
if (!merged.length) return value;
|
|
927
|
+
|
|
928
|
+
const properties = [...(value.properties ?? [])];
|
|
929
|
+
|
|
930
|
+
for (const branch of merged) {
|
|
931
|
+
for (const property of branch.properties ?? []) {
|
|
932
|
+
const existing = properties.findIndex(
|
|
933
|
+
(entry) => entry.name === property.name
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
// A later branch refines an earlier one rather than duplicating it, and
|
|
937
|
+
// `required` is a union: required anywhere is required.
|
|
938
|
+
if (existing === -1) properties.push(property);
|
|
939
|
+
else {
|
|
940
|
+
properties[existing] = {
|
|
941
|
+
...property,
|
|
942
|
+
required: properties[existing]!.required || property.required
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const result: DuxtOpenApiSchema = {
|
|
949
|
+
...value,
|
|
950
|
+
types: value.types?.length
|
|
951
|
+
? value.types
|
|
952
|
+
: merged.some((branch) => branch.types?.includes('object'))
|
|
953
|
+
? ['object']
|
|
954
|
+
: [],
|
|
955
|
+
description:
|
|
956
|
+
value.description ??
|
|
957
|
+
merged.find((branch) => branch.description)?.description,
|
|
958
|
+
properties: properties.length ? properties : undefined,
|
|
959
|
+
allOf: kept.length ? kept : undefined
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
return prune(result);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function blank(over: Partial<DuxtOpenApiSchema>): DuxtOpenApiSchema {
|
|
966
|
+
return { types: [], examples: [], constraints: {}, ...over };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** Drop the keys that are `undefined`, so a rendered prop carries no noise. */
|
|
970
|
+
function prune(value: DuxtOpenApiSchema): DuxtOpenApiSchema {
|
|
971
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
972
|
+
if (entry === undefined) delete value[key as keyof DuxtOpenApiSchema];
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return value;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Keys whose value is the DOCUMENT'S data rather than the model's structure.
|
|
980
|
+
*
|
|
981
|
+
* `compact` does not descend into these. An example whose value is genuinely
|
|
982
|
+
* `{}` or `[]` is a fact about the API — dropping it would make the page say
|
|
983
|
+
* the endpoint takes no body when the document says it takes an empty one.
|
|
984
|
+
*/
|
|
985
|
+
/**
|
|
986
|
+
* Keys whose EMPTY value is a statement rather than an absence.
|
|
987
|
+
*
|
|
988
|
+
* `security: []` is how an operation says it needs no authentication at all,
|
|
989
|
+
* and it has to survive: dropped, the operation reads as inheriting the
|
|
990
|
+
* document's requirement and the reference tells a reader to send credentials
|
|
991
|
+
* to an endpoint that wants none. The one key in the model where empty and
|
|
992
|
+
* absent are opposite answers.
|
|
993
|
+
*/
|
|
994
|
+
const KEEP = new Set(['security']);
|
|
995
|
+
|
|
996
|
+
const RAW = new Set([
|
|
997
|
+
'value',
|
|
998
|
+
'default',
|
|
999
|
+
'const',
|
|
1000
|
+
'enum',
|
|
1001
|
+
'example',
|
|
1002
|
+
'examples',
|
|
1003
|
+
'mapping'
|
|
1004
|
+
]);
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* The model, small enough to read in the page's own source.
|
|
1008
|
+
*
|
|
1009
|
+
* Three jobs in one pass. It drops the `undefined`s the model carries, which
|
|
1010
|
+
* YAML has no spelling for. It drops what is EMPTY — `examples: []`,
|
|
1011
|
+
* `constraints: {}`, `headers: []` — which on a document of any size is most of
|
|
1012
|
+
* the payload, and which lands in `rawbody` and so in `llms-full.txt` and the
|
|
1013
|
+
* copy-page button. And it rebuilds every object, which breaks the shared
|
|
1014
|
+
* references between `security[].scheme` and `securitySchemes` that YAML would
|
|
1015
|
+
* otherwise write out as anchors and aliases: valid, and unreadable.
|
|
1016
|
+
*
|
|
1017
|
+
* `false` and `0` SURVIVE. `additionalProperties: false` is a constraint, not
|
|
1018
|
+
* an absence, and a `minimum: 0` is the whole of what some parameters allow.
|
|
1019
|
+
*/
|
|
1020
|
+
export function compactOpenApi<T>(value: T, raw = false): T {
|
|
1021
|
+
if (raw || value === null || typeof value !== 'object') return value;
|
|
1022
|
+
|
|
1023
|
+
if (Array.isArray(value)) {
|
|
1024
|
+
return value.map((entry) => compactOpenApi(entry)) as T;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const built: Record<string, unknown> = {};
|
|
1028
|
+
|
|
1029
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1030
|
+
const cleaned = compactOpenApi(entry, RAW.has(key));
|
|
1031
|
+
|
|
1032
|
+
if (cleaned === undefined) continue;
|
|
1033
|
+
|
|
1034
|
+
if (!KEEP.has(key)) {
|
|
1035
|
+
if (cleaned === '') continue;
|
|
1036
|
+
if (Array.isArray(cleaned) && !cleaned.length) continue;
|
|
1037
|
+
if (
|
|
1038
|
+
!RAW.has(key) &&
|
|
1039
|
+
cleaned !== null &&
|
|
1040
|
+
typeof cleaned === 'object' &&
|
|
1041
|
+
!Array.isArray(cleaned) &&
|
|
1042
|
+
!Object.keys(cleaned).length
|
|
1043
|
+
) {
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
built[key] = cleaned;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
return built as T;
|
|
1052
|
+
}
|