@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,62 @@
1
+ /**
2
+ * Icon for a filename or a language id.
3
+ *
4
+ * The vscode-icons collection is the one that actually has an icon per
5
+ * ecosystem; lucide covers the fallbacks. Used by both the code block header
6
+ * and the file tree, so a `.ts` file looks the same in either.
7
+ */
8
+ const byExtension: Record<string, string> = {
9
+ bash: 'vscode-icons:file-type-shell',
10
+ css: 'vscode-icons:file-type-css',
11
+ env: 'vscode-icons:file-type-dotenv',
12
+ go: 'vscode-icons:file-type-go',
13
+ html: 'vscode-icons:file-type-html',
14
+ jpg: 'vscode-icons:file-type-image',
15
+ javascript: 'vscode-icons:file-type-js',
16
+ js: 'vscode-icons:file-type-js',
17
+ json: 'vscode-icons:file-type-json',
18
+ jsonc: 'vscode-icons:file-type-json',
19
+ md: 'vscode-icons:file-type-markdown',
20
+ mdc: 'vscode-icons:file-type-markdown',
21
+ mjs: 'vscode-icons:file-type-js',
22
+ php: 'vscode-icons:file-type-php',
23
+ png: 'vscode-icons:file-type-image',
24
+ python: 'vscode-icons:file-type-python',
25
+ py: 'vscode-icons:file-type-python',
26
+ rs: 'vscode-icons:file-type-rust',
27
+ sh: 'vscode-icons:file-type-shell',
28
+ svg: 'vscode-icons:file-type-svg',
29
+ ts: 'vscode-icons:file-type-typescript',
30
+ typescript: 'vscode-icons:file-type-typescript',
31
+ toml: 'vscode-icons:file-type-toml',
32
+ vue: 'vscode-icons:file-type-vue',
33
+ yaml: 'vscode-icons:file-type-yaml',
34
+ yml: 'vscode-icons:file-type-yaml'
35
+ };
36
+
37
+ /** Whole filenames worth their own icon, checked before the extension. */
38
+ const byName: Record<string, string> = {
39
+ 'package.json': 'vscode-icons:file-type-npm',
40
+ 'pnpm-lock.yaml': 'vscode-icons:file-type-pnpm',
41
+ 'pnpm-workspace.yaml': 'vscode-icons:file-type-pnpm',
42
+ 'nuxt.config.ts': 'vscode-icons:file-type-nuxt',
43
+ 'content.config.ts': 'vscode-icons:file-type-nuxt',
44
+ 'app.config.ts': 'vscode-icons:file-type-nuxt',
45
+ 'tsconfig.json': 'vscode-icons:file-type-tsconfig',
46
+ dockerfile: 'vscode-icons:file-type-docker',
47
+ '.gitignore': 'vscode-icons:file-type-git'
48
+ };
49
+
50
+ export function fileIcon(input?: string, fallback = 'lucide:file') {
51
+ if (!input) return fallback;
52
+
53
+ const name = input.split('/').pop()?.toLowerCase() ?? '';
54
+ if (byName[name]) return byName[name];
55
+
56
+ const extension = name.includes('.') ? name.split('.').pop()! : name;
57
+ return byExtension[extension] ?? fallback;
58
+ }
59
+
60
+ export function folderIcon(open = false) {
61
+ return open ? 'lucide:folder-open' : 'lucide:folder';
62
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * What the app does with a generated section.
3
+ *
4
+ * A section is an ordinary collection at a prefix of its own, so almost nothing
5
+ * in the theme has to know it exists — the collection lookup, the search, the
6
+ * breadcrumb and the navigation tree all work off the manifest and were not
7
+ * touched. Two things do not follow from the collection, and they are here:
8
+ * WHERE its navbar entry goes, and WHICH LAYOUT its pages render in.
9
+ *
10
+ * Pure, and beside the resolver's other path arithmetic for the same reason:
11
+ * every version bug this layer has had came from prefix work done inline in a
12
+ * component, where it could only be checked by clicking.
13
+ */
14
+ // Named rather than auto-imported: a test runs this file outside Nuxt, where
15
+ // nothing fills the auto-imports in.
16
+ import { sourceForPath } from './version-paths';
17
+
18
+ /**
19
+ * The config with each generated section's navbar entry in place.
20
+ *
21
+ * Appended here rather than written into `appConfig` by the build, and that is
22
+ * not a detail: the generated template merges `appConfig` BEHIND every
23
+ * `app.config.ts`, and `mergeDuxtConfig` replaces an array rather than
24
+ * appending to it — so a consumer naming `sections` at all would have replaced
25
+ * the entry the build had just added, and the section would be unreachable on
26
+ * exactly the sites that configure the most.
27
+ *
28
+ * A section already listed by hand is left alone, so a consumer who wants it
29
+ * somewhere else in the row simply writes it there. "Listed by hand" is asked
30
+ * of the SECTION rather than of one URL: the consumer wrote the entry at the
31
+ * position they were reading, and a `per-version` section is at a different URL
32
+ * on every other version — so the per-URL question answered no there and
33
+ * appended a second entry with the same label.
34
+ *
35
+ * ONE DECLARATION IS ONE ENTRY, whatever it resolved to. A section is a
36
+ * collection per version and per locale, so `per-version` and `per-locale`
37
+ * reach here as several manifest entries for the same declared section — and
38
+ * the row is "the top-level parts of the documentation", not a list of the
39
+ * versions of one. So the entry is resolved against the section the reader is
40
+ * currently in, exactly as `DuxtVersion` and `useDuxtVersion` resolve theirs,
41
+ * with a de-dupe by `to` underneath it: two entries pointing at one URL are one
42
+ * link twice, and under `DuxtHeader`'s label key, one Vue key twice.
43
+ *
44
+ * Which entries are one declaration is READ OFF THE MANIFEST, never guessed —
45
+ * see `DuxtGeneratedMeta.declaration` for what guessing it cost.
46
+ */
47
+ export function withGeneratedSections(
48
+ config: DuxtConfig,
49
+ path = '/'
50
+ ): DuxtConfig {
51
+ const sources = config.resolvedSources ?? [];
52
+ const declarations = declarationsIn(sources);
53
+
54
+ if (!declarations.length) return config;
55
+
56
+ const base = readerBase(path, sources);
57
+
58
+ const entriesFor = (
59
+ placement: DuxtSectionPlacementInput,
60
+ existing: DuxtLink[]
61
+ ) => {
62
+ // Seeded from what the consumer wrote and UPDATED as entries are added, so
63
+ // the de-dupe holds among the generated entries as well as against the
64
+ // hand-written ones. Building it once before the map was the bug: it
65
+ // answered for the consumer's list only.
66
+ const taken = new Set(existing.map((entry) => entry.to).filter(Boolean));
67
+ const entries: DuxtLink[] = [];
68
+
69
+ for (const declaration of declarations) {
70
+ const meta = declaration[0]!.generated!;
71
+ if (meta.navigation !== placement) continue;
72
+
73
+ // ANY position of this declaration counts as already in the row, not just
74
+ // the one this reader would be given: a section listed by hand is listed,
75
+ // and the entry `to` resolves to is always one of these prefixes, so this
76
+ // is the de-dupe by URL as well.
77
+ if (declaration.some((entry) => taken.has(entry.prefix))) continue;
78
+
79
+ const to = entryPath(declaration, base);
80
+
81
+ taken.add(to);
82
+ entries.push({ label: meta.label, to, icon: meta.icon });
83
+ }
84
+
85
+ return entries;
86
+ };
87
+
88
+ const navigation = entriesFor('navigation', config.navigation ?? []);
89
+ const sections = entriesFor('sections', config.sections ?? []);
90
+
91
+ if (!navigation.length && !sections.length) return config;
92
+
93
+ return {
94
+ ...config,
95
+ // Only the row that gains something is rewritten: an empty array where the
96
+ // config had `undefined` is a different value, and the header reads both.
97
+ ...(navigation.length
98
+ ? { navigation: [...(config.navigation ?? []), ...navigation] }
99
+ : {}),
100
+ ...(sections.length
101
+ ? { sections: [...(config.sections ?? []), ...sections] }
102
+ : {})
103
+ };
104
+ }
105
+
106
+ /**
107
+ * The manifest's generated entries, grouped into the declarations that made
108
+ * them, in the order the site declared them.
109
+ *
110
+ * A `Map` rather than a sort: the manifest already lists a declaration's
111
+ * entries together, and the grouping has to survive it not doing so.
112
+ */
113
+ function declarationsIn(sources: DuxtResolvedSource[]): DuxtResolvedSource[][] {
114
+ const groups = new Map<number, DuxtResolvedSource[]>();
115
+
116
+ for (const source of sources) {
117
+ if (!source.generated) continue;
118
+
119
+ const group = groups.get(source.generated.declaration);
120
+ if (group) group.push(source);
121
+ else groups.set(source.generated.declaration, [source]);
122
+ }
123
+
124
+ return [...groups.values()];
125
+ }
126
+
127
+ /**
128
+ * The prefix the reader's DOCUMENTATION sits at — their repository and their
129
+ * version, with no section segment on the end.
130
+ *
131
+ * Inside a generated section the reader's version can only be read off that
132
+ * section's own prefix, so its slug is taken back off. `/v1.x/releases` is a
133
+ * reader on `v1.x`, and the row has to keep saying so.
134
+ */
135
+ function readerBase(path: string, sources: DuxtResolvedSource[]): string {
136
+ const current = sourceForPath(path, sources);
137
+ if (!current) return '';
138
+
139
+ const own = current.generated ? `/${current.generated.slug}` : '';
140
+
141
+ return own && current.prefix.endsWith(own)
142
+ ? current.prefix.slice(0, -own.length)
143
+ : current.prefix;
144
+ }
145
+
146
+ /**
147
+ * Where one declared section's navbar entry points, from where the reader is.
148
+ *
149
+ * The entries handed in ARE the declaration — its versions and its languages,
150
+ * as the manifest recorded them. The reader's own base picks one of them.
151
+ */
152
+ function entryPath(declaration: DuxtResolvedSource[], base: string): string {
153
+ const here = `${base}/${declaration[0]!.generated!.slug}`;
154
+
155
+ if (declaration.some((other) => other.prefix === here)) return here;
156
+
157
+ // Nothing at the reader's version: a `global` section has one entry and no
158
+ // version at all, and a version may simply have declared no artefact. The
159
+ // entry nearest the root is the default version's — the resolver adds a
160
+ // version segment for every version EXCEPT the one served without a prefix.
161
+ return declaration.reduce((nearest, other) =>
162
+ depth(other.prefix) < depth(nearest.prefix) ? other : nearest
163
+ ).prefix;
164
+ }
165
+
166
+ /** How many segments deep a prefix is; `''` is the root and therefore zero. */
167
+ const depth = (prefix: string) => (prefix ? prefix.split('/').length - 1 : 0);
168
+
169
+ /**
170
+ * The layout a route asks for, when it is inside a generated section.
171
+ *
172
+ * THE SHARED SLOT. Each type names its own layout and every type renders
173
+ * through this one lookup, which is what stops two types inventing two
174
+ * unrelated ways to escape the docs chrome. A type that names none renders in
175
+ * the ordinary chrome, and the lookup answers `undefined`.
176
+ *
177
+ * Filtered to the sections that name a layout before the prefix match, so a
178
+ * site whose types all render in the docs chrome does no work at all.
179
+ */
180
+ export function generatedLayout(
181
+ path: string,
182
+ sources: DuxtResolvedSource[]
183
+ ): string | undefined {
184
+ const withLayout = sources.filter((source) => source.generated?.layout);
185
+ if (!withLayout.length) return undefined;
186
+
187
+ // Every entry here carries a prefix, so `sourceForPath` returns a genuine
188
+ // match or nothing — it has no prefixless source to fall back to.
189
+ return sourceForPath(path, withLayout)?.generated?.layout;
190
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * The contents column for a page whose headings a COMPONENT draws.
3
+ *
4
+ * Content builds `body.toc` from the Markdown headings it parsed, which is the
5
+ * whole outline of an ordinary page and none of the outline of a generated one:
6
+ * a release page's headings are the group names `ChangelogGroup` renders from a
7
+ * prop. Content sees a component call and nothing else, so `body.toc.links`
8
+ * comes back empty and the page had no contents column at all.
9
+ *
10
+ * So the outline is read back off the AST the page ships anyway — the same
11
+ * nodes `ContentRenderer` is about to draw, before it draws them. That keeps it
12
+ * SSR-safe and pure: no registry a child fills during render (which renders
13
+ * empty on the server and mismatches on hydration), and no second query.
14
+ *
15
+ * Anchors come from `changelog.ts`, which is also what the two components use,
16
+ * so a link here and the id it points at cannot drift apart.
17
+ */
18
+ import { changelogAnchor, changelogLabel } from './changelog';
19
+
20
+ export interface DuxtGeneratedTocLink {
21
+ id: string;
22
+ text: string;
23
+ depth: number;
24
+ children?: DuxtGeneratedTocLink[];
25
+ }
26
+
27
+ const HEADING = /^h([23])$/;
28
+
29
+ /**
30
+ * A prop, whichever spelling it arrived in.
31
+ *
32
+ * Content writes a non-string prop under a `:`-prefixed key holding JSON — the
33
+ * same encoding MDC uses in a document — so `count` is `":count"` and
34
+ * `releases` is a JSON string. Read both, because which one a prop takes
35
+ * depends on the value the parser gave it and not on the component.
36
+ */
37
+ function prop(props: Record<string, unknown>, name: string): unknown {
38
+ if (name in props) return props[name];
39
+
40
+ const raw = props[`:${name}`];
41
+ if (typeof raw !== 'string') return raw;
42
+
43
+ try {
44
+ return JSON.parse(raw);
45
+ } catch {
46
+ return undefined;
47
+ }
48
+ }
49
+
50
+ /** The words a node shows, with its markup dropped. */
51
+ function text(node: unknown): string {
52
+ if (typeof node === 'string') return node;
53
+ if (!Array.isArray(node)) return '';
54
+
55
+ return node.slice(2).map(text).join('');
56
+ }
57
+
58
+ /**
59
+ * Every entry the page's own AST names, in document order and flat.
60
+ *
61
+ * Depth follows what the page RENDERS rather than what the node is: a group is
62
+ * an `<h2>` on the release page, and a heading written under that group was
63
+ * promoted to `##` by the parser — so inside a group it is one level further
64
+ * in, or the column would claim both are siblings.
65
+ */
66
+ function walk(nodes: unknown[], inside: boolean): DuxtGeneratedTocLink[] {
67
+ const links: DuxtGeneratedTocLink[] = [];
68
+
69
+ for (const node of nodes) {
70
+ if (!Array.isArray(node)) continue;
71
+
72
+ const [tag, ...rest] = node as unknown[];
73
+ if (typeof tag !== 'string') continue;
74
+
75
+ const props = (
76
+ rest[0] && typeof rest[0] === 'object' ? rest[0] : {}
77
+ ) as Record<string, unknown>;
78
+ const children = rest.slice(1);
79
+
80
+ if (tag === 'changelog-group') {
81
+ const name = String(prop(props, 'name') ?? '');
82
+ const id = changelogAnchor(name);
83
+
84
+ // The same words the heading itself draws — the anchor is still built
85
+ // from the file's own name, so the two cannot point apart.
86
+ if (id) links.push({ id, text: changelogLabel(name), depth: 2 });
87
+ links.push(...walk(children, true));
88
+ continue;
89
+ }
90
+
91
+ // The release OVERVIEW contributes nothing. Its versions are already the
92
+ // whole visible page — a column repeating the six lines the reader is
93
+ // looking at is a second copy of the list, not a way around it. What is
94
+ // left there is the aside's own links and the page's provenance, which is
95
+ // why the column is drawn on a page with no outline at all.
96
+ if (tag === 'changelog-releases') continue;
97
+
98
+ const heading = HEADING.exec(tag);
99
+ if (heading) {
100
+ const id = props.id;
101
+ if (typeof id === 'string' && id) {
102
+ links.push({
103
+ id,
104
+ text: text(node).trim(),
105
+ depth: inside ? 3 : Number(heading[1])
106
+ });
107
+ }
108
+ continue;
109
+ }
110
+
111
+ links.push(...walk(children, inside));
112
+ }
113
+
114
+ return links;
115
+ }
116
+
117
+ /** The flat list nested the way `DuxtToc` draws it: one level of children. */
118
+ export function generatedToc(body: unknown): DuxtGeneratedTocLink[] {
119
+ const value = (body as { value?: unknown })?.value;
120
+ if (!Array.isArray(value)) return [];
121
+
122
+ const links: DuxtGeneratedTocLink[] = [];
123
+
124
+ for (const link of walk(value, false)) {
125
+ const parent = links.at(-1);
126
+
127
+ if (link.depth > 2 && parent) (parent.children ??= []).push(link);
128
+ else links.push({ ...link, depth: 2 });
129
+ }
130
+
131
+ return links;
132
+ }
133
+
134
+ /**
135
+ * Does this page open on a heading of its own?
136
+ *
137
+ * The one question that decides whether a generated page gets the DOCS HEADER —
138
+ * breadcrumb, title, description, the copy control beside it. A type whose
139
+ * parser writes an `<h1>` has said it draws its own (an endpoint wants its
140
+ * title beside a method chip, which no shell could know); one that writes none
141
+ * has a title and a trail like every other page and should look like one.
142
+ *
143
+ * Read off the artefact rather than declared as a flag on the type: the two
144
+ * cannot then disagree, and the failure a flag would have — an `<h1>` from the
145
+ * parser under an `<h1>` from the shell — is not expressible.
146
+ */
147
+ export function generatedTitle(body: unknown): boolean {
148
+ const value = (body as { value?: unknown })?.value;
149
+ if (!Array.isArray(value)) return false;
150
+
151
+ return value.some((node) => Array.isArray(node) && node[0] === 'h1');
152
+ }
153
+
154
+ /**
155
+ * The prose a generated page OPENS with, flattened to its words.
156
+ *
157
+ * The shell draws a page's description under its title, and for a generated
158
+ * page that was suppressed outright: Content derives a description from the
159
+ * body where the frontmatter names none, and the body then printed the same
160
+ * sentence again three lines below.
161
+ *
162
+ * Derived and written are indistinguishable once they are in `description`, so
163
+ * this answers the question that actually matters — does the body already open
164
+ * with it? A type that writes a real description (an API document's `summary`,
165
+ * which appears nowhere in its prose) gets the line every written page has; one
166
+ * whose description was lifted out of its own first paragraph does not.
167
+ *
168
+ * Walks INTO the first node, because a generated body opens on a component
169
+ * call: the prose is that component's children, not a sibling of it.
170
+ */
171
+ export function generatedLead(body: unknown): string {
172
+ const value = (body as { value?: unknown })?.value;
173
+ if (!Array.isArray(value)) return '';
174
+
175
+ return collapse(lead(value));
176
+ }
177
+
178
+ /**
179
+ * Does the body already open with this description?
180
+ *
181
+ * `startsWith` rather than equality, because a page's description is often the
182
+ * FIRST LINE of prose the page then prints in full — the same sentence at two
183
+ * lengths, and printing both puts one above the rule and one under it.
184
+ *
185
+ * Both sides are collapsed first. Frontmatter folds a wrapped line into a
186
+ * space; the AST keeps the newline the author typed, so the two spellings of
187
+ * one sentence differ by exactly the whitespace nobody can see.
188
+ */
189
+ export function generatedLeadsWith(
190
+ description: string,
191
+ body: unknown
192
+ ): boolean {
193
+ const opening = generatedLead(body);
194
+
195
+ return Boolean(opening) && opening.startsWith(collapse(description));
196
+ }
197
+
198
+ /** One sentence, however it was wrapped. */
199
+ const collapse = (value: string) => value.replace(/\s+/g, ' ').trim();
200
+
201
+ /** The first paragraph's words, however deep the components go. */
202
+ function lead(nodes: unknown[]): string {
203
+ for (const node of nodes) {
204
+ if (!Array.isArray(node)) continue;
205
+
206
+ const [tag, ...rest] = node as unknown[];
207
+ if (typeof tag !== 'string') continue;
208
+
209
+ if (tag === 'p') return text(node).trim();
210
+
211
+ const found = lead(rest.slice(1));
212
+ if (found) return found;
213
+ }
214
+
215
+ return '';
216
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Does this heading's own content already contain a link?
3
+ *
4
+ * A Prose heading wraps the WHOLE heading in an anchor, because the words are
5
+ * what a reader aims at when they want to link to a section. That cannot be
6
+ * done when the heading is itself a link — `## [1.4.0](…/compare/…)`, which is
7
+ * what release-please writes on every release, rendered as one page by the
8
+ * changelog's `flat` granularity. An anchor inside an anchor is invalid HTML:
9
+ * the parser closes the outer one early, and what is left is a link with no
10
+ * text in it. `pnpm check:a11y` found it as five `link-name` violations, which
11
+ * is the honest description — a link a screen reader announces as nothing.
12
+ *
13
+ * READ OFF THE VNODES rather than declared by the caller: a heading does not
14
+ * know what a Markdown file put inside it, and asking is the only way to be
15
+ * right for content nobody has written yet.
16
+ *
17
+ * `href` rather than the tag or the component: MDC renders a link as `ProseA`,
18
+ * a raw one arrives as `a`, and both carry the prop. Recursive, because a
19
+ * heading may wrap its link in emphasis.
20
+ */
21
+ import type { VNode } from 'vue';
22
+
23
+ export function headingHasLink(nodes: unknown): boolean {
24
+ if (Array.isArray(nodes)) return nodes.some(headingHasLink);
25
+ if (!nodes || typeof nodes !== 'object') return false;
26
+
27
+ const node = nodes as VNode;
28
+
29
+ if (node.props && 'href' in node.props) return true;
30
+
31
+ return headingHasLink(node.children);
32
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The route path without its locale segment.
3
+ *
4
+ * With `prefix_except_default` the browser is on `/de-DE/guide/deploying` while
5
+ * the documentation page is `/guide/deploying` — the locale translates the
6
+ * INTERFACE, not the content tree. Every lookup in the theme means the second
7
+ * one: which collection serves this route, which section is active, which page
8
+ * to query. Handing them the raw path 404s every page in every non-default
9
+ * locale, which is exactly what happened the first time this was wired up.
10
+ *
11
+ * Pure and separate from the composable so the seam can be tested without a
12
+ * Nuxt environment, like the source resolver next to it.
13
+ */
14
+ export function stripLocalePrefix(
15
+ path: string,
16
+ codes: Iterable<string>
17
+ ): string {
18
+ return splitLocalePath(path, codes).path;
19
+ }
20
+
21
+ /**
22
+ * The same split, keeping the half that was thrown away.
23
+ *
24
+ * Which language a URL asks for is not decoration: original and translation
25
+ * live under IDENTICAL content paths in different collections, so a lookup
26
+ * given only the stripped path cannot tell them apart and takes whichever
27
+ * sorted first. Anything choosing a collection needs both halves — see
28
+ * `sourcesForRoute`.
29
+ */
30
+ export function splitLocalePath(
31
+ path: string,
32
+ codes: Iterable<string>
33
+ ): { locale?: string; path: string } {
34
+ const first = path.split('/')[1];
35
+ if (!first) return { path };
36
+
37
+ for (const code of codes) {
38
+ if (code !== first) continue;
39
+
40
+ const rest = path.slice(first.length + 1);
41
+ return { locale: code, path: rest.startsWith('/') ? rest : rest || '/' };
42
+ }
43
+
44
+ return { path };
45
+ }