@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,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
+ }