@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.
Files changed (401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/app/app.config.ts +13 -0
  4. package/app/app.vue +122 -0
  5. package/app/assets/css/duxt.css +555 -0
  6. package/app/assets/css/typeset.css +498 -0
  7. package/app/components/DuxtBrand.vue +53 -0
  8. package/app/components/DuxtBreadcrumb.vue +37 -0
  9. package/app/components/DuxtCodeBlock.vue +143 -0
  10. package/app/components/DuxtCopyPage.vue +117 -0
  11. package/app/components/DuxtFooter.vue +86 -0
  12. package/app/components/DuxtHeader.vue +496 -0
  13. package/app/components/DuxtJsonEditor.vue +359 -0
  14. package/app/components/DuxtLandingProse.vue +90 -0
  15. package/app/components/DuxtLandingShowcase.vue +295 -0
  16. package/app/components/DuxtLiveWindow.vue +343 -0
  17. package/app/components/DuxtLocale.vue +86 -0
  18. package/app/components/DuxtMetaList.vue +76 -0
  19. package/app/components/DuxtNavigation.vue +94 -0
  20. package/app/components/DuxtNavigationLink.vue +41 -0
  21. package/app/components/DuxtOpenApiCallbacks.vue +92 -0
  22. package/app/components/DuxtOpenApiClient.vue +1339 -0
  23. package/app/components/DuxtOpenApiMedia.vue +108 -0
  24. package/app/components/DuxtOpenApiMethod.vue +59 -0
  25. package/app/components/DuxtOpenApiParameters.vue +114 -0
  26. package/app/components/DuxtOpenApiResponses.vue +109 -0
  27. package/app/components/DuxtOpenApiSchema.vue +365 -0
  28. package/app/components/DuxtOpenApiSecurity.vue +115 -0
  29. package/app/components/DuxtPageFeedback.vue +56 -0
  30. package/app/components/DuxtPageInfo.vue +194 -0
  31. package/app/components/DuxtPageNav.vue +93 -0
  32. package/app/components/DuxtPreviewSkeleton.vue +262 -0
  33. package/app/components/DuxtProgress.vue +45 -0
  34. package/app/components/DuxtSearch.vue +271 -0
  35. package/app/components/DuxtSections.vue +65 -0
  36. package/app/components/DuxtShortcuts.vue +46 -0
  37. package/app/components/DuxtSkipLink.vue +34 -0
  38. package/app/components/DuxtToc.vue +86 -0
  39. package/app/components/DuxtTranslationBanner.vue +64 -0
  40. package/app/components/DuxtVersion.vue +168 -0
  41. package/app/components/DuxtVersionBanner.vue +109 -0
  42. package/app/components/OgImage/Duxt.satori.vue +82 -0
  43. package/app/components/content/Accordion.vue +37 -0
  44. package/app/components/content/AccordionItem.vue +46 -0
  45. package/app/components/content/Callout.vue +58 -0
  46. package/app/components/content/ChangelogGroup.vue +91 -0
  47. package/app/components/content/ChangelogReleases.vue +520 -0
  48. package/app/components/content/CodeGroup.vue +183 -0
  49. package/app/components/content/DevtoolsPanel.vue +107 -0
  50. package/app/components/content/Field.vue +50 -0
  51. package/app/components/content/FieldGroup.vue +8 -0
  52. package/app/components/content/FileTree.vue +92 -0
  53. package/app/components/content/Mermaid.vue +70 -0
  54. package/app/components/content/OpenApiOperation.vue +185 -0
  55. package/app/components/content/OpenApiOperations.vue +72 -0
  56. package/app/components/content/OpenApiOverview.vue +302 -0
  57. package/app/components/content/PackageManagers.vue +187 -0
  58. package/app/components/content/PageCards.vue +48 -0
  59. package/app/components/content/Partial.vue +65 -0
  60. package/app/components/content/Preview.vue +173 -0
  61. package/app/components/content/ProseA.vue +55 -0
  62. package/app/components/content/ProseH2.vue +53 -0
  63. package/app/components/content/ProseH3.vue +53 -0
  64. package/app/components/content/ProseH4.vue +53 -0
  65. package/app/components/content/ProseImg.vue +87 -0
  66. package/app/components/content/ProsePre.vue +58 -0
  67. package/app/components/content/ProseTable.vue +11 -0
  68. package/app/components/content/Since.vue +21 -0
  69. package/app/components/content/Steps.vue +20 -0
  70. package/app/components/content/Tab.vue +23 -0
  71. package/app/components/content/TabGroup.vue +58 -0
  72. package/app/components/ui/alert/Alert.vue +21 -0
  73. package/app/components/ui/alert/AlertDescription.vue +22 -0
  74. package/app/components/ui/alert/AlertTitle.vue +22 -0
  75. package/app/components/ui/alert/index.ts +24 -0
  76. package/app/components/ui/badge/Badge.vue +28 -0
  77. package/app/components/ui/badge/index.ts +52 -0
  78. package/app/components/ui/breadcrumb/Breadcrumb.vue +13 -0
  79. package/app/components/ui/breadcrumb/BreadcrumbEllipsis.vue +22 -0
  80. package/app/components/ui/breadcrumb/BreadcrumbItem.vue +17 -0
  81. package/app/components/ui/breadcrumb/BreadcrumbLink.vue +24 -0
  82. package/app/components/ui/breadcrumb/BreadcrumbList.vue +22 -0
  83. package/app/components/ui/breadcrumb/BreadcrumbPage.vue +20 -0
  84. package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +21 -0
  85. package/app/components/ui/breadcrumb/index.ts +7 -0
  86. package/app/components/ui/button/Button.vue +31 -0
  87. package/app/components/ui/button/index.ts +39 -0
  88. package/app/components/ui/card/Card.vue +22 -0
  89. package/app/components/ui/card/CardAction.vue +22 -0
  90. package/app/components/ui/card/CardContent.vue +14 -0
  91. package/app/components/ui/card/CardDescription.vue +17 -0
  92. package/app/components/ui/card/CardFooter.vue +17 -0
  93. package/app/components/ui/card/CardHeader.vue +22 -0
  94. package/app/components/ui/card/CardTitle.vue +17 -0
  95. package/app/components/ui/card/index.ts +7 -0
  96. package/app/components/ui/collapsible/Collapsible.vue +19 -0
  97. package/app/components/ui/collapsible/CollapsibleContent.vue +12 -0
  98. package/app/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
  99. package/app/components/ui/collapsible/index.ts +3 -0
  100. package/app/components/ui/command/Command.vue +98 -0
  101. package/app/components/ui/command/CommandDialog.vue +42 -0
  102. package/app/components/ui/command/CommandEmpty.vue +31 -0
  103. package/app/components/ui/command/CommandGroup.vue +52 -0
  104. package/app/components/ui/command/CommandInput.vue +45 -0
  105. package/app/components/ui/command/CommandItem.vue +85 -0
  106. package/app/components/ui/command/CommandList.vue +32 -0
  107. package/app/components/ui/command/CommandSeparator.vue +23 -0
  108. package/app/components/ui/command/CommandShortcut.vue +19 -0
  109. package/app/components/ui/command/index.ts +29 -0
  110. package/app/components/ui/dialog/Dialog.vue +15 -0
  111. package/app/components/ui/dialog/DialogClose.vue +12 -0
  112. package/app/components/ui/dialog/DialogContent.vue +61 -0
  113. package/app/components/ui/dialog/DialogDescription.vue +25 -0
  114. package/app/components/ui/dialog/DialogFooter.vue +30 -0
  115. package/app/components/ui/dialog/DialogHeader.vue +17 -0
  116. package/app/components/ui/dialog/DialogOverlay.vue +28 -0
  117. package/app/components/ui/dialog/DialogScrollContent.vue +65 -0
  118. package/app/components/ui/dialog/DialogTitle.vue +25 -0
  119. package/app/components/ui/dialog/DialogTrigger.vue +12 -0
  120. package/app/components/ui/dialog/index.ts +10 -0
  121. package/app/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
  122. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +47 -0
  123. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +47 -0
  124. package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +12 -0
  125. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +41 -0
  126. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +27 -0
  127. package/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
  128. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +48 -0
  129. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
  130. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +19 -0
  131. package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
  132. package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +34 -0
  133. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +34 -0
  134. package/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
  135. package/app/components/ui/dropdown-menu/index.ts +16 -0
  136. package/app/components/ui/input/Input.vue +35 -0
  137. package/app/components/ui/input/index.ts +1 -0
  138. package/app/components/ui/navigation-menu/NavigationMenu.vue +42 -0
  139. package/app/components/ui/navigation-menu/NavigationMenuContent.vue +35 -0
  140. package/app/components/ui/navigation-menu/NavigationMenuIndicator.vue +32 -0
  141. package/app/components/ui/navigation-menu/NavigationMenuItem.vue +23 -0
  142. package/app/components/ui/navigation-menu/NavigationMenuLink.vue +30 -0
  143. package/app/components/ui/navigation-menu/NavigationMenuList.vue +30 -0
  144. package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +31 -0
  145. package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +30 -0
  146. package/app/components/ui/navigation-menu/index.ts +14 -0
  147. package/app/components/ui/scroll-area/ScrollArea.vue +31 -0
  148. package/app/components/ui/scroll-area/ScrollBar.vue +38 -0
  149. package/app/components/ui/scroll-area/index.ts +2 -0
  150. package/app/components/ui/select/Select.vue +15 -0
  151. package/app/components/ui/select/SelectContent.vue +59 -0
  152. package/app/components/ui/select/SelectGroup.vue +19 -0
  153. package/app/components/ui/select/SelectItem.vue +42 -0
  154. package/app/components/ui/select/SelectItemText.vue +12 -0
  155. package/app/components/ui/select/SelectLabel.vue +16 -0
  156. package/app/components/ui/select/SelectScrollDownButton.vue +28 -0
  157. package/app/components/ui/select/SelectScrollUpButton.vue +28 -0
  158. package/app/components/ui/select/SelectSeparator.vue +20 -0
  159. package/app/components/ui/select/SelectTrigger.vue +32 -0
  160. package/app/components/ui/select/SelectValue.vue +12 -0
  161. package/app/components/ui/select/index.ts +11 -0
  162. package/app/components/ui/separator/Separator.vue +30 -0
  163. package/app/components/ui/separator/index.ts +1 -0
  164. package/app/components/ui/sheet/Sheet.vue +15 -0
  165. package/app/components/ui/sheet/SheetClose.vue +12 -0
  166. package/app/components/ui/sheet/SheetContent.vue +68 -0
  167. package/app/components/ui/sheet/SheetDescription.vue +23 -0
  168. package/app/components/ui/sheet/SheetFooter.vue +15 -0
  169. package/app/components/ui/sheet/SheetHeader.vue +15 -0
  170. package/app/components/ui/sheet/SheetOverlay.vue +28 -0
  171. package/app/components/ui/sheet/SheetTitle.vue +23 -0
  172. package/app/components/ui/sheet/SheetTrigger.vue +12 -0
  173. package/app/components/ui/sheet/index.ts +8 -0
  174. package/app/components/ui/sidebar/Sidebar.vue +110 -0
  175. package/app/components/ui/sidebar/SidebarContent.vue +23 -0
  176. package/app/components/ui/sidebar/SidebarFooter.vue +18 -0
  177. package/app/components/ui/sidebar/SidebarGroup.vue +18 -0
  178. package/app/components/ui/sidebar/SidebarGroupAction.vue +31 -0
  179. package/app/components/ui/sidebar/SidebarGroupContent.vue +18 -0
  180. package/app/components/ui/sidebar/SidebarGroupLabel.vue +30 -0
  181. package/app/components/ui/sidebar/SidebarHeader.vue +18 -0
  182. package/app/components/ui/sidebar/SidebarInput.vue +19 -0
  183. package/app/components/ui/sidebar/SidebarInset.vue +23 -0
  184. package/app/components/ui/sidebar/SidebarMenu.vue +18 -0
  185. package/app/components/ui/sidebar/SidebarMenuAction.vue +42 -0
  186. package/app/components/ui/sidebar/SidebarMenuBadge.vue +28 -0
  187. package/app/components/ui/sidebar/SidebarMenuButton.vue +60 -0
  188. package/app/components/ui/sidebar/SidebarMenuButtonChild.vue +36 -0
  189. package/app/components/ui/sidebar/SidebarMenuItem.vue +18 -0
  190. package/app/components/ui/sidebar/SidebarMenuSkeleton.vue +35 -0
  191. package/app/components/ui/sidebar/SidebarMenuSub.vue +24 -0
  192. package/app/components/ui/sidebar/SidebarMenuSubButton.vue +43 -0
  193. package/app/components/ui/sidebar/SidebarMenuSubItem.vue +18 -0
  194. package/app/components/ui/sidebar/SidebarProvider.vue +109 -0
  195. package/app/components/ui/sidebar/SidebarRail.vue +35 -0
  196. package/app/components/ui/sidebar/SidebarSeparator.vue +19 -0
  197. package/app/components/ui/sidebar/SidebarTrigger.vue +26 -0
  198. package/app/components/ui/sidebar/index.ts +62 -0
  199. package/app/components/ui/sidebar/utils.ts +19 -0
  200. package/app/components/ui/skeleton/Skeleton.vue +17 -0
  201. package/app/components/ui/skeleton/index.ts +1 -0
  202. package/app/components/ui/sonner/Sonner.vue +67 -0
  203. package/app/components/ui/sonner/index.ts +1 -0
  204. package/app/components/ui/tabs/Tabs.vue +26 -0
  205. package/app/components/ui/tabs/TabsContent.vue +23 -0
  206. package/app/components/ui/tabs/TabsList.vue +28 -0
  207. package/app/components/ui/tabs/TabsTrigger.vue +30 -0
  208. package/app/components/ui/tabs/index.ts +4 -0
  209. package/app/components/ui/textarea/Textarea.vue +32 -0
  210. package/app/components/ui/textarea/index.ts +1 -0
  211. package/app/components/ui/tooltip/Tooltip.vue +15 -0
  212. package/app/components/ui/tooltip/TooltipContent.vue +49 -0
  213. package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
  214. package/app/components/ui/tooltip/TooltipTrigger.vue +12 -0
  215. package/app/components/ui/tooltip/index.ts +4 -0
  216. package/app/composables/useActiveHeading.ts +90 -0
  217. package/app/composables/useDuxtAnimatedHeight.ts +155 -0
  218. package/app/composables/useDuxtBreadcrumb.ts +60 -0
  219. package/app/composables/useDuxtChoice.ts +28 -0
  220. package/app/composables/useDuxtCollection.ts +89 -0
  221. package/app/composables/useDuxtConfig.ts +47 -0
  222. package/app/composables/useDuxtNavigation.ts +87 -0
  223. package/app/composables/useDuxtPageFocus.ts +30 -0
  224. package/app/composables/useDuxtPath.ts +42 -0
  225. package/app/composables/useDuxtSearch.ts +208 -0
  226. package/app/composables/useDuxtSection.ts +44 -0
  227. package/app/composables/useDuxtSectionRow.ts +49 -0
  228. package/app/composables/useDuxtShortcuts.ts +58 -0
  229. package/app/composables/useDuxtSiteUrl.ts +27 -0
  230. package/app/composables/useDuxtToast.ts +25 -0
  231. package/app/composables/useDuxtVersion.ts +138 -0
  232. package/app/composables/useFuzzySearch.ts +83 -0
  233. package/app/composables/usePackageManager.ts +4 -0
  234. package/app/composables/useRecentPages.ts +52 -0
  235. package/app/error.vue +133 -0
  236. package/app/layouts/changelog.vue +85 -0
  237. package/app/layouts/default.vue +23 -0
  238. package/app/layouts/docs.vue +62 -0
  239. package/app/layouts/reference.vue +85 -0
  240. package/app/lib/utils.ts +7 -0
  241. package/app/middleware/duxt-section-layout.global.ts +57 -0
  242. package/app/pages/[...slug].vue +441 -0
  243. package/app/pages/index.vue +479 -0
  244. package/app/types/duxt.d.ts +955 -0
  245. package/app/utils/aside.ts +16 -0
  246. package/app/utils/changelog.ts +182 -0
  247. package/app/utils/code-highlight.ts +116 -0
  248. package/app/utils/duxt-config.ts +185 -0
  249. package/app/utils/duxt-text.ts +135 -0
  250. package/app/utils/file-icons.ts +62 -0
  251. package/app/utils/generated-sections.ts +190 -0
  252. package/app/utils/generated-toc.ts +216 -0
  253. package/app/utils/heading-link.ts +32 -0
  254. package/app/utils/locale-path.ts +45 -0
  255. package/app/utils/navigation-tree.ts +206 -0
  256. package/app/utils/nearest-page.ts +72 -0
  257. package/app/utils/openapi.ts +1028 -0
  258. package/app/utils/package-command.ts +162 -0
  259. package/app/utils/request-samples.ts +635 -0
  260. package/app/utils/section-scope.ts +97 -0
  261. package/app/utils/sheet-navigation.ts +100 -0
  262. package/app/utils/source-file.ts +84 -0
  263. package/app/utils/version-choices.ts +68 -0
  264. package/app/utils/version-paths.ts +65 -0
  265. package/bin/duxt-report.mjs +45 -0
  266. package/components.json +19 -0
  267. package/content-cache.ts +193 -0
  268. package/content.config.ts +38 -0
  269. package/duxt-app-config.ts +124 -0
  270. package/highlight-langs.ts +68 -0
  271. package/i18n/i18n.config.ts +16 -0
  272. package/i18n/locales/de/duxt/changelog.json +17 -0
  273. package/i18n/locales/de/duxt/code.json +13 -0
  274. package/i18n/locales/de/duxt/defaults.json +22 -0
  275. package/i18n/locales/de/duxt/devtools.json +7 -0
  276. package/i18n/locales/de/duxt/error.json +10 -0
  277. package/i18n/locales/de/duxt/footer.json +7 -0
  278. package/i18n/locales/de/duxt/locale.json +7 -0
  279. package/i18n/locales/de/duxt/nav.json +17 -0
  280. package/i18n/locales/de/duxt/openapi.json +89 -0
  281. package/i18n/locales/de/duxt/page.json +34 -0
  282. package/i18n/locales/de/duxt/search.json +12 -0
  283. package/i18n/locales/de/duxt/shortcuts.json +11 -0
  284. package/i18n/locales/de/duxt/theme.json +7 -0
  285. package/i18n/locales/de/duxt/toc.json +7 -0
  286. package/i18n/locales/de/duxt/version.json +25 -0
  287. package/i18n/locales/en/duxt/changelog.json +17 -0
  288. package/i18n/locales/en/duxt/code.json +13 -0
  289. package/i18n/locales/en/duxt/defaults.json +22 -0
  290. package/i18n/locales/en/duxt/devtools.json +7 -0
  291. package/i18n/locales/en/duxt/error.json +10 -0
  292. package/i18n/locales/en/duxt/footer.json +7 -0
  293. package/i18n/locales/en/duxt/locale.json +7 -0
  294. package/i18n/locales/en/duxt/nav.json +17 -0
  295. package/i18n/locales/en/duxt/openapi.json +89 -0
  296. package/i18n/locales/en/duxt/page.json +34 -0
  297. package/i18n/locales/en/duxt/search.json +12 -0
  298. package/i18n/locales/en/duxt/shortcuts.json +11 -0
  299. package/i18n/locales/en/duxt/theme.json +7 -0
  300. package/i18n/locales/en/duxt/toc.json +7 -0
  301. package/i18n/locales/en/duxt/version.json +25 -0
  302. package/i18n/locales/es/duxt/changelog.json +17 -0
  303. package/i18n/locales/es/duxt/code.json +13 -0
  304. package/i18n/locales/es/duxt/defaults.json +22 -0
  305. package/i18n/locales/es/duxt/devtools.json +7 -0
  306. package/i18n/locales/es/duxt/error.json +10 -0
  307. package/i18n/locales/es/duxt/footer.json +7 -0
  308. package/i18n/locales/es/duxt/locale.json +7 -0
  309. package/i18n/locales/es/duxt/nav.json +17 -0
  310. package/i18n/locales/es/duxt/openapi.json +89 -0
  311. package/i18n/locales/es/duxt/page.json +34 -0
  312. package/i18n/locales/es/duxt/search.json +12 -0
  313. package/i18n/locales/es/duxt/shortcuts.json +11 -0
  314. package/i18n/locales/es/duxt/theme.json +7 -0
  315. package/i18n/locales/es/duxt/toc.json +7 -0
  316. package/i18n/locales/es/duxt/version.json +25 -0
  317. package/i18n/locales/fr/duxt/changelog.json +17 -0
  318. package/i18n/locales/fr/duxt/code.json +13 -0
  319. package/i18n/locales/fr/duxt/defaults.json +22 -0
  320. package/i18n/locales/fr/duxt/devtools.json +7 -0
  321. package/i18n/locales/fr/duxt/error.json +10 -0
  322. package/i18n/locales/fr/duxt/footer.json +7 -0
  323. package/i18n/locales/fr/duxt/locale.json +7 -0
  324. package/i18n/locales/fr/duxt/nav.json +17 -0
  325. package/i18n/locales/fr/duxt/openapi.json +89 -0
  326. package/i18n/locales/fr/duxt/page.json +34 -0
  327. package/i18n/locales/fr/duxt/search.json +12 -0
  328. package/i18n/locales/fr/duxt/shortcuts.json +11 -0
  329. package/i18n/locales/fr/duxt/theme.json +7 -0
  330. package/i18n/locales/fr/duxt/toc.json +7 -0
  331. package/i18n/locales/fr/duxt/version.json +25 -0
  332. package/i18n/locales/pt/duxt/changelog.json +17 -0
  333. package/i18n/locales/pt/duxt/code.json +13 -0
  334. package/i18n/locales/pt/duxt/defaults.json +22 -0
  335. package/i18n/locales/pt/duxt/devtools.json +7 -0
  336. package/i18n/locales/pt/duxt/error.json +10 -0
  337. package/i18n/locales/pt/duxt/footer.json +7 -0
  338. package/i18n/locales/pt/duxt/locale.json +7 -0
  339. package/i18n/locales/pt/duxt/nav.json +17 -0
  340. package/i18n/locales/pt/duxt/openapi.json +89 -0
  341. package/i18n/locales/pt/duxt/page.json +34 -0
  342. package/i18n/locales/pt/duxt/search.json +12 -0
  343. package/i18n/locales/pt/duxt/shortcuts.json +11 -0
  344. package/i18n/locales/pt/duxt/theme.json +7 -0
  345. package/i18n/locales/pt/duxt/toc.json +7 -0
  346. package/i18n/locales/pt/duxt/version.json +25 -0
  347. package/i18n/locales/pt-BR/duxt/error.json +8 -0
  348. package/i18n/locales/pt-BR/duxt/nav.json +10 -0
  349. package/i18n/locales/pt-BR/duxt/page.json +10 -0
  350. package/i18n/locales/pt-BR/duxt/search.json +11 -0
  351. package/i18n/locales/pt-BR/duxt/version.json +13 -0
  352. package/mdc.config.ts +28 -0
  353. package/modules/config.ts +440 -0
  354. package/modules/devtools.ts +126 -0
  355. package/modules/git-meta.ts +349 -0
  356. package/modules/redirects.ts +152 -0
  357. package/modules/validate.ts +137 -0
  358. package/nuxt.config.ts +530 -0
  359. package/openapi-model.ts +358 -0
  360. package/openapi-parse.ts +1052 -0
  361. package/package.json +161 -0
  362. package/public/devtools/cache.html +318 -0
  363. package/public/devtools/checks.html +309 -0
  364. package/public/devtools/config.html +461 -0
  365. package/public/devtools/i18n.html +309 -0
  366. package/public/devtools/pages.html +348 -0
  367. package/public/devtools/paths.html +315 -0
  368. package/public/devtools/redirects.html +309 -0
  369. package/public/devtools/search.html +324 -0
  370. package/public/devtools/sources.html +309 -0
  371. package/public/devtools/versions.html +318 -0
  372. package/report.ts +303 -0
  373. package/repository-root.ts +26 -0
  374. package/section-reports.ts +55 -0
  375. package/sections-changelog.ts +644 -0
  376. package/sections-openapi.ts +450 -0
  377. package/sections-resolve.ts +711 -0
  378. package/sections.ts +157 -0
  379. package/server/devtools/content.ts +120 -0
  380. package/server/devtools/context.ts +38 -0
  381. package/server/devtools/entry-path.ts +30 -0
  382. package/server/devtools/handler.ts +104 -0
  383. package/server/devtools/preview.ts +461 -0
  384. package/server/devtools/render/content.ts +233 -0
  385. package/server/devtools/render/sources.ts +414 -0
  386. package/server/devtools/render/system.ts +352 -0
  387. package/server/devtools/shell.ts +611 -0
  388. package/server/devtools/sources.ts +122 -0
  389. package/server/devtools/system.ts +178 -0
  390. package/server/mcp/tools/list-pages.ts +52 -0
  391. package/server/mcp/tools/read-page.ts +57 -0
  392. package/server/mcp/tools/search-docs.ts +74 -0
  393. package/server/middleware/raw-markdown.ts +86 -0
  394. package/server/routes/llms-full.txt.get.ts +75 -0
  395. package/server/routes/llms.txt.get.ts +63 -0
  396. package/server/routes/rss.xml.get.ts +125 -0
  397. package/server/utils/duxt-server-text.ts +46 -0
  398. package/sources-git.ts +113 -0
  399. package/sources-resolve.ts +908 -0
  400. package/sources.ts +287 -0
  401. package/validate-report.ts +494 -0
@@ -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
+ }