@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,908 @@
1
+ // Type-only, and therefore erased: the section resolver imports the functions
2
+ // below, so a value import here would be a cycle between the two files.
3
+ import type {
4
+ DuxtGeneratedMeta,
5
+ DuxtGeneratedSection
6
+ } from './sections-resolve';
7
+
8
+ /**
9
+ * A documentation source: a folder, in this repository or another, at the
10
+ * current checkout or at named refs.
11
+ */
12
+ export interface DuxtSource {
13
+ /** Folder holding the Markdown, relative to the repository root. */
14
+ path?: string;
15
+ /** `owner/name` or a full git URL. Omitted means this repository. */
16
+ repo?: string;
17
+ /**
18
+ * Refs to publish as versions. Omitted means the current checkout.
19
+ *
20
+ * A bare string is a branch. A tag has to say so — git keeps the two in
21
+ * separate namespaces, and asking for a tag under refs/heads fails the
22
+ * build with "Could not find refs/heads/…".
23
+ */
24
+ refs?: DuxtRef[];
25
+ /**
26
+ * Languages this source is available in, beyond the one written in `path`.
27
+ *
28
+ * A string is the folder inside `path`: `'de-DE'` reads `docs/de-DE/`. An
29
+ * object overrides where that language lives — its own folder, its own
30
+ * repository, its own ref — which is what lets a translation be maintained by
31
+ * other people, at another pace, in a repository of their own.
32
+ *
33
+ * The DEFAULT locale is the exception: it is the tree in `path` itself, with
34
+ * no folder, so listing it changes nothing. That is what keeps this key
35
+ * additive — a site that adds `locales` does not move the pages it already
36
+ * serves. Which one is the default comes from `defaultLocale`.
37
+ */
38
+ locales?: DuxtSourceLocale[];
39
+ /**
40
+ * The version THIS source is, where no ref names one.
41
+ *
42
+ * A version is normally a checkout — list `refs` and each becomes one. An API
43
+ * is usually not versioned that way: `openapi/v1.yaml` sits beside
44
+ * `openapi/v2.yaml` in one repository, and without this key the two are two
45
+ * unrelated sections rather than two versions of one document.
46
+ *
47
+ * Named here, everything else follows the ref path exactly: the URL segment,
48
+ * the switcher entry (scoped to the same artefact), the banner and the
49
+ * canonical. `sourceOptions.defaultRef` names which of them is served without
50
+ * a prefix, whether it is a ref or one of these.
51
+ *
52
+ * Never beside `refs` — a source with both would have to be served at two
53
+ * prefixes at once, and the resolver says so rather than picking one.
54
+ */
55
+ version?: string;
56
+ /** Shown in the version switcher and used in the URL; defaults to the ref. */
57
+ label?: string;
58
+ /**
59
+ * Segment used in the URL for this source; defaults to the repository name.
60
+ *
61
+ * NAMING ONE IS A CLAIM ON A SEGMENT, and that is what makes it more than a
62
+ * spelling: the automatic rule adds a segment to every source once the list
63
+ * holds more than one repository, which cannot express "the documentation at
64
+ * the root, one thing beside it". A source that names a slug gets its segment
65
+ * whether or not the rule fires, and the sources that name none are untouched
66
+ * — so a site adding a second source keeps every URL it already serves.
67
+ */
68
+ slug?: string;
69
+ /**
70
+ * Lifecycle of every version this entry publishes, unless a ref says
71
+ * otherwise. See `DuxtSourceStatus`.
72
+ */
73
+ status?: DuxtSourceStatus;
74
+ /**
75
+ * The repository these pages live in, for a source read off disk.
76
+ *
77
+ * Only ever used to link back — "Edit this page", the contributor list.
78
+ * Writing `repo` instead would be the obvious move and the wrong one: `repo`
79
+ * is what makes Content DOWNLOAD a source, so naming this repository there
80
+ * would have the build clone the checkout it is already standing in.
81
+ */
82
+ origin?: { repo: string; ref?: string };
83
+ /**
84
+ * Read this source's git history for "Last updated" and the contributors.
85
+ *
86
+ * Off by default for a REMOTE source, and the reason is a cost, not a
87
+ * limitation: Content clones a repository with `--depth 1`, so the checkout
88
+ * on disk holds exactly one commit and every file appears to have been
89
+ * written by whoever cut the tip — wrong data rather than missing data.
90
+ * Turning this on has the build unshallow that clone once, which downloads
91
+ * the repository's whole history. Worth it for a docs repo, a real cost for
92
+ * a monorepo, and the consumer is the one who knows which they have.
93
+ *
94
+ * A source read off disk is already a full checkout, so its history is read
95
+ * whether or not this is set.
96
+ */
97
+ history?: boolean;
98
+ /**
99
+ * Artefacts beside this source's Markdown, published as pages of the site.
100
+ *
101
+ * Off until declared, the same rule as `feed.path`. A changelog, an OpenAPI
102
+ * document, whatever a registered type can read — see `DuxtGeneratedSection`.
103
+ */
104
+ generated?: DuxtGeneratedSection[];
105
+ }
106
+
107
+ /**
108
+ * Where a version sits in its life.
109
+ *
110
+ * Not the same question as "is this the default". A site can publish v2 as the
111
+ * default while v1 is merely older and v0 is genuinely dead, and the reader has
112
+ * to be told which of the three they are in — a badge in the switcher, a banner
113
+ * on the page, and for `eol` no place in the sitemap at all. Driven by the
114
+ * config rather than derived, because only the maintainer knows.
115
+ *
116
+ * `upcoming` is the one that is not about age. A version can be off the default
117
+ * because it has not happened yet, and telling that reader to "upgrade" is
118
+ * exactly backwards — what they need told is that what they are reading may
119
+ * still change.
120
+ */
121
+ export type DuxtSourceStatus =
122
+ | 'upcoming'
123
+ | 'current'
124
+ | 'maintained'
125
+ | 'deprecated'
126
+ | 'eol';
127
+
128
+ /** A branch by name, or a tag stated as one. */
129
+ export type DuxtRef =
130
+ | string
131
+ | ({ branch: string } & DuxtRefOptions)
132
+ | ({ tag: string } & DuxtRefOptions);
133
+
134
+ interface DuxtRefOptions {
135
+ /** Shown in the switcher and used in the URL; defaults to the ref name. */
136
+ label?: string;
137
+ /** This one version's lifecycle, overriding the source's. */
138
+ status?: DuxtSourceStatus;
139
+ /**
140
+ * The languages THIS version is available in, overriding the source's.
141
+ *
142
+ * Resolved exactly as `status` is (`ref.locales ?? source.locales`), because
143
+ * a translation is usually kept for the current version and not for the two
144
+ * behind it — and a version whose translation nobody maintains is better
145
+ * declared untranslated than served stale.
146
+ */
147
+ locales?: DuxtSourceLocale[];
148
+ }
149
+
150
+ /**
151
+ * One language of a source: the folder it lives in, or where else it lives.
152
+ *
153
+ * The string form is the whole of it for a repository that translates in
154
+ * place. The object form is what the big projects do instead — React keeps
155
+ * `de.react.dev` as its own repository, Vue an entire `vuejs-translations`
156
+ * org — because translators work at their own pace under their own review. A
157
+ * layer that already sources across repositories and refs can offer that as a
158
+ * source entry rather than as a second website.
159
+ */
160
+ export type DuxtSourceLocale =
161
+ | string
162
+ | {
163
+ /** The locale code, matching one the site serves. */
164
+ locale: string;
165
+ /** Folder holding this language, relative to the repository root. */
166
+ path?: string;
167
+ /** A repository of its own — `owner/name` or a git URL. */
168
+ repo?: string;
169
+ /** A ref of its own, when the translation is versioned separately. */
170
+ ref?: DuxtRef;
171
+ };
172
+
173
+ /** The code of a locale entry, whichever form it takes. */
174
+ export const localeCode = (locale: DuxtSourceLocale): string =>
175
+ typeof locale === 'string' ? locale : locale.locale;
176
+
177
+ /**
178
+ * The tag shorthand: the newest one, resolved at build time.
179
+ *
180
+ * Reserved as a ref NAME, so a branch genuinely called `latest` has to be
181
+ * written `{ branch: 'latest' }`. Worth the collision: naming a tag by hand
182
+ * means every release edits the consumer's source list, which is the one thing
183
+ * a version model should not require.
184
+ */
185
+ export const LATEST = 'latest';
186
+
187
+ /** The name of a ref, whichever kind it is. */
188
+ export const refName = (ref: DuxtRef): string =>
189
+ typeof ref === 'string' ? ref : 'tag' in ref ? ref.tag : ref.branch;
190
+
191
+ /** Is this ref the `latest` shorthand rather than a name git knows? */
192
+ export const isLatestRef = (ref: DuxtRef): boolean =>
193
+ refName(ref) === LATEST && (typeof ref === 'string' || !('branch' in ref));
194
+
195
+ /** A tag has to say so — git keeps the two namespaces apart. */
196
+ export const refIsTag = (ref: DuxtRef): boolean =>
197
+ typeof ref !== 'string' && 'tag' in ref;
198
+
199
+ export interface DuxtSourcesOptions {
200
+ /**
201
+ * Force a repository segment even with a single repository.
202
+ *
203
+ * All or nothing, and deliberately: it answers "does this site have prefixes
204
+ * at all". A single source that wants one uses `slug` instead.
205
+ */
206
+ showRepo?: boolean;
207
+ /** Force a version segment even with a single version. */
208
+ showVersion?: boolean;
209
+ /** The ref served without a version prefix, by name. Defaults to the first. */
210
+ defaultRef?: string;
211
+ /**
212
+ * The locale whose pages are the tree in `path` itself, with no folder.
213
+ *
214
+ * Defaults to the first entry of the first `locales` list, so a site that
215
+ * never sets it still resolves the same way on both sides. It has to be
216
+ * read from the config rather than from `i18n.defaultLocale`: the
217
+ * collections are declared in `content.config.ts`, which has no access to
218
+ * the Nuxt config, and the two halves must land on identical names.
219
+ */
220
+ defaultLocale?: string;
221
+ }
222
+
223
+ /** One resolved source: which collection serves which URL prefix. */
224
+ export interface DuxtResolvedSource {
225
+ /** Collection name Content will register. */
226
+ collection: string;
227
+ /** URL prefix it serves; '' for the root. */
228
+ prefix: string;
229
+ /**
230
+ * The source's own segment, where it has one — because the list holds more
231
+ * than one repository, or because the source named a `slug`.
232
+ *
233
+ * Read as an IDENTITY as much as a segment: the version switcher and the
234
+ * search grouping both scope themselves by it, so two sources with different
235
+ * segments never offer each other's versions.
236
+ */
237
+ repo?: string;
238
+ /** Version label, when the list has more than one version. */
239
+ version?: string;
240
+ /** True for the version served without a prefix. */
241
+ isDefault: boolean;
242
+ /**
243
+ * Where the pages came from, carried through so the theme can link back to
244
+ * them. "Edit this page", "Last updated" and the contributor list are all
245
+ * this plus a file name — none of them needs config of its own.
246
+ */
247
+ repository?: string;
248
+ /** The browsable URL of that repository, when there is one. */
249
+ repositoryUrl?: string;
250
+ /** The ref the pages were read at, by name. */
251
+ ref?: string;
252
+ /** Which namespace that ref lives in. */
253
+ refKind?: 'branch' | 'tag';
254
+ /** Folder inside the repository holding the Markdown. */
255
+ path: string;
256
+ /**
257
+ * The locale this collection serves, when the site has translations.
258
+ *
259
+ * Absent on a site with none — which keeps every existing collection name,
260
+ * prefix and query exactly as it was.
261
+ */
262
+ locale?: string;
263
+ /** True for the locale served from `path` itself, without a folder. */
264
+ isDefaultLocale: boolean;
265
+ /** Where this version sits in its life; `current` unless stated. */
266
+ status: DuxtSourceStatus;
267
+ /** Whether the build may read this source's git history. */
268
+ history: boolean;
269
+ /**
270
+ * Present when this collection is a GENERATED SECTION rather than a docs
271
+ * tree — see `resolveGeneratedSections`.
272
+ *
273
+ * The one question anything downstream asks: the version switcher to know it
274
+ * has nothing to offer here, the layout slot to know which layout to set, and
275
+ * "edit this page" to link at the artefact instead of at a file per page.
276
+ */
277
+ generated?: DuxtGeneratedMeta;
278
+ }
279
+
280
+ /**
281
+ * A ref or a repository name, as a URL segment.
282
+ *
283
+ * The trim is a loop rather than `/^[-.]+|[-.]+$/`: an anchored `+` over a
284
+ * character class backtracks, so a name that is nothing but separators — which
285
+ * a branch name may be, and which comes from config this layer does not
286
+ * own — costs time quadratic in its length. Scanning from each end costs its
287
+ * length, once.
288
+ */
289
+ export const slugify = (value: string) => {
290
+ const collapsed = value.replace(/[^a-z0-9.]+/gi, '-');
291
+
292
+ const isSeparator = (char: string | undefined) =>
293
+ char === '-' || char === '.';
294
+
295
+ let start = 0;
296
+ let end = collapsed.length;
297
+ while (start < end && isSeparator(collapsed[start])) start += 1;
298
+ while (end > start && isSeparator(collapsed[end - 1])) end -= 1;
299
+
300
+ return collapsed.slice(start, end);
301
+ };
302
+
303
+ /**
304
+ * Collection names are not URL segments.
305
+ *
306
+ * Content requires a valid JavaScript identifier and silently DROPS a
307
+ * collection whose name is not one — a warning in the build log, a version
308
+ * missing from the site, and nothing connecting the two. So the name is
309
+ * derived separately from the prefix: dashes and dots become underscores.
310
+ */
311
+ /** The name a partial in the default language resolves against. */
312
+ export const PARTIALS_COLLECTION = 'duxt_partials';
313
+
314
+ /**
315
+ * The partials collection one language reads.
316
+ *
317
+ * Named exactly as the page collections are: the default language keeps the
318
+ * bare name a single-language site already had, and every other language
319
+ * appends its code. So a site that declares no `locales` gets the one
320
+ * collection it always got, under the name it always had.
321
+ *
322
+ * Takes the RESOLVED SOURCE rather than a code, because "the default language"
323
+ * is a property of the manifest and not of the string `en`: a site whose
324
+ * default is `de` still calls that collection `duxt_partials`. A bare string is
325
+ * accepted for the build's own grouping, where the default is already
326
+ * `undefined`.
327
+ */
328
+ export function partialsCollection(
329
+ locale?: Pick<DuxtResolvedSource, 'locale' | 'isDefaultLocale'> | string
330
+ ): string {
331
+ const code =
332
+ typeof locale === 'string'
333
+ ? locale
334
+ : locale && !locale.isDefaultLocale
335
+ ? locale.locale
336
+ : undefined;
337
+
338
+ return code
339
+ ? `${PARTIALS_COLLECTION}_${identifier(code)}`
340
+ : PARTIALS_COLLECTION;
341
+ }
342
+
343
+ /**
344
+ * A collection name Content and TypeScript both accept.
345
+ *
346
+ * Exported because the partials collections are named from the same pieces the
347
+ * page collections are, and a second spelling of this rule is a name that
348
+ * drifts the first time a locale carries a character neither expected.
349
+ */
350
+ export const identifier = (value: string) =>
351
+ value.replace(/[^a-z0-9]+/gi, '_').replace(/^_|_$/g, '');
352
+
353
+ export const repoSlug = (source: DuxtSource) =>
354
+ source.slug ??
355
+ slugify(
356
+ source.repo
357
+ ?.split('/')
358
+ .pop()
359
+ ?.replace(/\.git$/, '') ?? 'docs'
360
+ );
361
+
362
+ /**
363
+ * The URL a repository is cloned from.
364
+ *
365
+ * A value starting with `-` is REJECTED rather than passed on: it reaches `git`
366
+ * as an argument, and `git ls-remote --upload-pack=<anything>` runs that
367
+ * anything. The config it comes from is a file in the consumer's repository
368
+ * rather than user input, but a source list may be computed — from an
369
+ * environment variable, from a directory listing — and the cost of the check is
370
+ * one comparison.
371
+ */
372
+ export const repoUrl = (repo: string) => {
373
+ if (repo.startsWith('-')) {
374
+ throw new Error(
375
+ `duxt: a repository may not start with "-" — got ${JSON.stringify(repo)}`
376
+ );
377
+ }
378
+
379
+ return repo.includes('://') ? repo : `https://github.com/${repo}`;
380
+ };
381
+
382
+ /**
383
+ * The locales one source-and-ref combination expands into.
384
+ *
385
+ * `[undefined]` — one entry, no locale — is the shape every site had before
386
+ * this key existed, and the one every site without it still has. That is what
387
+ * keeps the whole feature additive: no `locales`, no second collection, no
388
+ * change to a single name.
389
+ */
390
+ function localesOf(
391
+ source: DuxtSource,
392
+ ref: DuxtRef | undefined
393
+ ): (DuxtSourceLocale | undefined)[] {
394
+ const declared =
395
+ (ref && typeof ref === 'object' ? ref.locales : undefined) ??
396
+ source.locales;
397
+
398
+ return declared?.length ? declared : [undefined];
399
+ }
400
+
401
+ /** The first locale any source declares — the default when none is configured. */
402
+ function firstLocale(sources: DuxtSource[]): string | undefined {
403
+ for (const source of sources) {
404
+ const declared =
405
+ source.locales ??
406
+ source.refs?.flatMap((ref) =>
407
+ typeof ref === 'object' && ref.locales ? ref.locales : []
408
+ );
409
+
410
+ if (declared?.length) return localeCode(declared[0]!);
411
+ }
412
+
413
+ return undefined;
414
+ }
415
+
416
+ /**
417
+ * Where one language of a source actually lives.
418
+ *
419
+ * The default locale is the tree in `path` itself; every other language is a
420
+ * folder inside it unless the entry says otherwise. An object may move the
421
+ * language wholesale — its own folder, repository and ref — which is the case
422
+ * a translation maintained by other people needs.
423
+ */
424
+ function localeEntry(
425
+ source: DuxtSource,
426
+ locale: DuxtSourceLocale | undefined,
427
+ defaultLocale: string | undefined
428
+ ): { path: string; repo?: string; ref?: DuxtRef } {
429
+ const base = source.path ?? 'docs';
430
+ if (!locale) return { path: base, repo: source.repo };
431
+
432
+ const code = localeCode(locale);
433
+ const override = typeof locale === 'string' ? undefined : locale;
434
+
435
+ if (code === defaultLocale) {
436
+ return {
437
+ path: override?.path ?? base,
438
+ repo: override?.repo ?? source.repo,
439
+ ref: override?.ref
440
+ };
441
+ }
442
+
443
+ return {
444
+ // A folder INSIDE the source's own path, so one repository holds its
445
+ // translations beside the original — and an override escapes that.
446
+ path: override?.path ?? `${base}/${code}`,
447
+ repo: override?.repo ?? source.repo,
448
+ ref: override?.ref
449
+ };
450
+ }
451
+
452
+ /**
453
+ * Resolve the list once: names, prefixes and labels.
454
+ *
455
+ * Both halves of the layer read this — `duxtSources` to declare the
456
+ * collections, `duxtSourceManifest` to tell the app which collection serves the
457
+ * route it is on. Computing it twice is how the two would drift.
458
+ *
459
+ * Each prefix is switched on by the SHAPE OF THE LIST, not per request: a
460
+ * repository segment once there is more than one repository, a version segment
461
+ * once there is more than one version. A single unversioned source therefore
462
+ * serves `/guide/deploying`, and nothing in the URL betrays that repositories
463
+ * or versions exist at all — which is also what keeps the scheme routable,
464
+ * since the shape is fixed before the first request.
465
+ */
466
+ /** One expanded combination: a source, at a ref, in a language. */
467
+ export interface DuxtExpandedSource {
468
+ source: DuxtSource;
469
+ ref?: DuxtRef;
470
+ locale?: DuxtSourceLocale;
471
+ /** Where this combination's Markdown actually lives. */
472
+ effective: { path: string; repo?: string; ref?: DuxtRef };
473
+ }
474
+
475
+ /**
476
+ * The one expansion both halves of the layer read.
477
+ *
478
+ * `content.config.ts` declares the collections and the duxt module resolves
479
+ * the manifest, and the two MUST agree entry for entry — the manifest tells
480
+ * the theme which collection serves a route, and a name computed twice is a
481
+ * name that drifts once. Exported for exactly that reason.
482
+ */
483
+ export function expandSources(
484
+ sources: DuxtSource[],
485
+ options: DuxtSourcesOptions = {}
486
+ ): DuxtExpandedSource[] {
487
+ const defaultLocale = options.defaultLocale ?? firstLocale(sources);
488
+
489
+ return sources.flatMap((source) =>
490
+ (source.refs?.length ? source.refs : [undefined]).flatMap((ref) =>
491
+ localesOf(source, ref).map((locale) => ({
492
+ source,
493
+ ref,
494
+ locale,
495
+ effective: localeEntry(source, locale, defaultLocale)
496
+ }))
497
+ )
498
+ );
499
+ }
500
+
501
+ export function resolveSources(
502
+ sources: DuxtSource[],
503
+ options: DuxtSourcesOptions = {}
504
+ ): DuxtResolvedSource[] {
505
+ const defaultLocale = options.defaultLocale ?? firstLocale(sources);
506
+ const expanded = expandSources(sources, options);
507
+
508
+ const repos = new Set(sources.map((source) => source.repo ?? ''));
509
+
510
+ /**
511
+ * What NAMES a version — a ref, or a source that declares one.
512
+ *
513
+ * A ref is the usual answer and the only one this resolver had: a version was
514
+ * a checkout, so a repository that does not tag its API had no versions at
515
+ * all. That is the common shape for an API — `openapi/v1.yaml` beside
516
+ * `openapi/v2.yaml` in one checkout, versioned by FILE — and it produced two
517
+ * unrelated sections rather than two versions of one document.
518
+ *
519
+ * So a source may name its own version. Everything downstream is unchanged,
520
+ * because everything downstream reads this name: the URL segment, the
521
+ * switcher entry, the banner, the canonical.
522
+ */
523
+ const names = new Set(
524
+ expanded
525
+ .map((entry) => (entry.ref ? refName(entry.ref) : entry.source.version))
526
+ .filter(Boolean) as string[]
527
+ );
528
+
529
+ const withRepo = options.showRepo ?? repos.size > 1;
530
+ const withVersion = options.showVersion ?? names.size > 1;
531
+ const defaultRef = options.defaultRef ?? [...names][0];
532
+
533
+ /**
534
+ * Does THIS source get a segment of its own?
535
+ *
536
+ * The list-wide rule above, or the source's own `slug` — see there for why a
537
+ * slug is a claim rather than a spelling. Per source rather than per list,
538
+ * which is the whole difference: a site whose docs sit at the root can hang
539
+ * one prefixed source beside them without moving a single existing URL.
540
+ */
541
+ const segmented = (source: DuxtSource) => withRepo || Boolean(source.slug);
542
+
543
+ const resolved: DuxtResolvedSource[] = [];
544
+ const taken = new Map<string, string>();
545
+
546
+ for (const { source, ref, locale, effective: entry } of expanded) {
547
+ const effectiveRef = entry.ref ?? ref;
548
+
549
+ // TWO TRUTHS ABOUT ONE SOURCE is not a state this resolver can be in: a
550
+ // source that both lists refs and names a version would have to be served
551
+ // at two prefixes at once, and the reader would meet the same document
552
+ // twice in the switcher.
553
+ if (effectiveRef && source.version) {
554
+ throw new Error(
555
+ `duxt: the source "${source.path ?? source.repo ?? ''}" names the ` +
556
+ `version "${source.version}" and lists refs. A version comes from ` +
557
+ 'a ref or from this key, never from both.'
558
+ );
559
+ }
560
+
561
+ const name = effectiveRef ? refName(effectiveRef) : source.version;
562
+ const label =
563
+ (ref && typeof ref === 'object' ? ref.label : undefined) ?? source.label;
564
+ const code = locale ? localeCode(locale) : undefined;
565
+ const isDefaultLocale = !code || code === defaultLocale;
566
+ const version = name ? slugify(label ?? name) : undefined;
567
+ const isDefault = !name || name === defaultRef;
568
+
569
+ const segments: string[] = [];
570
+ if (segmented(source)) segments.push(repoSlug(source));
571
+ if (withVersion && version && !isDefault) segments.push(version);
572
+
573
+ // THE LOCALE IS NOT PART OF THE PREFIX. @nuxtjs/i18n already puts it in
574
+ // front of the path, so a collection carrying it too would spell it twice
575
+ // in one URL. Original and translation therefore live under IDENTICAL
576
+ // content paths in different collections — which leaves every path
577
+ // comparison in the theme untouched and makes the fallback a second query
578
+ // for the same path.
579
+ const prefix = segments.length ? `/${segments.join('/')}` : '';
580
+ const collection =
581
+ ['docs', ...(isDefaultLocale ? [] : [code!]), ...segments]
582
+ .map(identifier)
583
+ .join('_') || 'docs';
584
+
585
+ // Claimed per locale: two languages serving one prefix is not a collision,
586
+ // it is the point.
587
+ const claim = `${code ?? ''}|${prefix}`;
588
+ const previous = taken.get(claim);
589
+ if (previous) {
590
+ // The one ambiguity the build-time decision leaves: a docs folder named
591
+ // like a repository or a version. Rejected rather than resolved silently.
592
+ throw new Error(
593
+ `duxt: two sources resolve to the same URL prefix "${prefix || '/'}" ` +
594
+ `(${previous} and ${source.repo ?? 'this repository'}${ref ? `@${ref}` : ''}). ` +
595
+ 'Give one of them a `slug` or a `label`.'
596
+ );
597
+ }
598
+ taken.set(
599
+ claim,
600
+ `${source.repo ?? 'this repository'}${name ? `@${name}` : ''}`
601
+ );
602
+
603
+ resolved.push({
604
+ collection,
605
+ prefix,
606
+ repo: segmented(source) ? repoSlug(source) : undefined,
607
+ version,
608
+ isDefault,
609
+ repository: entry.repo ?? source.origin?.repo,
610
+ repositoryUrl: entry.repo
611
+ ? repoUrl(entry.repo)
612
+ : source.origin?.repo
613
+ ? repoUrl(source.origin.repo)
614
+ : undefined,
615
+ ref: name ?? source.origin?.ref,
616
+ refKind: effectiveRef
617
+ ? refIsTag(effectiveRef)
618
+ ? 'tag'
619
+ : 'branch'
620
+ : undefined,
621
+ path: entry.path,
622
+ locale: code,
623
+ isDefaultLocale,
624
+ status:
625
+ (ref && typeof ref === 'object' ? ref.status : undefined) ??
626
+ source.status ??
627
+ 'current',
628
+ // A local source is a full checkout already; a remote one has to be
629
+ // unshallowed, which is why it has to be asked for.
630
+ history: entry.repo ? (source.history ?? false) : true
631
+ });
632
+ }
633
+
634
+ return resolved;
635
+ }
636
+
637
+ /**
638
+ * The locales a page may be served from, best first.
639
+ *
640
+ * A translated Markdown file cannot be merged the way a locale FILE is: a page
641
+ * is translated whole or not at all, so this is a chain of choices rather than
642
+ * a deep merge. It follows the rule the layer already applies twice — the
643
+ * `files` arrays in `nuxt.config.ts` and `resolveDuxtText` both let the
644
+ * language carry the content and the region carry only its deviations:
645
+ *
646
+ * 1. the locale itself — `de-DE` reads `de-DE`;
647
+ * 2. its base language — `de-DE` reads a `de` tree, which is what lets one
648
+ * `pt/` folder serve both `pt-PT` and `pt-BR`;
649
+ * 3. a sibling of the same language — `pt-BR` reads `pt-PT` before it gives
650
+ * up on Portuguese and falls back to English;
651
+ * 4. `fallbackLocale`, the value vue-i18n already carries for missing keys,
652
+ * so the interface and the pages agree on where they fall back to;
653
+ * 5. the default locale, which is the untranslated original.
654
+ *
655
+ * `undefined` closes every chain: it is the collection of a site that declares
656
+ * no locales at all, and the one a translation ultimately falls back to.
657
+ */
658
+ export function localeChain(
659
+ locale: string | undefined,
660
+ available: (string | undefined)[],
661
+ fallbackLocale?: string | string[]
662
+ ): (string | undefined)[] {
663
+ const has = (code: string | undefined) => available.includes(code);
664
+ const chain: (string | undefined)[] = [];
665
+
666
+ const add = (code: string | undefined) => {
667
+ if (!chain.includes(code) && has(code)) chain.push(code);
668
+ };
669
+
670
+ if (locale) {
671
+ add(locale);
672
+
673
+ const base = locale.split('-')[0]!;
674
+ add(base);
675
+
676
+ // A sibling region of the same language, in the order the site declares
677
+ // them: `pt-BR` takes `pt-PT` over English, every time.
678
+ for (const code of available) {
679
+ if (code && code !== locale && code.split('-')[0] === base) add(code);
680
+ }
681
+ }
682
+
683
+ for (const code of [fallbackLocale ?? []].flat()) add(code);
684
+
685
+ // The original. Always last, and always reachable — a page that exists in no
686
+ // translation still has to render.
687
+ if (!chain.includes(undefined)) chain.push(undefined);
688
+
689
+ return chain;
690
+ }
691
+
692
+ /**
693
+ * The resolved list, as data the app can read.
694
+ *
695
+ * Kept out of `sources.ts` deliberately: that file imports `@nuxt/content` to
696
+ * declare collections, and importing a module's entry point from client code is
697
+ * rejected by the bundler. This file is plain logic, so `app.config.ts` can read
698
+ * it and both halves still resolve the list exactly once.
699
+ *
700
+ * THE DOCUMENTATION HALF ONLY. `duxtSources` walks this list index-for-index
701
+ * against `expandSources`, so a generated section appended here would put the
702
+ * two out of step; `duxtManifest` in `sections-resolve.ts` is the whole
703
+ * manifest, and what everything serving a site reads.
704
+ */
705
+ export function duxtSourceManifest(
706
+ sources: DuxtSource[],
707
+ options: DuxtSourcesOptions = {}
708
+ ): DuxtResolvedSource[] {
709
+ return resolveSources(sources, options);
710
+ }
711
+
712
+ /**
713
+ * Order two version-ish tag names the way a release list is ordered.
714
+ *
715
+ * Semver order, NOT tag date: a patch cut for an old line after the newest
716
+ * minor would otherwise become "latest" and move the whole site back a version.
717
+ * A `v` prefix is optional, a pre-release sorts below the release it precedes,
718
+ * and anything that is not a version at all sorts last so it can never win.
719
+ */
720
+ export function compareVersionTags(a: string, b: string): number {
721
+ const parse = (value: string) => {
722
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(value.trim());
723
+ if (!match) return undefined;
724
+
725
+ return {
726
+ numbers: [Number(match[1]), Number(match[2]), Number(match[3])],
727
+ pre: match[4]
728
+ };
729
+ };
730
+
731
+ const left = parse(a);
732
+ const right = parse(b);
733
+
734
+ if (!left || !right) return left ? -1 : right ? 1 : a.localeCompare(b);
735
+
736
+ for (let index = 0; index < 3; index++) {
737
+ const difference = right.numbers[index]! - left.numbers[index]!;
738
+ if (difference) return difference;
739
+ }
740
+
741
+ // 1.0.0 outranks 1.0.0-rc.1; two pre-releases fall back to their own order.
742
+ if (left.pre && !right.pre) return 1;
743
+ if (right.pre && !left.pre) return -1;
744
+ if (left.pre && right.pre) return right.pre.localeCompare(left.pre);
745
+
746
+ return 0;
747
+ }
748
+
749
+ /** The newest tag in a list, by `compareVersionTags`. */
750
+ export function newestTag(tags: string[]): string | undefined {
751
+ return [...tags].sort(compareVersionTags)[0];
752
+ }
753
+
754
+ /**
755
+ * The URL segments a source's own folders must not use.
756
+ *
757
+ * With a prefix active, `/workflows` may be a repository, a version or a docs
758
+ * folder — and the resolver settles which at build time, so the folder is the
759
+ * one that loses. This names the collision rather than resolving it silently:
760
+ * for each source, the segments that any OTHER source's prefix claims one level
761
+ * below its own.
762
+ */
763
+ export function reservedSegments(
764
+ resolved: DuxtResolvedSource[]
765
+ ): Map<string, Set<string>> {
766
+ const reserved = new Map<string, Set<string>>();
767
+
768
+ for (const source of resolved) {
769
+ const claimed = new Set<string>();
770
+
771
+ for (const other of resolved) {
772
+ if (other === source || !other.prefix) continue;
773
+ if (other.prefix === source.prefix) continue;
774
+
775
+ const inside = source.prefix
776
+ ? other.prefix.startsWith(`${source.prefix}/`) &&
777
+ other.prefix.slice(source.prefix.length + 1)
778
+ : other.prefix.slice(1);
779
+
780
+ if (!inside) continue;
781
+
782
+ const segment = inside.split('/')[0];
783
+ if (segment) claimed.add(segment);
784
+ }
785
+
786
+ reserved.set(source.collection, claimed);
787
+ }
788
+
789
+ return reserved;
790
+ }
791
+
792
+ /**
793
+ * Is the version being read older or newer than the one served by default?
794
+ *
795
+ * The banner needs this and cannot get it from `isDefault`, which only says
796
+ * "not the current one" — and that covers both a release from two years ago and
797
+ * a branch documenting next month's. Telling the second reader to upgrade is
798
+ * exactly backwards.
799
+ *
800
+ * Only answered when BOTH names are versions. A branch is not comparable to a
801
+ * tag: `main` sorts nowhere, and guessing would have said `v0.7.0` was newer
802
+ * than `main` — which is how the banner came to offer a downgrade as an
803
+ * upgrade. Where it cannot be answered, the config's own `status` is what
804
+ * decides, because only the maintainer knows what a branch called `next` is.
805
+ */
806
+ export function versionRelation(
807
+ version: string | undefined,
808
+ preferred: string | undefined
809
+ ): 'older' | 'newer' | 'same' | 'unknown' {
810
+ if (!version || !preferred) return 'unknown';
811
+ if (version === preferred) return 'same';
812
+
813
+ const isVersion = (value: string) =>
814
+ /^v?\d+\.\d+\.\d+(?:-.+)?$/.test(value.trim());
815
+ if (!isVersion(version) || !isVersion(preferred)) return 'unknown';
816
+
817
+ const order = compareVersionTags(version, preferred);
818
+
819
+ // `compareVersionTags` sorts newest FIRST, so a negative result means this
820
+ // version leads the list — it is the newer of the two.
821
+ return order === 0 ? 'same' : order < 0 ? 'newer' : 'older';
822
+ }
823
+
824
+ /**
825
+ * The collections that serve one route, best first.
826
+ *
827
+ * Two decisions in one, because neither is complete without the other: WHICH
828
+ * PREFIX claims the path, and WHICH LANGUAGE of that prefix the reader gets.
829
+ * The locale is deliberately not part of a prefix — `@nuxtjs/i18n` already puts
830
+ * it in front of the path — so original and translation are two collections
831
+ * behind one identical prefix, and a lookup handed only the path cannot tell
832
+ * them apart. Whichever sorted first won, which is the bug this exists to close.
833
+ *
834
+ * `path` is the DOCUMENTATION path, locale segment already stripped, and
835
+ * `locale` is what was stripped off it. The rest is `localeChain`, so the theme
836
+ * and every view that debugs it fall back through the same order.
837
+ *
838
+ * Never empty while `sources` is not: a route whose manifest has not arrived
839
+ * still has to query something, and an empty chain 404s a page that exists.
840
+ */
841
+ export function sourcesForRoute(
842
+ path: string,
843
+ locale: string | undefined,
844
+ sources: DuxtResolvedSource[],
845
+ fallbackLocale?: string | string[]
846
+ ): DuxtResolvedSource[] {
847
+ if (!sources.length) return [];
848
+
849
+ // Read the prefix off the DEFAULT-language entries: every language of one
850
+ // source shares the prefix, so the pool only has to be free of duplicates.
851
+ const withDefaultLocale = sources.filter((source) => source.isDefaultLocale);
852
+ const pool = withDefaultLocale.length ? withDefaultLocale : sources;
853
+
854
+ const prefix =
855
+ (
856
+ [...pool]
857
+ .sort((a, b) => b.prefix.length - a.prefix.length)
858
+ .find(
859
+ (source) => !source.prefix || isInsidePrefix(path, source.prefix)
860
+ ) ??
861
+ pool.find((source) => !source.prefix) ??
862
+ // The landing page matches no prefix on a site whose every source has
863
+ // one. It still needs a real collection for the navigation the header
864
+ // draws, and a literal `docs` names one such a site does not have.
865
+ pool[0]
866
+ )?.prefix ?? '';
867
+
868
+ const candidates = sources.filter((source) => source.prefix === prefix);
869
+
870
+ // Deduplicated, because two links of the chain routinely land on ONE
871
+ // collection: `fallbackLocale` is usually the default locale, and the chain
872
+ // ends at the untranslated original regardless — so `en` and `undefined` both
873
+ // resolve to the same entry, and the panel printed "falls back to docs → docs".
874
+ const ordered = [
875
+ ...new Set(
876
+ localeChain(
877
+ locale,
878
+ candidates.map((source) => source.locale),
879
+ fallbackLocale
880
+ )
881
+ .map((code) =>
882
+ candidates.find((source) =>
883
+ code === undefined ? source.isDefaultLocale : source.locale === code
884
+ )
885
+ )
886
+ .filter(Boolean) as DuxtResolvedSource[]
887
+ )
888
+ ];
889
+
890
+ if (ordered.length) return ordered;
891
+ if (candidates.length) return candidates.slice(0, 1);
892
+
893
+ return sources.slice(0, 1);
894
+ }
895
+
896
+ /**
897
+ * Is `path` inside `prefix`? Segment-aware — `/workflows-old` is not inside
898
+ * `/workflows`, though it starts with it.
899
+ *
900
+ * The same rule as `isInside` in `app/utils/version-paths.ts`, and duplicated
901
+ * rather than imported: that file imports from THIS one, and a cycle between
902
+ * them is what `app.config.ts` reading this module cannot survive.
903
+ */
904
+ function isInsidePrefix(path: string, prefix: string): boolean {
905
+ if (!prefix) return true;
906
+
907
+ return path === prefix || path.startsWith(`${prefix}/`);
908
+ }