@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,1028 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The arithmetic the reference pages do, out of the components that draw them.
|
|
3
|
+
*
|
|
4
|
+
* A schema's type as a word, a worked example derived from a schema, a server
|
|
5
|
+
* URL with its variables filled in, the request the try-it client is about to
|
|
6
|
+
* send and the `curl` that would send the same thing. Every one of those is a
|
|
7
|
+
* pure function of the model, and every one of them was the kind of thing that
|
|
8
|
+
* ends up written inline in a template where it can only be checked by
|
|
9
|
+
* clicking — the same argument `version-paths.ts` makes about prefixes, and the
|
|
10
|
+
* same answer.
|
|
11
|
+
*
|
|
12
|
+
* Type-only import, so nothing here drags the YAML parser into the browser
|
|
13
|
+
* bundle: `openapi-model.ts` exists for exactly this reason.
|
|
14
|
+
*/
|
|
15
|
+
import type {
|
|
16
|
+
DuxtOpenApiOperation,
|
|
17
|
+
DuxtOpenApiParameter,
|
|
18
|
+
DuxtOpenApiResponse,
|
|
19
|
+
DuxtOpenApiSchema,
|
|
20
|
+
DuxtOpenApiServer
|
|
21
|
+
} from '../../openapi-model';
|
|
22
|
+
|
|
23
|
+
/** How deep an example is derived before it is called recursive enough. */
|
|
24
|
+
const MAX_DEPTH = 6;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A schema's type, as the one word a table cell has room for.
|
|
28
|
+
*
|
|
29
|
+
* The `$ref`'s NAME is deliberately not part of it — a schema drawn as `Pet` is
|
|
30
|
+
* drawn as `Pet` beside its type, not instead of it, so a reader sees both the
|
|
31
|
+
* component and what it is made of.
|
|
32
|
+
*/
|
|
33
|
+
export function openApiTypeLabel(schema?: DuxtOpenApiSchema): string {
|
|
34
|
+
if (!schema) return 'any';
|
|
35
|
+
|
|
36
|
+
const declared = schema.types ?? [];
|
|
37
|
+
const nullable = declared.includes('null');
|
|
38
|
+
const types = declared.filter((type) => type !== 'null');
|
|
39
|
+
|
|
40
|
+
const written = types.map((type) =>
|
|
41
|
+
type === 'array' ? `array<${openApiTypeLabel(schema.items)}>` : type
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (!written.length) {
|
|
45
|
+
if (schema.properties?.length) written.push('object');
|
|
46
|
+
else if (schema.items)
|
|
47
|
+
written.push(`array<${openApiTypeLabel(schema.items)}>`);
|
|
48
|
+
else if (schema.oneOf ?? schema.anyOf ?? schema.allOf) return 'any';
|
|
49
|
+
else if (nullable) return 'null';
|
|
50
|
+
else return 'any';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return written.join(' | ') + (nullable ? ' | null' : '');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The constraints a schema states, as keys the interface translates. */
|
|
57
|
+
export function openApiConstraints(
|
|
58
|
+
schema?: DuxtOpenApiSchema
|
|
59
|
+
): { key: string; value: string }[] {
|
|
60
|
+
const constraints = schema?.constraints;
|
|
61
|
+
if (!constraints) return [];
|
|
62
|
+
|
|
63
|
+
const entries: { key: string; value: string }[] = [];
|
|
64
|
+
|
|
65
|
+
for (const [key, value] of Object.entries(constraints)) {
|
|
66
|
+
if (value === undefined) continue;
|
|
67
|
+
entries.push({ key, value: String(value) });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return entries;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Which way a body is travelling.
|
|
75
|
+
*
|
|
76
|
+
* `readOnly` and `writeOnly` are statements about direction, and until this
|
|
77
|
+
* existed the code had no word for it: one function guessed "request" and the
|
|
78
|
+
* rest ignored the question, so a response example dropped the `id` and
|
|
79
|
+
* `createdAt` a response is mostly about.
|
|
80
|
+
*/
|
|
81
|
+
export type DuxtOpenApiDirection = 'request' | 'response';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether a property cannot appear in a body travelling this way.
|
|
85
|
+
*
|
|
86
|
+
* The rule OpenAPI states, and the reason `required` alone is not enough to go
|
|
87
|
+
* on: a `readOnly` property belongs to responses, a `writeOnly` one to
|
|
88
|
+
* requests, and either may still be listed under `required` — where the
|
|
89
|
+
* requirement then applies only in the direction the property exists in.
|
|
90
|
+
*/
|
|
91
|
+
export function excluded(
|
|
92
|
+
schema: DuxtOpenApiSchema,
|
|
93
|
+
direction: DuxtOpenApiDirection
|
|
94
|
+
): boolean {
|
|
95
|
+
return direction === 'request'
|
|
96
|
+
? schema.readOnly === true
|
|
97
|
+
: schema.writeOnly === true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A worked example of what a schema describes.
|
|
102
|
+
*
|
|
103
|
+
* The document's own example WINS wherever it has one — `examples`, then
|
|
104
|
+
* `default`, then `const`, then the first `enum` member — and only what is left
|
|
105
|
+
* is derived from the type. A reference that invents a body where the author
|
|
106
|
+
* wrote one is a reference nobody trusts twice.
|
|
107
|
+
*
|
|
108
|
+
* A schema the parser cut as `circular` stops here rather than being expanded:
|
|
109
|
+
* `Pet.friends: Pet[]` would otherwise derive until the stack gave out.
|
|
110
|
+
*/
|
|
111
|
+
export function openApiExampleValue(
|
|
112
|
+
schema: DuxtOpenApiSchema | undefined,
|
|
113
|
+
direction: DuxtOpenApiDirection,
|
|
114
|
+
depth = 0
|
|
115
|
+
): unknown {
|
|
116
|
+
if (!schema || schema.circular || depth > MAX_DEPTH) return null;
|
|
117
|
+
|
|
118
|
+
if (schema.examples?.length) return schema.examples[0];
|
|
119
|
+
if (schema.default !== undefined) return schema.default;
|
|
120
|
+
if (schema.const !== undefined) return schema.const;
|
|
121
|
+
if (schema.enum?.length) return schema.enum[0];
|
|
122
|
+
|
|
123
|
+
const branch = schema.oneOf?.[0] ?? schema.anyOf?.[0];
|
|
124
|
+
if (branch) return openApiExampleValue(branch, direction, depth + 1);
|
|
125
|
+
|
|
126
|
+
const declared = (schema.types ?? []).filter((type) => type !== 'null');
|
|
127
|
+
const type =
|
|
128
|
+
declared[0] ??
|
|
129
|
+
(schema.properties?.length
|
|
130
|
+
? 'object'
|
|
131
|
+
: schema.items
|
|
132
|
+
? 'array'
|
|
133
|
+
: schema.allOf?.length
|
|
134
|
+
? 'object'
|
|
135
|
+
: undefined);
|
|
136
|
+
|
|
137
|
+
switch (type) {
|
|
138
|
+
case 'object': {
|
|
139
|
+
const built: Record<string, unknown> = {};
|
|
140
|
+
|
|
141
|
+
for (const property of schema.properties ?? []) {
|
|
142
|
+
// `readOnly` and `writeOnly` say WHICH WAY a field travels, and an
|
|
143
|
+
// example is a value rather than documentation — so a field that cannot
|
|
144
|
+
// appear in this direction is not in the example either. At every depth:
|
|
145
|
+
// this used to hold at the top level only, so a nested `id` the server
|
|
146
|
+
// assigns landed in the body the try-it client prefilled.
|
|
147
|
+
if (excluded(property.schema, direction)) continue;
|
|
148
|
+
|
|
149
|
+
built[property.name] = openApiExampleValue(
|
|
150
|
+
property.schema,
|
|
151
|
+
direction,
|
|
152
|
+
depth + 1
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const merged of schema.allOf ?? []) {
|
|
157
|
+
Object.assign(
|
|
158
|
+
built,
|
|
159
|
+
openApiExampleValue(merged, direction, depth + 1) as Record<
|
|
160
|
+
string,
|
|
161
|
+
unknown
|
|
162
|
+
>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return built;
|
|
167
|
+
}
|
|
168
|
+
case 'array':
|
|
169
|
+
return [openApiExampleValue(schema.items, direction, depth + 1)];
|
|
170
|
+
case 'boolean':
|
|
171
|
+
return true;
|
|
172
|
+
case 'integer':
|
|
173
|
+
case 'number':
|
|
174
|
+
return numberExample(schema);
|
|
175
|
+
case 'null':
|
|
176
|
+
return null;
|
|
177
|
+
case 'string':
|
|
178
|
+
return stringExample(schema);
|
|
179
|
+
default:
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A number the schema would actually accept.
|
|
186
|
+
*
|
|
187
|
+
* Zero is the obvious placeholder and is wrong more often than it looks: a
|
|
188
|
+
* `status` bounded to 400-599, or a `page` bounded to 1 upwards, rejects it —
|
|
189
|
+
* so the example a reader copies into the try-it client fails validation
|
|
190
|
+
* before it reaches anything interesting.
|
|
191
|
+
*/
|
|
192
|
+
function numberExample(schema: DuxtOpenApiSchema): number {
|
|
193
|
+
const { minimum, exclusiveMinimum, maximum, exclusiveMaximum } =
|
|
194
|
+
schema.constraints ?? {};
|
|
195
|
+
|
|
196
|
+
if (minimum !== undefined) return minimum;
|
|
197
|
+
if (exclusiveMinimum !== undefined) return exclusiveMinimum + 1;
|
|
198
|
+
if (maximum !== undefined && maximum < 0) return maximum;
|
|
199
|
+
if (exclusiveMaximum !== undefined && exclusiveMaximum <= 0) {
|
|
200
|
+
return exclusiveMaximum - 1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A placeholder that looks like what the format asks for. */
|
|
207
|
+
function stringExample(schema: DuxtOpenApiSchema): string {
|
|
208
|
+
switch (schema.format) {
|
|
209
|
+
case 'date':
|
|
210
|
+
return '2026-01-01';
|
|
211
|
+
case 'date-time':
|
|
212
|
+
return '2026-01-01T09:00:00Z';
|
|
213
|
+
case 'time':
|
|
214
|
+
return '09:00:00';
|
|
215
|
+
case 'uuid':
|
|
216
|
+
return '00000000-0000-4000-8000-000000000000';
|
|
217
|
+
case 'email':
|
|
218
|
+
return 'name@example.org';
|
|
219
|
+
case 'uri':
|
|
220
|
+
case 'url':
|
|
221
|
+
return 'https://example.org';
|
|
222
|
+
case 'hostname':
|
|
223
|
+
return 'example.org';
|
|
224
|
+
case 'ipv4':
|
|
225
|
+
return '192.0.2.1';
|
|
226
|
+
case 'ipv6':
|
|
227
|
+
return '2001:db8::1';
|
|
228
|
+
case 'byte':
|
|
229
|
+
return 'ZHV4dA==';
|
|
230
|
+
case 'binary':
|
|
231
|
+
return '<binary>';
|
|
232
|
+
case 'password':
|
|
233
|
+
return '••••••••';
|
|
234
|
+
default:
|
|
235
|
+
// NOT the `pattern`, tempting as it is: a regular expression is a
|
|
236
|
+
// description of the shape, not a value of it, and it would land in the
|
|
237
|
+
// request body the try-it client sends.
|
|
238
|
+
return 'string';
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A server's URL with its variables substituted.
|
|
244
|
+
*
|
|
245
|
+
* A template left unfilled is a URL the try-it client cannot send to and a
|
|
246
|
+
* reader cannot copy, so an unanswered variable falls back to the document's
|
|
247
|
+
* own default rather than staying `{stage}`.
|
|
248
|
+
*/
|
|
249
|
+
/** What kind of control a value of this schema should be typed into. */
|
|
250
|
+
export interface DuxtOpenApiField {
|
|
251
|
+
/** `select` when the document names the whole set of legal values. */
|
|
252
|
+
control: 'text' | 'number' | 'select';
|
|
253
|
+
/** The values a `select` offers, as the strings the request will carry. */
|
|
254
|
+
options?: string[];
|
|
255
|
+
/** Attributes the box takes verbatim — `type`, `step`, `min`, `max`. */
|
|
256
|
+
attrs: Record<string, string>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The box a parameter is typed into, chosen from what the document says it is.
|
|
261
|
+
*
|
|
262
|
+
* Every try-it client the layer has drawn until now offered one text box for
|
|
263
|
+
* every parameter, and a text box is the honest control only where the document
|
|
264
|
+
* says nothing: `limit: integer, min 1, max 100` is a number with a range, and
|
|
265
|
+
* an `enum` is a list — typing either by hand is a chance to send something the
|
|
266
|
+
* endpoint will reject, on a form whose whole purpose is to reach it.
|
|
267
|
+
*
|
|
268
|
+
* The VALUE STAYS A STRING whatever the control is, because that is what goes
|
|
269
|
+
* on the wire: a query, a path segment and a header are text, and parsing them
|
|
270
|
+
* into numbers here would only be undone one function later in
|
|
271
|
+
* `openApiQueryString`. What the type buys is the box, the stepper and the
|
|
272
|
+
* range — not a different representation.
|
|
273
|
+
*
|
|
274
|
+
* Pure, and here rather than in the component, for the reason everything else
|
|
275
|
+
* in this file is: a mapping written inline in a template can only be checked
|
|
276
|
+
* by clicking through every operation of every document.
|
|
277
|
+
*/
|
|
278
|
+
export function openApiField(schema?: DuxtOpenApiSchema): DuxtOpenApiField {
|
|
279
|
+
const types = schema?.types ?? [];
|
|
280
|
+
|
|
281
|
+
// The document naming the values wins over the type it names them as: an
|
|
282
|
+
// `enum` of integers is still a list to pick from, not a number to type.
|
|
283
|
+
if (schema?.enum?.length) {
|
|
284
|
+
return {
|
|
285
|
+
control: 'select',
|
|
286
|
+
options: schema.enum.map((value) =>
|
|
287
|
+
typeof value === 'string' ? value : String(value)
|
|
288
|
+
),
|
|
289
|
+
attrs: {}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (types.includes('boolean')) {
|
|
294
|
+
return { control: 'select', options: ['true', 'false'], attrs: {} };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (types.includes('integer') || types.includes('number')) {
|
|
298
|
+
const { minimum, maximum, multipleOf } = schema?.constraints ?? {};
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
control: 'number',
|
|
302
|
+
attrs: {
|
|
303
|
+
type: 'number',
|
|
304
|
+
// An integer steps by one unless the document says otherwise; a
|
|
305
|
+
// `number` takes anything, or the browser's own validation rejects a
|
|
306
|
+
// decimal in a field it decided was whole.
|
|
307
|
+
step: String(multipleOf ?? (types.includes('integer') ? 1 : 'any')),
|
|
308
|
+
...(minimum === undefined ? {} : { min: String(minimum) }),
|
|
309
|
+
...(maximum === undefined ? {} : { max: String(maximum) })
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return { control: 'text', attrs: {} };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** One property of a request body, as the box it is typed into. */
|
|
318
|
+
export interface DuxtOpenApiBodyField {
|
|
319
|
+
name: string;
|
|
320
|
+
required: boolean;
|
|
321
|
+
description?: string;
|
|
322
|
+
field: DuxtOpenApiField;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Why a body cannot be drawn as a form, when it cannot. */
|
|
326
|
+
export type DuxtOpenApiBodyReason =
|
|
327
|
+
| 'not-object'
|
|
328
|
+
| 'empty'
|
|
329
|
+
| 'nested'
|
|
330
|
+
| 'variants'
|
|
331
|
+
| 'unresolved'
|
|
332
|
+
| 'dynamic';
|
|
333
|
+
|
|
334
|
+
export type DuxtOpenApiBodyForm =
|
|
335
|
+
| { expressible: true; fields: DuxtOpenApiBodyField[] }
|
|
336
|
+
| { expressible: false; reason: DuxtOpenApiBodyReason };
|
|
337
|
+
|
|
338
|
+
/** The scalar types a single box can carry a value of. */
|
|
339
|
+
const SCALARS = new Set(['string', 'number', 'integer', 'boolean', 'null']);
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Can one box hold a value of this schema?
|
|
343
|
+
*
|
|
344
|
+
* An UNTYPED property counts, and that is the interesting half. A schema that
|
|
345
|
+
* names no type at all constrains nothing — OpenAPI's `any` — and refusing the
|
|
346
|
+
* whole form over one of them is how a body of five strings and one `any` ended
|
|
347
|
+
* up with no form at all. It gets a text box like any other unconstrained
|
|
348
|
+
* value, and the JSON view is one click away for the reader who needs to put an
|
|
349
|
+
* object in it. A schema that names no type but DOES carry properties, items or
|
|
350
|
+
* a combinator is a shape rather than a value, and still disqualifies.
|
|
351
|
+
*/
|
|
352
|
+
function scalar(schema: DuxtOpenApiSchema): boolean {
|
|
353
|
+
if (schema.enum?.length) return true;
|
|
354
|
+
|
|
355
|
+
const types = schema.types ?? [];
|
|
356
|
+
if (types.length) return types.every((type) => SCALARS.has(type));
|
|
357
|
+
|
|
358
|
+
return !(
|
|
359
|
+
schema.properties?.length ||
|
|
360
|
+
schema.items ||
|
|
361
|
+
schema.prefixItems?.length ||
|
|
362
|
+
schema.oneOf?.length ||
|
|
363
|
+
schema.anyOf?.length ||
|
|
364
|
+
schema.allOf?.length ||
|
|
365
|
+
schema.additionalProperties
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Can this request body be drawn as a form, and out of which boxes?
|
|
371
|
+
*
|
|
372
|
+
* THE ANSWER IS OFTEN NO, and saying so is the whole point. A JSON body is a
|
|
373
|
+
* tree: `oneOf` between two shapes, an array of objects, a `$ref` that loops or
|
|
374
|
+
* leaves the document, a map with no fixed keys. A form covers the flat object
|
|
375
|
+
* — which is most request bodies most of the time — and a form that silently
|
|
376
|
+
* could not express the rest would be worse than no form at all, because the
|
|
377
|
+
* reader would not learn that what they typed is not what would be sent. So
|
|
378
|
+
* every case it cannot draw comes back named, and the client shows the editor
|
|
379
|
+
* instead.
|
|
380
|
+
*
|
|
381
|
+
* `null` is allowed among a property's types rather than disqualifying it: a
|
|
382
|
+
* nullable string is a string with one more legal value, and 3.0's `nullable`
|
|
383
|
+
* reaches the model as exactly that — see `DuxtOpenApiSchema.types`.
|
|
384
|
+
*/
|
|
385
|
+
export function openApiBodyForm(
|
|
386
|
+
schema?: DuxtOpenApiSchema
|
|
387
|
+
): DuxtOpenApiBodyForm {
|
|
388
|
+
if (!schema) return { expressible: false, reason: 'not-object' };
|
|
389
|
+
|
|
390
|
+
// A reference the build could not follow has no properties to read, and one
|
|
391
|
+
// that loops would draw a form of itself.
|
|
392
|
+
//
|
|
393
|
+
// NOT `schema.ref`, which was the bug: the parser keeps `ref` on a reference
|
|
394
|
+
// it DID follow, beside the name and the properties it resolved — so testing
|
|
395
|
+
// it refused every body that is a named component, which is very nearly all
|
|
396
|
+
// of them. `circular` and `external` are the two that actually mean "there is
|
|
397
|
+
// nothing here to read".
|
|
398
|
+
if (schema.circular || schema.external) {
|
|
399
|
+
return { expressible: false, reason: 'unresolved' };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (schema.oneOf?.length || schema.anyOf?.length || schema.allOf?.length) {
|
|
403
|
+
return { expressible: false, reason: 'variants' };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (schema.types?.length && !schema.types.includes('object')) {
|
|
407
|
+
return { expressible: false, reason: 'not-object' };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// A map with no fixed keys: the form would offer the properties it happens to
|
|
411
|
+
// know and quietly forbid every other key the endpoint accepts. `false` is
|
|
412
|
+
// the opposite statement — no other keys exist — and a form says that fine.
|
|
413
|
+
if (schema.additionalProperties) {
|
|
414
|
+
return { expressible: false, reason: 'dynamic' };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* A `readOnly` property is not part of a REQUEST, and this form is only ever
|
|
419
|
+
* a request.
|
|
420
|
+
*
|
|
421
|
+
* The document says who fills a field in, and the form used to ignore it: an
|
|
422
|
+
* `id` the server assigns got a box, and — where the schema also listed it
|
|
423
|
+
* under `required`, which is the ordinary way to describe such a field — a
|
|
424
|
+
* red marker demanding the reader invent one. Dropped before the checks
|
|
425
|
+
* below, not after: a read-only property that is itself an object must not
|
|
426
|
+
* cost the whole form its `nested` verdict for a field the request never
|
|
427
|
+
* carries.
|
|
428
|
+
*/
|
|
429
|
+
const properties = (schema.properties ?? []).filter(
|
|
430
|
+
(property) => !excluded(property.schema, 'request')
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
if (!properties.length) return { expressible: false, reason: 'empty' };
|
|
434
|
+
|
|
435
|
+
if (properties.some((property) => !scalar(property.schema))) {
|
|
436
|
+
return { expressible: false, reason: 'nested' };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
expressible: true,
|
|
441
|
+
fields: properties.map((property) => ({
|
|
442
|
+
name: property.name,
|
|
443
|
+
required: property.required,
|
|
444
|
+
description: property.schema.description,
|
|
445
|
+
field: openApiField(property.schema)
|
|
446
|
+
}))
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* The form's boxes, filled from a JSON body.
|
|
452
|
+
*
|
|
453
|
+
* Read from the TEXT rather than kept beside it, so the two views are one
|
|
454
|
+
* value: whatever the reader last typed in the editor is what the form opens
|
|
455
|
+
* on, and a body the form cannot account for is not silently dropped — see
|
|
456
|
+
* `openApiBodyJson`, which writes back only what the form owns.
|
|
457
|
+
*/
|
|
458
|
+
export function openApiBodyValues(
|
|
459
|
+
fields: DuxtOpenApiBodyField[],
|
|
460
|
+
json: string
|
|
461
|
+
): Record<string, string> {
|
|
462
|
+
let parsed: unknown;
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
parsed = JSON.parse(json);
|
|
466
|
+
} catch {
|
|
467
|
+
parsed = undefined;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const source =
|
|
471
|
+
parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
472
|
+
? (parsed as Record<string, unknown>)
|
|
473
|
+
: {};
|
|
474
|
+
|
|
475
|
+
return Object.fromEntries(
|
|
476
|
+
fields.map((entry) => {
|
|
477
|
+
const value = source[entry.name];
|
|
478
|
+
|
|
479
|
+
return [
|
|
480
|
+
entry.name,
|
|
481
|
+
value === undefined || value === null
|
|
482
|
+
? ''
|
|
483
|
+
: typeof value === 'string'
|
|
484
|
+
? value
|
|
485
|
+
: String(value)
|
|
486
|
+
];
|
|
487
|
+
})
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* The form's boxes, back as a JSON body.
|
|
493
|
+
*
|
|
494
|
+
* An empty OPTIONAL box is left out rather than sent as `""`: a form has no way
|
|
495
|
+
* to say "absent" other than by being empty, and an endpoint told a field is
|
|
496
|
+
* the empty string was told something. A required one stays, because dropping
|
|
497
|
+
* it would send a body the document says is invalid without saying so.
|
|
498
|
+
*
|
|
499
|
+
* The TYPE decides the JSON type, which is the half a textarea cannot help
|
|
500
|
+
* with: `20` typed into a number box is a number, `true` in a boolean box is a
|
|
501
|
+
* boolean, and both were strings a moment ago.
|
|
502
|
+
*/
|
|
503
|
+
export function openApiBodyJson(
|
|
504
|
+
fields: DuxtOpenApiBodyField[],
|
|
505
|
+
values: Record<string, string>,
|
|
506
|
+
json = ''
|
|
507
|
+
): string {
|
|
508
|
+
// WHAT THE FORM DOES NOT OWN SURVIVES. A body may carry keys the document
|
|
509
|
+
// never described — a reader typed one in the JSON view, or the server takes
|
|
510
|
+
// more than it says — and rebuilding the object from the boxes alone deleted
|
|
511
|
+
// every one of them the moment a box was touched. The form edits the body it
|
|
512
|
+
// was given; it does not replace it.
|
|
513
|
+
let body: Record<string, unknown> = {};
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
const parsed: unknown = JSON.parse(json);
|
|
517
|
+
|
|
518
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
519
|
+
body = { ...(parsed as Record<string, unknown>) };
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
// Not an object yet; the boxes are the whole body.
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
for (const entry of fields) {
|
|
526
|
+
const raw = values[entry.name] ?? '';
|
|
527
|
+
|
|
528
|
+
if (raw === '' && !entry.required) {
|
|
529
|
+
delete body[entry.name];
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
body[entry.name] = openApiBodyValue(entry, raw);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return openApiJson(body);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** One box's text, as the JSON value its schema calls for. */
|
|
540
|
+
function openApiBodyValue(entry: DuxtOpenApiBodyField, raw: string): unknown {
|
|
541
|
+
if (entry.field.control === 'number') {
|
|
542
|
+
const value = Number(raw);
|
|
543
|
+
|
|
544
|
+
// Not a number after all — the box is empty, or holds something the
|
|
545
|
+
// browser let through. Sent as written rather than as `NaN`, which is not
|
|
546
|
+
// JSON at all and would be serialised as `null`.
|
|
547
|
+
return raw !== '' && Number.isFinite(value) ? value : raw;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (entry.field.options?.length === 2 && entry.field.options[0] === 'true') {
|
|
551
|
+
return raw === 'true';
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return raw;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** One key a body may carry, as the editor offers and checks it. */
|
|
558
|
+
export interface DuxtOpenApiKey {
|
|
559
|
+
name: string;
|
|
560
|
+
required: boolean;
|
|
561
|
+
/** The one word `openApiTypeLabel` would print, for the completion list. */
|
|
562
|
+
type: string;
|
|
563
|
+
description?: string;
|
|
564
|
+
/** The values the document allows, where it named them. */
|
|
565
|
+
enum?: string[];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* The keys a request body's TOP LEVEL may carry, for the editor.
|
|
570
|
+
*
|
|
571
|
+
* Narrower than `openApiBodyForm` on purpose, and the two answer different
|
|
572
|
+
* questions. The form asks "can I draw every one of these as a box?" and gives
|
|
573
|
+
* up on the first nested object; completion has no such requirement — offering
|
|
574
|
+
* `address` and letting the reader type the object themselves is strictly
|
|
575
|
+
* better than offering nothing, and marking a misspelt key is useful whatever
|
|
576
|
+
* its value looks like.
|
|
577
|
+
*
|
|
578
|
+
* Empty where the top level is not a fixed set of keys at all: an array, a
|
|
579
|
+
* `oneOf` between two shapes, a reference that could not be followed, or a map
|
|
580
|
+
* with no fixed keys. Empty means the editor still lints the JSON itself and
|
|
581
|
+
* simply has nothing to add — never that it starts inventing.
|
|
582
|
+
*/
|
|
583
|
+
export function openApiBodyKeys(schema?: DuxtOpenApiSchema): DuxtOpenApiKey[] {
|
|
584
|
+
// `ref` is deliberately not among these — see `openApiBodyForm`.
|
|
585
|
+
if (!schema || schema.circular || schema.external) return [];
|
|
586
|
+
if (schema.oneOf?.length || schema.anyOf?.length || schema.allOf?.length) {
|
|
587
|
+
return [];
|
|
588
|
+
}
|
|
589
|
+
if (schema.types?.length && !schema.types.includes('object')) return [];
|
|
590
|
+
|
|
591
|
+
return (schema.properties ?? []).map((property) => ({
|
|
592
|
+
name: property.name,
|
|
593
|
+
/**
|
|
594
|
+
* `readOnly` UNDOES `required` HERE, because this list describes a request.
|
|
595
|
+
*
|
|
596
|
+
* OpenAPI is explicit: where a property is `readOnly` and also listed under
|
|
597
|
+
* `required`, the requirement applies to the response only. A request must
|
|
598
|
+
* not carry it at all. Without this the editor demanded a field it had
|
|
599
|
+
* itself, correctly, left out of the body it prefilled — the linter and the
|
|
600
|
+
* default contradicting each other inside one component.
|
|
601
|
+
*
|
|
602
|
+
* It stays in the list rather than being dropped: the document DOES
|
|
603
|
+
* describe it, so a reader who types it deserves completion and not the
|
|
604
|
+
* "not described by this document" warning that removing it would produce.
|
|
605
|
+
*/
|
|
606
|
+
required: property.required && !excluded(property.schema, 'request'),
|
|
607
|
+
type: openApiTypeLabel(property.schema),
|
|
608
|
+
description: property.schema.description,
|
|
609
|
+
enum: property.schema.enum?.length
|
|
610
|
+
? property.schema.enum.map((value) =>
|
|
611
|
+
typeof value === 'string' ? value : String(value)
|
|
612
|
+
)
|
|
613
|
+
: undefined
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** One thing wrong with a body, at the character where it is wrong. */
|
|
618
|
+
export interface DuxtOpenApiBodyProblem {
|
|
619
|
+
from: number;
|
|
620
|
+
to: number;
|
|
621
|
+
severity: 'error' | 'warning';
|
|
622
|
+
message: string;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* What the document says is wrong with this body — MISSING and UNKNOWN keys.
|
|
627
|
+
*
|
|
628
|
+
* Only the two the top level can be sure about, and each is a different kind
|
|
629
|
+
* of claim: a required key that is not there is an `error`, because the
|
|
630
|
+
* document says the request is invalid without it; a key the document does not
|
|
631
|
+
* list is a `warning`, because a server is free to accept more than it
|
|
632
|
+
* describes and this client exists partly to find out that it does.
|
|
633
|
+
*
|
|
634
|
+
* Positions are found by SEARCHING THE TEXT rather than by a parser with
|
|
635
|
+
* offsets: `JSON.parse` throws away where anything was, and pulling in a
|
|
636
|
+
* position-preserving parser for two rules would be a dependency for a
|
|
637
|
+
* squiggle. The search is for the quoted key at the top nesting level, which is
|
|
638
|
+
* the only place these rules apply anyway — a nested `"name"` is somebody
|
|
639
|
+
* else's key and is skipped by the depth count.
|
|
640
|
+
*/
|
|
641
|
+
export function openApiBodyProblems(
|
|
642
|
+
json: string,
|
|
643
|
+
keys: DuxtOpenApiKey[]
|
|
644
|
+
): DuxtOpenApiBodyProblem[] {
|
|
645
|
+
if (!keys.length || !json.trim()) return [];
|
|
646
|
+
|
|
647
|
+
let parsed: unknown;
|
|
648
|
+
try {
|
|
649
|
+
parsed = JSON.parse(json);
|
|
650
|
+
} catch {
|
|
651
|
+
// Not JSON yet. The editor's own linter is already saying so, and a second
|
|
652
|
+
// opinion on a document that does not parse is noise.
|
|
653
|
+
return [];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return [];
|
|
657
|
+
|
|
658
|
+
const body = parsed as Record<string, unknown>;
|
|
659
|
+
const known = new Set(keys.map((key) => key.name));
|
|
660
|
+
const problems: DuxtOpenApiBodyProblem[] = [];
|
|
661
|
+
|
|
662
|
+
for (const name of Object.keys(body)) {
|
|
663
|
+
if (known.has(name)) continue;
|
|
664
|
+
|
|
665
|
+
const at = topLevelKey(json, name);
|
|
666
|
+
if (!at) continue;
|
|
667
|
+
|
|
668
|
+
problems.push({
|
|
669
|
+
...at,
|
|
670
|
+
severity: 'warning',
|
|
671
|
+
message: `"${name}" is not described by this document.`
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const missing = keys
|
|
676
|
+
.filter((key) => key.required && !(key.name in body))
|
|
677
|
+
.map((key) => key.name);
|
|
678
|
+
|
|
679
|
+
if (missing.length) {
|
|
680
|
+
const open = json.indexOf('{');
|
|
681
|
+
|
|
682
|
+
problems.push({
|
|
683
|
+
from: Math.max(open, 0),
|
|
684
|
+
to: Math.max(open, 0) + 1,
|
|
685
|
+
severity: 'error',
|
|
686
|
+
message: `Required by this document: ${missing.join(', ')}.`
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return problems;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Where a key sits, counted at the top level of the object only. */
|
|
694
|
+
function topLevelKey(
|
|
695
|
+
json: string,
|
|
696
|
+
name: string
|
|
697
|
+
): { from: number; to: number } | undefined {
|
|
698
|
+
const needle = JSON.stringify(name);
|
|
699
|
+
|
|
700
|
+
let depth = 0;
|
|
701
|
+
let inString = false;
|
|
702
|
+
let escaped = false;
|
|
703
|
+
|
|
704
|
+
for (let index = 0; index < json.length; index += 1) {
|
|
705
|
+
const character = json[index]!;
|
|
706
|
+
|
|
707
|
+
if (escaped) {
|
|
708
|
+
escaped = false;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (inString) {
|
|
713
|
+
if (character === '\\') escaped = true;
|
|
714
|
+
else if (character === '"') inString = false;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (character === '"') {
|
|
719
|
+
// A string opening exactly here, one level in, and spelled like the key.
|
|
720
|
+
if (depth === 1 && json.startsWith(needle, index)) {
|
|
721
|
+
return { from: index, to: index + needle.length };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
inString = true;
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (character === '{' || character === '[') depth += 1;
|
|
729
|
+
else if (character === '}' || character === ']') depth -= 1;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export function openApiServerUrl(
|
|
736
|
+
server?: DuxtOpenApiServer,
|
|
737
|
+
values: Record<string, string> = {}
|
|
738
|
+
): string {
|
|
739
|
+
if (!server) return '';
|
|
740
|
+
|
|
741
|
+
return server.url.replaceAll(/\{([^{}]+)\}/g, (whole, name: string) => {
|
|
742
|
+
const declared = server.variables?.find((entry) => entry.name === name);
|
|
743
|
+
|
|
744
|
+
return values[name] ?? declared?.default ?? whole;
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* The one key a parameter's answer is filed under.
|
|
750
|
+
*
|
|
751
|
+
* OpenAPI identifies a parameter by the PAIR `(name, in)`, not by its name:
|
|
752
|
+
* `id` in the path and `id` in the query are two parameters, and a document is
|
|
753
|
+
* allowed to declare both. Keying the try-it client's boxes on the name alone
|
|
754
|
+
* therefore sent one answer to both places, substituted a query parameter into
|
|
755
|
+
* a path template, and emitted duplicate `v-for` keys — so the state, the
|
|
756
|
+
* request builder and the form's own `label`/`id` pairs all key on this.
|
|
757
|
+
*
|
|
758
|
+
* The join is unambiguous because `in` is a closed set of four words and none
|
|
759
|
+
* of them is a prefix of another: no `(name, in)` pair can spell the string
|
|
760
|
+
* another pair spells.
|
|
761
|
+
*/
|
|
762
|
+
export function openApiParameterKey(
|
|
763
|
+
parameter: Pick<DuxtOpenApiParameter, 'name' | 'in'>
|
|
764
|
+
): string {
|
|
765
|
+
return `${parameter.in}-${parameter.name}`;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* The path with its path parameters substituted, unanswered ones left visible.
|
|
770
|
+
*
|
|
771
|
+
* A `{…}` template in a path can only ever be the PATH parameter of that name —
|
|
772
|
+
* so the lookup names that location rather than trusting the name alone, and a
|
|
773
|
+
* query parameter called `id` can no longer substitute itself into `/pets/{id}`
|
|
774
|
+
* and send the request somewhere else.
|
|
775
|
+
*/
|
|
776
|
+
export function openApiFillPath(
|
|
777
|
+
path: string,
|
|
778
|
+
values: Record<string, string> = {}
|
|
779
|
+
): string {
|
|
780
|
+
return path.replaceAll(/\{([^{}]+)\}/g, (whole, name: string) => {
|
|
781
|
+
const value = values[openApiParameterKey({ name, in: 'path' })];
|
|
782
|
+
|
|
783
|
+
return value ? encodeURIComponent(value) : whole;
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* A query string from the parameters the reader filled in.
|
|
789
|
+
*
|
|
790
|
+
* An empty value is left OUT rather than sent as `?limit=`: an empty query
|
|
791
|
+
* parameter is a different request from an absent one, and the one the reader
|
|
792
|
+
* meant is the absent one. `allowEmptyValue` is how a document says otherwise.
|
|
793
|
+
*/
|
|
794
|
+
export function openApiQueryString(
|
|
795
|
+
parameters: DuxtOpenApiParameter[],
|
|
796
|
+
values: Record<string, string>
|
|
797
|
+
): string {
|
|
798
|
+
const query = new URLSearchParams();
|
|
799
|
+
|
|
800
|
+
for (const parameter of parameters) {
|
|
801
|
+
if (parameter.in !== 'query') continue;
|
|
802
|
+
|
|
803
|
+
const value = values[openApiParameterKey(parameter)] ?? '';
|
|
804
|
+
if (!value && !parameter.allowEmptyValue) continue;
|
|
805
|
+
|
|
806
|
+
query.append(parameter.name, value);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const written = query.toString();
|
|
810
|
+
|
|
811
|
+
return written ? `?${written}` : '';
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** The request an operation, a server and the reader's answers add up to. */
|
|
815
|
+
export function openApiRequest(
|
|
816
|
+
operation: Pick<
|
|
817
|
+
DuxtOpenApiOperation,
|
|
818
|
+
'method' | 'path' | 'parameters' | 'responses'
|
|
819
|
+
>,
|
|
820
|
+
server: string,
|
|
821
|
+
values: Record<string, string>,
|
|
822
|
+
headers: Record<string, string> = {},
|
|
823
|
+
body?: string
|
|
824
|
+
): DuxtOpenApiRequest {
|
|
825
|
+
const parameters = operation.parameters ?? [];
|
|
826
|
+
|
|
827
|
+
const path = openApiFillPath(operation.path, values);
|
|
828
|
+
const query = openApiQueryString(parameters, values);
|
|
829
|
+
|
|
830
|
+
const sent: Record<string, string> = { ...headers };
|
|
831
|
+
|
|
832
|
+
for (const parameter of parameters) {
|
|
833
|
+
if (parameter.in !== 'header') continue;
|
|
834
|
+
|
|
835
|
+
const value = values[openApiParameterKey(parameter)];
|
|
836
|
+
if (value) sent[parameter.name] = value;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// A COOKIE PARAMETER IS DELIBERATELY NOT SENT. `Cookie` is a forbidden header
|
|
840
|
+
// name for `fetch`: a browser drops it with no error, no rejection and
|
|
841
|
+
// nothing in the console. Building it anyway produced a request that quietly
|
|
842
|
+
// lacked the credential while the `curl` sample rendered from this same
|
|
843
|
+
// object showed it present — the one place a reader would look to confirm.
|
|
844
|
+
// The client says the cookie cannot be set from a page instead, so what is
|
|
845
|
+
// sent and what is shown are the same request again.
|
|
846
|
+
|
|
847
|
+
return {
|
|
848
|
+
method: operation.method.toUpperCase(),
|
|
849
|
+
// Trailing slash on the server against leading slash on the path: one of
|
|
850
|
+
// the two has to go, or every request is sent to a doubled separator.
|
|
851
|
+
url: `${server.replace(/\/+$/, '')}${path}${query}`,
|
|
852
|
+
headers: sent,
|
|
853
|
+
body,
|
|
854
|
+
responseType: openApiResponseType(operation.responses)
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* What a successful response returns, by the name the document gave it.
|
|
860
|
+
*
|
|
861
|
+
* THE FIRST 2xx WITH A NAMED SCHEMA, and a name is the whole requirement: a
|
|
862
|
+
* TypeScript sample can write `$fetch<Widget>` only where `Widget` exists to be
|
|
863
|
+
* imported, and an inline schema has nothing to call itself. `204 No Content`
|
|
864
|
+
* and a response described inline therefore yield nothing, and the sample drops
|
|
865
|
+
* its type parameter rather than inventing a shape.
|
|
866
|
+
*
|
|
867
|
+
* An array is named by its items, because that is how the type reads: a list
|
|
868
|
+
* endpoint returns `Widget[]` and no document declares a component for the
|
|
869
|
+
* bracket.
|
|
870
|
+
*/
|
|
871
|
+
export function openApiResponseType(
|
|
872
|
+
responses?: DuxtOpenApiResponse[]
|
|
873
|
+
): string | undefined {
|
|
874
|
+
for (const response of responses ?? []) {
|
|
875
|
+
if (!response.status.startsWith('2')) continue;
|
|
876
|
+
|
|
877
|
+
const schema = response.content?.[0]?.schema;
|
|
878
|
+
if (!schema) continue;
|
|
879
|
+
|
|
880
|
+
if (schema.name) return schema.name;
|
|
881
|
+
if (schema.items?.name) return `${schema.items.name}[]`;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return undefined;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/** Which family a response status belongs to, for the colour it is drawn in. */
|
|
888
|
+
export function openApiStatusKind(
|
|
889
|
+
status: string
|
|
890
|
+
): 'info' | 'success' | 'redirect' | 'client' | 'server' | 'default' {
|
|
891
|
+
const first = status.trim()[0];
|
|
892
|
+
|
|
893
|
+
switch (first) {
|
|
894
|
+
case '1':
|
|
895
|
+
return 'info';
|
|
896
|
+
case '2':
|
|
897
|
+
return 'success';
|
|
898
|
+
case '3':
|
|
899
|
+
return 'redirect';
|
|
900
|
+
case '4':
|
|
901
|
+
return 'client';
|
|
902
|
+
case '5':
|
|
903
|
+
return 'server';
|
|
904
|
+
default:
|
|
905
|
+
return 'default';
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* The colour a status is drawn in, in the badge's own construction — a tenth of
|
|
911
|
+
* the colour as surface, a fifth as border.
|
|
912
|
+
*
|
|
913
|
+
* Here rather than beside one of its callers because TWO draw a status: the
|
|
914
|
+
* responses of an operation, and the responses a callback expects back. They
|
|
915
|
+
* were two copies of the same map, which is two chances for a 4xx to be amber
|
|
916
|
+
* in one place and grey in the other.
|
|
917
|
+
*/
|
|
918
|
+
export function openApiStatusTone(status: string): string {
|
|
919
|
+
const tones: Record<string, string> = {
|
|
920
|
+
info: 'border-border bg-muted text-muted-foreground',
|
|
921
|
+
success: 'border-success/20 bg-success/10 text-success',
|
|
922
|
+
redirect: 'border-sky-500/20 bg-sky-500/10 text-sky-700 dark:text-sky-400',
|
|
923
|
+
client:
|
|
924
|
+
'border-amber-500/20 bg-amber-500/10 text-amber-700 dark:text-amber-400',
|
|
925
|
+
server: 'border-destructive/20 bg-destructive/10 text-destructive',
|
|
926
|
+
default: 'border-border bg-muted text-muted-foreground'
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
return tones[openApiStatusKind(status)] ?? tones.default!;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** A value as the JSON a code sample shows. */
|
|
933
|
+
export function openApiJson(value: unknown): string {
|
|
934
|
+
if (typeof value === 'string') return value;
|
|
935
|
+
|
|
936
|
+
try {
|
|
937
|
+
return JSON.stringify(value, null, 2) ?? '';
|
|
938
|
+
} catch {
|
|
939
|
+
// A document may carry anything, `BigInt` and a cyclic example included.
|
|
940
|
+
return String(value);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* The props of the `open-api-operation` node inside a generated page's body.
|
|
946
|
+
*
|
|
947
|
+
* The landing page shows the try-it client on its own — no frame, no second
|
|
948
|
+
* copy of the application — and the client needs exactly what the operation
|
|
949
|
+
* page hands it. That data is not in the page's frontmatter: `sections-openapi`
|
|
950
|
+
* writes it as the props of one MDC component in the body, so this walks the
|
|
951
|
+
* parsed body and lifts them back out.
|
|
952
|
+
*
|
|
953
|
+
* A PURE FUNCTION OVER THE TREE, and separate from the component, because it is
|
|
954
|
+
* the one part of this that couples to a shape somebody else produces —
|
|
955
|
+
* Content's minimal AST, where an element is `[tag, props, ...children]` — and
|
|
956
|
+
* a coupling that can be tested is a coupling that fails loudly.
|
|
957
|
+
*
|
|
958
|
+
* Returns undefined rather than throwing: a page that carries no operation is
|
|
959
|
+
* a band that draws nothing, not a landing page that 500s.
|
|
960
|
+
*/
|
|
961
|
+
export function openApiOperationProps(
|
|
962
|
+
body: unknown
|
|
963
|
+
): Record<string, unknown> | undefined {
|
|
964
|
+
const value = (body as { value?: unknown })?.value ?? body;
|
|
965
|
+
if (!Array.isArray(value)) return undefined;
|
|
966
|
+
|
|
967
|
+
for (const node of value) {
|
|
968
|
+
if (!Array.isArray(node)) continue;
|
|
969
|
+
|
|
970
|
+
const [tag, props, ...children] = node as [unknown, unknown, ...unknown[]];
|
|
971
|
+
|
|
972
|
+
if (
|
|
973
|
+
tag === 'open-api-operation' &&
|
|
974
|
+
props &&
|
|
975
|
+
typeof props === 'object' &&
|
|
976
|
+
!Array.isArray(props)
|
|
977
|
+
) {
|
|
978
|
+
return mdcProps(props as Record<string, unknown>);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Depth first: the node sits under the body's root element, and a page may
|
|
982
|
+
// wrap it in whatever the generator put around it.
|
|
983
|
+
const nested = openApiOperationProps(children);
|
|
984
|
+
if (nested) return nested;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return undefined;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* MDC's stored props, as a component would receive them.
|
|
992
|
+
*
|
|
993
|
+
* A component's frontmatter is serialised into the tree the way it was written
|
|
994
|
+
* in the Markdown — a BOUND prop keeps its colon and its value stays a JSON
|
|
995
|
+
* string, because that is what `:operation="…"` is in an MDC block. The
|
|
996
|
+
* renderer parses it on the way into the component; anything reading the tree
|
|
997
|
+
* directly gets `{ ':operation': '{"kind":"operation",…}' }` and, handed
|
|
998
|
+
* straight on, a component whose every prop is undefined.
|
|
999
|
+
*
|
|
1000
|
+
* A value that will not parse is kept as the string it is: a bound prop may
|
|
1001
|
+
* carry an expression rather than data, and dropping it silently would be the
|
|
1002
|
+
* same failure one level along.
|
|
1003
|
+
*/
|
|
1004
|
+
function mdcProps(props: Record<string, unknown>): Record<string, unknown> {
|
|
1005
|
+
const out: Record<string, unknown> = {};
|
|
1006
|
+
|
|
1007
|
+
for (const [key, value] of Object.entries(props)) {
|
|
1008
|
+
if (!key.startsWith(':')) {
|
|
1009
|
+
out[key] = value;
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const name = key.slice(1);
|
|
1014
|
+
|
|
1015
|
+
if (typeof value !== 'string') {
|
|
1016
|
+
out[name] = value;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
try {
|
|
1021
|
+
out[name] = JSON.parse(value);
|
|
1022
|
+
} catch {
|
|
1023
|
+
out[name] = value;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return out;
|
|
1028
|
+
}
|