@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,97 @@
1
+ /**
2
+ * Which section-row entries belong to the part of the site the reader is in.
3
+ *
4
+ * `duxt.sections` is one flat list, and on a site with one source that is the
5
+ * whole truth: every entry is a top-level part of the documentation. A site
6
+ * with a SECOND source has two sets of top-level parts, and rendering the list
7
+ * whole showed both everywhere — the demo API's own entries in the row above a
8
+ * getting-started page, and the documentation's six above the demo API.
9
+ *
10
+ * So an entry belongs to the source whose prefix it lies under, and the row
11
+ * shows the entries of the source the reader is in. A site that declares one
12
+ * source has one area, every entry is inside it, and nothing changes.
13
+ *
14
+ * Pure, and beside the resolver's other prefix arithmetic for the same reason
15
+ * that lives there: prefix work done inline in a component can only be checked
16
+ * by clicking.
17
+ */
18
+ import { isInside, sourceForPath } from './version-paths';
19
+
20
+ /** The minimum a source has to carry to place a path or an entry. */
21
+ interface Area {
22
+ prefix: string;
23
+ generated?: unknown;
24
+ }
25
+
26
+ /**
27
+ * The AREAS a site has — one per documentation tree, never per generated
28
+ * section.
29
+ *
30
+ * A reference at `/demo/api` is not a part of the site beside `/demo`; it is
31
+ * something published under it, and its own entry is what the row offers. Were
32
+ * its prefix an area of its own, an operation page would look like a third part
33
+ * of the site and the row would empty out on it.
34
+ */
35
+ const areas = (sources: Area[]): string[] => [
36
+ ...new Set(sources.filter((source) => !source.generated).map((s) => s.prefix))
37
+ ];
38
+
39
+ /**
40
+ * The area a path is in — the longest documentation prefix it sits under.
41
+ *
42
+ * `''` for a site whose sources are all unprefixed, which is every site that
43
+ * never asked for any of this.
44
+ */
45
+ export function areaForPath(path: string, sources: Area[]): string {
46
+ return (
47
+ sourceForPath(
48
+ path,
49
+ areas(sources).map((prefix) => ({ prefix }))
50
+ )?.prefix ?? ''
51
+ );
52
+ }
53
+
54
+ /**
55
+ * The entries of one area, in the order the site declared them.
56
+ *
57
+ * An entry with no `to` cannot be placed and is kept in every area: it is a
58
+ * link the consumer wrote for a reason the layer cannot read, and dropping it
59
+ * would make it disappear from a site that gained a second source.
60
+ */
61
+ export function sectionsForPath<T extends { to?: string }>(
62
+ sections: T[],
63
+ sources: Area[],
64
+ path: string
65
+ ): T[] {
66
+ const area = areaForPath(path, sources);
67
+ const all = areas(sources);
68
+
69
+ // Only when the site HAS two areas: with one, every entry is in it, and the
70
+ // filter is a walk over the list for an answer that cannot change.
71
+ if (all.length < 2) return sections;
72
+
73
+ return sections.filter(
74
+ (section) => !section.to || areaForPath(section.to, sources) === area
75
+ );
76
+ }
77
+
78
+ /**
79
+ * WHICH ONE of an area's entries is the one being read.
80
+ *
81
+ * A row of siblings answers this by itself — `/guides` and `/reference` cannot
82
+ * both prefix a path. An area whose parts nest can: `/demo/api` is inside
83
+ * `/demo`, so marking every entry that prefixes the path lit two chips at once
84
+ * and told the reader they were in both places.
85
+ *
86
+ * The longest match wins, which is the rule `sourceForPath` and `useDuxtSection`
87
+ * already follow — the deepest thing that claims the page is the thing the page
88
+ * is in.
89
+ */
90
+ export function currentSection<T extends { to?: string }>(
91
+ sections: T[],
92
+ path: string
93
+ ): T | undefined {
94
+ return sections
95
+ .filter((section) => section.to && isInside(path, section.to))
96
+ .sort((a, b) => (b.to?.length ?? 0) - (a.to?.length ?? 0))[0];
97
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The mobile sheet's navigation, with the sections hung where they belong.
3
+ *
4
+ * The desktop header shows two rows: the navbar links up top and the sections
5
+ * below. The sheet has no rows, and listing the two as sibling blocks made the
6
+ * documentation look like a peer of its own parts — "Docs" in one list and
7
+ * "Guides", "ADR", "API" in another, with nothing saying the second is what the
8
+ * first opens.
9
+ *
10
+ * It already is, everywhere else. A navbar entry with no `to` means "the
11
+ * documentation" (see `DuxtHeader.linkTarget`), and `entryActive` lights it
12
+ * wherever ANY section is. This gives that entry the children it has always
13
+ * implied, so the sheet can render one tree instead of two lists.
14
+ *
15
+ * Pure and here rather than inline in the component, because the interesting
16
+ * part is what happens when there is nothing to hang them on — and a section
17
+ * that silently reaches no list at all is unreachable on a phone.
18
+ */
19
+ import { areaForPath } from './section-scope';
20
+
21
+ /** The navbar with the sections adopted, plus whatever nobody adopted. */
22
+ export interface DuxtSheetNavigation {
23
+ entries: DuxtLink[];
24
+ /**
25
+ * Sections left over.
26
+ *
27
+ * Empty on the ordinary site, where an entry took them. A consumer who
28
+ * replaces `navigation` wholesale — `mergeDuxtConfig` REPLACES an array — may
29
+ * leave no entry that qualifies, and the sheet then renders these as their own
30
+ * block. Dropping them instead would take the whole documentation off the
31
+ * phone.
32
+ */
33
+ sections: DuxtSection[];
34
+ }
35
+
36
+ /**
37
+ * Which entry may adopt the sections: the first with neither a `to` nor
38
+ * children of its own.
39
+ *
40
+ * Both halves matter. An entry WITH a `to` names one page — a consumer wrote a
41
+ * destination and meant it, and appending eight children under it would
42
+ * repurpose their link. An entry that already has children carries a menu the
43
+ * consumer authored, and the sections are not more of it.
44
+ */
45
+ function adopts(entry: DuxtLink) {
46
+ return !entry.to && !entry.children?.length;
47
+ }
48
+
49
+ /**
50
+ * And which entry adopts ANOTHER AREA's sections: the one pointing at that
51
+ * area's own root.
52
+ *
53
+ * The desktop row scopes itself to the area the reader is in — see
54
+ * `sectionsForPath` — and the sheet has no rows to scope. So a site with a
55
+ * second source hangs that source's parts under the navbar entry that opens it,
56
+ * which is the same relationship the to-less entry has with the documentation,
57
+ * stated with a `to` because this one has somewhere to point.
58
+ */
59
+ const adoptsArea = (entry: DuxtLink, area: string) =>
60
+ Boolean(area) && entry.to === area && !entry.children?.length;
61
+
62
+ export function buildSheetNavigation(
63
+ navigation: DuxtLink[] | undefined,
64
+ sections: DuxtSection[] | undefined,
65
+ sources: { prefix: string; generated?: unknown }[] = []
66
+ ): DuxtSheetNavigation {
67
+ const entries = navigation ?? [];
68
+ const list = sections ?? [];
69
+
70
+ if (!list.length) return { entries, sections: [] };
71
+
72
+ // One bucket per area, in the order the site declared the sections.
73
+ const byArea = new Map<string, DuxtSection[]>();
74
+ for (const section of list) {
75
+ const area = section.to ? areaForPath(section.to, sources) : '';
76
+ byArea.set(area, [...(byArea.get(area) ?? []), section]);
77
+ }
78
+
79
+ const adopted = entries.map((entry) => ({
80
+ entry,
81
+ taken: [] as DuxtSection[]
82
+ }));
83
+ const orphans: DuxtSection[] = [];
84
+
85
+ for (const [area, group] of byArea) {
86
+ const host = adopted.find(({ entry }) =>
87
+ area ? adoptsArea(entry, area) : adopts(entry)
88
+ );
89
+
90
+ if (host) host.taken = group;
91
+ else orphans.push(...group);
92
+ }
93
+
94
+ return {
95
+ entries: adopted.map(({ entry, taken }) =>
96
+ taken.length ? { ...entry, children: taken } : entry
97
+ ),
98
+ sections: orphans
99
+ };
100
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * From a page back to the file it was written in.
3
+ *
4
+ * "Edit this page" is the visible half; the same two functions answer "which
5
+ * file is this" for anything else that needs it. Pure, because every part of
6
+ * this is string arithmetic over a scheme with three moving pieces — the URL
7
+ * prefix, the numbered folder names and the source's own docs folder — and the
8
+ * only way that stays right is a test rather than a click.
9
+ */
10
+
11
+ /**
12
+ * The file's path inside its repository.
13
+ *
14
+ * `stem` is Content's own: the file path with the extension gone and the URL
15
+ * prefix in front, and — unlike `path` — with the numbered folder names still
16
+ * on it. `1.guides/2.add-a-body` renders at `/guides/add-a-body`, so the URL
17
+ * cannot be turned back into a file name; the stem can.
18
+ */
19
+ export function sourceFilePath(
20
+ stem: string,
21
+ prefix: string,
22
+ folder: string
23
+ ): string {
24
+ const lead = prefix.replace(/^\//, '');
25
+ const relative =
26
+ lead && stem.startsWith(`${lead}/`) ? stem.slice(lead.length + 1) : stem;
27
+
28
+ return [folder.replace(/\/+$/, ''), `${relative}.md`]
29
+ .filter(Boolean)
30
+ .join('/');
31
+ }
32
+
33
+ /**
34
+ * The link back to a file: an edit form where one is possible, a view where it
35
+ * is not.
36
+ *
37
+ * A TAG CANNOT BE EDITED. GitHub's web editor commits to a branch, so
38
+ * `/edit/v0.8.0/…` is a 404 for anyone signed in — and a version served from a
39
+ * tag is exactly the case this layer creates. Sending that reader to the
40
+ * default branch instead would be worse than the 404: they would be editing
41
+ * today's documentation while believing they were fixing the version they were
42
+ * reading.
43
+ *
44
+ * So a tag gets the source, not a form. The caller draws the label the kind
45
+ * asks for.
46
+ *
47
+ * GitHub and GitLab spell both forms differently and every other host is left
48
+ * alone — a wrong link is worse than none, so no button is drawn at all.
49
+ */
50
+ export interface SourceLink {
51
+ url: string;
52
+ /** `edit` opens a form; `view` opens the file as it stands. */
53
+ kind: 'edit' | 'view';
54
+ }
55
+
56
+ export function sourceLink(
57
+ repositoryUrl: string | undefined,
58
+ ref: string | undefined,
59
+ refKind: 'branch' | 'tag' | undefined,
60
+ file: string
61
+ ): SourceLink | undefined {
62
+ if (!repositoryUrl) return undefined;
63
+
64
+ const base = repositoryUrl.replace(/\.git$/, '').replace(/\/+$/, '');
65
+
66
+ // Without a ref there is nothing to open a form against either: `HEAD` is a
67
+ // symbolic name the editor does not accept, though it reads fine.
68
+ const editable = refKind !== 'tag' && Boolean(ref);
69
+ const at = ref || 'HEAD';
70
+ const kind = editable ? 'edit' : 'view';
71
+
72
+ if (base.includes('gitlab')) {
73
+ return {
74
+ url: `${base}/-/${editable ? 'edit' : 'blob'}/${at}/${file}`,
75
+ kind
76
+ };
77
+ }
78
+
79
+ if (base.includes('github')) {
80
+ return { url: `${base}/${editable ? 'edit' : 'blob'}/${at}/${file}`, kind };
81
+ }
82
+
83
+ return undefined;
84
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Which versions the switcher may offer.
3
+ *
4
+ * Lifted out of `DuxtVersion` because two components need the answer and only
5
+ * one of them draws the control: the mobile sheet renders a full-width select
6
+ * where there is something to choose and a plain badge beside the brand where
7
+ * there is not, and it has to know which before it decides where the version
8
+ * goes at all.
9
+ *
10
+ * Pure, and beside the resolver's other path arithmetic for the same reason the
11
+ * rest of it is: every version bug this layer has had came from prefix work
12
+ * done inline in a component, where it could only be checked by clicking.
13
+ */
14
+
15
+ /**
16
+ * Are these two collections versions of the SAME thing?
17
+ *
18
+ * A documentation tree and an API reference are both published per version, and
19
+ * neither is a version of the other — so a switcher that compared only the
20
+ * repository offered `v2 → /` and `v2 → /api` side by side, twice each label,
21
+ * and a canonical URL on a reference page pointed at the documentation's root.
22
+ *
23
+ * Compared by DECLARATION, which is the identity `resolveGeneratedSections`
24
+ * records for exactly this question. Two documentation trees answer `undefined`
25
+ * on both sides and are the same artefact, which is what keeps a site without a
26
+ * single generated section reading exactly as it did.
27
+ */
28
+ export const sameArtefact = (
29
+ a: DuxtResolvedSource | undefined,
30
+ b: DuxtResolvedSource | undefined
31
+ ) => a?.generated?.declaration === b?.generated?.declaration;
32
+
33
+ export function versionChoices(
34
+ sources: DuxtResolvedSource[],
35
+ current: DuxtResolvedSource | undefined,
36
+ configured: DuxtLink[] | undefined
37
+ ): DuxtLink[] {
38
+ // A version-neutral generated section suppresses the control entirely, and
39
+ // that is the point of the policy rather than a tidy-up: a changelog is one
40
+ // global history, so every entry the switcher could offer would move the
41
+ // reader to a URL that section does not serve.
42
+ if (current?.generated?.versioning === 'global') return [];
43
+
44
+ // `versions` in the config still wins where a label needs to read differently
45
+ // from the URL segment.
46
+ if (configured?.length) return configured;
47
+
48
+ return sources
49
+ .filter(
50
+ (source) =>
51
+ source.version &&
52
+ source.repo === current?.repo &&
53
+ sameArtefact(source, current)
54
+ )
55
+ .map((source) => ({
56
+ label: source.version!,
57
+ to: source.prefix || '/',
58
+ // The lifecycle, not just "is this the default": a list where three
59
+ // entries look alike says nothing about which of them is still safe to
60
+ // read. `current` needs no word — it is what the reader assumes.
61
+ description:
62
+ source.status && source.status !== 'current'
63
+ ? `duxt.version.status.${source.status}`
64
+ : source.isDefault
65
+ ? 'default'
66
+ : undefined
67
+ }));
68
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Which source serves a path, and how to move a path between versions.
3
+ *
4
+ * Pure functions, deliberately: every version bug this layer has had came from
5
+ * prefix arithmetic done inline in a component, where it could only be checked
6
+ * by clicking. `/workflows/v0.7.0` starts with `/workflows` as well, so taking
7
+ * the *first* matching prefix said "main" while reading the tag — and swapping
8
+ * versions then appended instead of replacing, turning /workflows/v0.7.0 into
9
+ * /workflows/v0.7.0/v0.7.0 on every click.
10
+ */
11
+
12
+ /** The source whose prefix the path is inside. Longest prefix wins. */
13
+ export function sourceForPath<T extends { prefix: string }>(
14
+ path: string,
15
+ sources: T[]
16
+ ): T | undefined {
17
+ return (
18
+ [...sources]
19
+ .sort((a, b) => b.prefix.length - a.prefix.length)
20
+ .find((source) => !source.prefix || isInside(path, source.prefix)) ??
21
+ sources.find((source) => !source.prefix)
22
+ );
23
+ }
24
+
25
+ /**
26
+ * Is `path` inside `prefix`?
27
+ *
28
+ * Segment-aware: `/workflows-old` is not inside `/workflows`, though it starts
29
+ * with it.
30
+ */
31
+ export function isInside(path: string, prefix: string): boolean {
32
+ if (!prefix) return true;
33
+
34
+ return path === prefix || path.startsWith(`${prefix}/`);
35
+ }
36
+
37
+ /**
38
+ * The same page in another version.
39
+ *
40
+ * Strips the current version's prefix and applies the target's, so a page keeps
41
+ * its place instead of dropping the reader at the version's root.
42
+ */
43
+ export function versionPath(
44
+ path: string,
45
+ from: string | undefined,
46
+ to: string
47
+ ): string {
48
+ const rest =
49
+ from && from !== '/' && isInside(path, from)
50
+ ? path.slice(from.length)
51
+ : path;
52
+ const target = to === '/' ? '' : to;
53
+
54
+ return `${target}${rest}` || '/';
55
+ }
56
+
57
+ /**
58
+ * Re-exported so the app can auto-import it.
59
+ *
60
+ * The function itself lives beside the resolver, because that is where the
61
+ * semver comparison it uses lives and where its tests are. `app/utils` is what
62
+ * Nuxt scans for auto-imports, and `sources-resolve.ts` is plain logic with no
63
+ * Node imports — the same property that lets `app.config.ts` read it.
64
+ */
65
+ export { versionRelation } from '../../sources-resolve';
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import { createJiti } from 'jiti';
3
+
4
+ /**
5
+ * The report, as a command.
6
+ *
7
+ * PLAIN JAVASCRIPT, and it has to be: Node refuses to strip types from a file
8
+ * under `node_modules`, so a TypeScript entry point runs in this repository —
9
+ * where the package is a symlink — and dies on the first real install with
10
+ * `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. That was measured against a
11
+ * packed tarball, not assumed.
12
+ *
13
+ * jiti is what loads the TypeScript behind it, the same way
14
+ * `duxt-app-config.ts` already reads a site's config, so the rest of the layer
15
+ * needs no build step to be reachable from here.
16
+ *
17
+ * Everything the command decides is in `report.ts`, where it is typed and
18
+ * tested. This file exists to have no logic in it.
19
+ */
20
+ // Content's cache is read through `node:sqlite`, which announces itself as
21
+ // experimental on every run. True, and not this command's news to break to
22
+ // anybody: the report is a thing you pipe into a file or paste into a chat, and
23
+ // a warning about somebody else's implementation detail is noise in both.
24
+ process.removeAllListeners('warning');
25
+
26
+ const jiti = createJiti(import.meta.url, { interopDefault: true });
27
+
28
+ const { duxtReport, runDuxtReportCli } = await jiti.import('../report');
29
+
30
+ try {
31
+ const { output, exitCode } = runDuxtReportCli(
32
+ process.argv.slice(2),
33
+ duxtReport()
34
+ );
35
+
36
+ console.log(output);
37
+ process.exitCode = exitCode;
38
+ } catch (error) {
39
+ // The message, not the stack. Everything thrown on this path is a statement
40
+ // about the SITE — an artefact that is not where the config says, a section
41
+ // option nobody knows — written to be read by whoever wrote that config, and
42
+ // a trace through jiti's loader helps none of them.
43
+ console.error(error instanceof Error ? error.message : String(error));
44
+ process.exitCode = 1;
45
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://shadcn-vue.com/schema.json",
3
+ "style": "new-york",
4
+ "typescript": true,
5
+ "tailwind": {
6
+ "config": "",
7
+ "css": "app/assets/css/duxt.css",
8
+ "baseColor": "neutral",
9
+ "cssVariables": true
10
+ },
11
+ "aliases": {
12
+ "components": "@duxt/components",
13
+ "composables": "@duxt/composables",
14
+ "utils": "@duxt/lib/utils",
15
+ "ui": "@duxt/components/ui",
16
+ "lib": "@duxt/lib"
17
+ },
18
+ "iconLibrary": "lucide"
19
+ }
@@ -0,0 +1,193 @@
1
+ import { existsSync, mkdirSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import type { Nuxt } from '@nuxt/schema';
5
+
6
+ /**
7
+ * Every parsed page, out of Content's own cache.
8
+ *
9
+ * The one place the build can see what Content actually produced. NOT the
10
+ * `content:file:afterParse` hook, which was the obvious choice and is the wrong
11
+ * one: Content skips the parse for every file whose checksum is unchanged, so
12
+ * on the second build that hook fires for nothing and a validator built on it
13
+ * reports an empty site. The cache holds the parsed document either way, remote
14
+ * sources included — Content downloads a repository into `.data/content/` and
15
+ * nothing else on disk describes what came back.
16
+ *
17
+ * Returns undefined rather than an empty list when there is no cache to read.
18
+ * A `nuxt prepare` run parses nothing, and "no pages" there is not a finding.
19
+ */
20
+ export interface CachedPage {
21
+ collection: string;
22
+ path: string;
23
+ file: string;
24
+ content: Record<string, unknown>;
25
+ }
26
+
27
+ /**
28
+ * Where Content keeps its parse cache.
29
+ *
30
+ * Read from the runtime config where Content has already put it there, and
31
+ * from Content's own default where it has not — which is the case for anything
32
+ * running before Content's setup, `enableWriteAheadLog` included.
33
+ */
34
+ function cacheFile(nuxt: Nuxt): string {
35
+ const configured = (
36
+ nuxt.options.runtimeConfig as {
37
+ content?: { localDatabase?: { filename?: string } };
38
+ }
39
+ ).content?.localDatabase?.filename;
40
+
41
+ return configured && existsSync(configured)
42
+ ? configured
43
+ : join(nuxt.options.rootDir, '.data/content/contents.sqlite');
44
+ }
45
+
46
+ export function readContentCache(
47
+ nuxt: Nuxt,
48
+ collections: Iterable<string>
49
+ ): CachedPage[] | undefined {
50
+ return readContentCacheAt(cacheFile(nuxt), collections);
51
+ }
52
+
53
+ /** Content's default location for that cache, for a reader with no Nuxt. */
54
+ export const contentCacheFile = (rootDir: string) =>
55
+ join(rootDir, '.data/content/contents.sqlite');
56
+
57
+ /**
58
+ * The same read, addressed by FILE rather than by a Nuxt instance.
59
+ *
60
+ * Split out for `report.ts`, which runs as a command beside the build rather
61
+ * than inside it and has no `nuxt` to be handed. Everything below was already
62
+ * a function of the path alone; only the path was not reachable.
63
+ */
64
+ export function readContentCacheAt(
65
+ file: string,
66
+ collections: Iterable<string>
67
+ ): CachedPage[] | undefined {
68
+ if (!existsSync(file)) return undefined;
69
+
70
+ const names = new Set(collections);
71
+ const pages: CachedPage[] = [];
72
+
73
+ /**
74
+ * WAIT for Content rather than failing next to it.
75
+ *
76
+ * SQLite's default journal mode locks the whole FILE while a writer holds it,
77
+ * so a reader gets SQLITE_BUSY immediately — `readOnly` does not help, it is
78
+ * the writer that excludes us. Content writes its parse cache from a
79
+ * `Promise.all` over the collections, and the build's own hooks read that
80
+ * cache; with a handful of files the write is over before anyone reads, and
81
+ * at six hundred it is not. The result was `database is locked` from
82
+ * whichever module read first, on a site whose only sin was having enough
83
+ * pages.
84
+ *
85
+ * A busy timeout turns the race into a wait: the reader retries internally
86
+ * until the writer commits. Fifteen seconds is far longer than a parse pass
87
+ * needs and still fails loudly if something genuinely holds the file — a
88
+ * stray dev server, say.
89
+ */
90
+ const database = new DatabaseSync(file, {
91
+ readOnly: true,
92
+ timeout: 15_000
93
+ });
94
+
95
+ try {
96
+ const rows = database
97
+ .prepare('SELECT id, value FROM _development_cache')
98
+ .all() as { id: string; value: string }[];
99
+
100
+ for (const row of rows) {
101
+ const collection = row.id.split('/')[0] ?? '';
102
+ // The cache carries a version marker and, after a config change, rows of
103
+ // collections that no longer exist.
104
+ if (!names.has(collection)) continue;
105
+
106
+ let content: Record<string, unknown>;
107
+ try {
108
+ content = JSON.parse(row.value) as Record<string, unknown>;
109
+ } catch {
110
+ continue;
111
+ }
112
+
113
+ const path = typeof content.path === 'string' ? content.path : undefined;
114
+ if (!path) continue;
115
+
116
+ pages.push({
117
+ collection,
118
+ path,
119
+ file: typeof content.id === 'string' ? content.id : row.id,
120
+ content
121
+ });
122
+ }
123
+ } finally {
124
+ database.close();
125
+ }
126
+
127
+ return pages;
128
+ }
129
+
130
+ /**
131
+ * Put Content's cache database into WAL mode, before Content opens it.
132
+ *
133
+ * The failure this removes: a `nuxt build` the kernel or a Ctrl-C left behind
134
+ * keeps `contents.sqlite` open, and every following build then fails with
135
+ * `database is locked` — a message naming neither the process nor the file.
136
+ * The busy timeout above covers the layer's own reader; Content's cache WRITER
137
+ * has no such patience, and a build cannot ask it for any.
138
+ *
139
+ * SQLite's default rollback journal locks the whole FILE while a writer holds
140
+ * it, so any reader is excluded outright. Write-ahead logging lets readers and
141
+ * one writer coexist, which is the actual shape of a build: Content writing the
142
+ * parse cache while this layer's modules read it. Measured on this repo's own
143
+ * site while translating — 237 lock errors went to zero.
144
+ *
145
+ * The mode lives in the FILE HEADER, not in a connection, so setting it once
146
+ * holds for every process that opens the file afterwards — Content's included,
147
+ * which is why this can be done from a module and does not need a patch.
148
+ *
149
+ * Creates the file when it is not there yet, so a first build gets WAL too.
150
+ * Safe: Content opens the path either way and probes for its cache table with
151
+ * a `SELECT` in a try/catch, so an empty database is the same to it as no file.
152
+ *
153
+ * Best-effort throughout. A database that cannot be opened, a filesystem that
154
+ * does not support WAL (a network mount is the usual one) and a read-only
155
+ * checkout each leave the build exactly as it was before this ran.
156
+ */
157
+ export function enableWriteAheadLog(nuxt: Nuxt): 'wal' | 'skipped' {
158
+ const file = cacheFile(nuxt);
159
+
160
+ /**
161
+ * NEVER CREATE THE FILE. `new DatabaseSync` on a missing path writes an empty
162
+ * 4 kB database, and Content then finds a file where it expected none and
163
+ * never creates its schema — every query afterwards dies with `no such table:
164
+ * _development_cache`.
165
+ *
166
+ * Invisible on any machine that has built once, because the file is there by
167
+ * then; it only bites a FRESH checkout, which is to say CI and every new
168
+ * contributor. Skipping here costs the first build its WAL and nothing else:
169
+ * that build has no cache to read anyway, and the second one finds the file
170
+ * and switches it over.
171
+ */
172
+ if (!existsSync(file)) return 'skipped';
173
+
174
+ try {
175
+ mkdirSync(dirname(file), { recursive: true });
176
+
177
+ const database = new DatabaseSync(file, { timeout: 15_000 });
178
+
179
+ try {
180
+ const [row] = database.prepare('PRAGMA journal_mode = WAL').all() as {
181
+ journal_mode?: string;
182
+ }[];
183
+
184
+ return row?.journal_mode?.toLowerCase() === 'wal' ? 'wal' : 'skipped';
185
+ } finally {
186
+ database.close();
187
+ }
188
+ } catch {
189
+ // Nothing here is worth failing a build over: the busy timeout on the
190
+ // reader is what this improves upon, not what it replaces.
191
+ return 'skipped';
192
+ }
193
+ }