@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,318 @@
1
+ <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
3
+ <title>duxt — Versions</title><style>
4
+ :root {
5
+ color-scheme: light dark;
6
+ --bg: #fff;
7
+ --fg: #12141a;
8
+ --muted: #6b7280;
9
+ --faint: #9ca3af;
10
+ --line: #e8e8ea;
11
+ --raised: #f6f6f7;
12
+ --accent: #00a86b;
13
+ --accent-soft: #e6f7f0;
14
+ --warn: #b45309;
15
+ --error: #b91c1c;
16
+ --ok: #15803d;
17
+ --radius: 7px;
18
+ }
19
+ @media (prefers-color-scheme: dark) {
20
+ :root:not([data-theme='light']) {
21
+ --bg: #0f1115;
22
+ --fg: #e8e8ea;
23
+ --muted: #9099a8;
24
+ --faint: #5b6472;
25
+ --line: #23262d;
26
+ --raised: #171a20;
27
+ --accent: #00dc82;
28
+ --accent-soft: #10291f;
29
+ --warn: #f59e0b;
30
+ --error: #f87171;
31
+ --ok: #4ade80;
32
+ }
33
+ }
34
+ :root[data-theme='dark'] {
35
+ --bg: #0f1115;
36
+ --fg: #e8e8ea;
37
+ --muted: #9099a8;
38
+ --faint: #5b6472;
39
+ --line: #23262d;
40
+ --raised: #171a20;
41
+ --accent: #00dc82;
42
+ --accent-soft: #10291f;
43
+ --warn: #f59e0b;
44
+ --error: #f87171;
45
+ --ok: #4ade80;
46
+ }
47
+
48
+ * { box-sizing: border-box; }
49
+ body {
50
+ margin: 0;
51
+ padding: 0 0 40px;
52
+ font: 13px/1.55 ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
53
+ color: var(--fg);
54
+ background: var(--bg);
55
+ -webkit-font-smoothing: antialiased;
56
+ }
57
+ a { color: inherit; }
58
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
59
+
60
+ header.bar {
61
+ position: sticky; top: 0; z-index: 2;
62
+ background: color-mix(in srgb, var(--bg) 88%, transparent);
63
+ backdrop-filter: blur(8px);
64
+ border-bottom: 1px solid var(--line);
65
+ padding: 10px 16px 0;
66
+ }
67
+ nav { display: flex; gap: 1px; flex-wrap: wrap; }
68
+ nav a {
69
+ padding: 5px 10px; border-radius: var(--radius) var(--radius) 0 0;
70
+ text-decoration: none; color: var(--muted); font-weight: 500;
71
+ border-bottom: 2px solid transparent; line-height: 1.4;
72
+ }
73
+ nav a:hover { color: var(--fg); background: var(--raised); }
74
+ nav a[aria-current] { color: var(--fg); border-bottom-color: var(--accent); font-weight: 600; }
75
+
76
+ main { padding: 18px 16px 0; max-width: 1400px; }
77
+ h1 { margin: 0 0 3px; font-size: 15px; font-weight: 650; letter-spacing: -0.01em; }
78
+ h2 {
79
+ margin: 26px 0 8px; font-size: 12px; font-weight: 600;
80
+ text-transform: uppercase; letter-spacing: .05em; color: var(--muted);
81
+ }
82
+ p.hint { margin: 0 0 16px; color: var(--muted); max-width: 78ch; }
83
+
84
+ .stats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
85
+ .stat {
86
+ display: flex; flex-direction: column; gap: 1px;
87
+ padding: 7px 12px; min-width: 78px;
88
+ border: 1px solid var(--line); border-radius: var(--radius); background: var(--raised);
89
+ }
90
+ .stat b { font-size: 16px; font-weight: 650; font-variant-numeric: tabular-nums; line-height: 1.2; }
91
+ .stat span { font-size: 10.5px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
92
+ .stat.ok b { color: var(--ok); }
93
+ .stat.warn b { color: var(--warn); }
94
+ .stat.error b { color: var(--error); }
95
+
96
+ .scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: 10px; }
97
+ table { width: 100%; border-collapse: collapse; }
98
+ th, td { padding: 7px 11px; text-align: left; vertical-align: top; }
99
+ /* Offset zero, not the height of the tab bar. .scroll sets overflow-x, which
100
+ computes overflow-y to auto as well, so the box — not the page — is what a
101
+ sticky heading offsets against. At 41px the heading sat 41px down from the
102
+ box's top edge, leaving a blank band above it AND covering the first row:
103
+ the Redirects panel counted eight rules and showed seven. */
104
+ thead th {
105
+ position: sticky; top: 0; z-index: 1;
106
+ background: var(--raised);
107
+ font-weight: 600; color: var(--muted);
108
+ font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em;
109
+ white-space: nowrap; border-bottom: 1px solid var(--line);
110
+ }
111
+ tbody tr { border-top: 1px solid var(--line); }
112
+ tbody tr:first-child { border-top: 0; }
113
+ tbody tr:hover { background: var(--raised); }
114
+ td.empty { color: var(--muted); padding: 16px 11px; text-align: center; }
115
+
116
+ code {
117
+ font: 11.5px/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
118
+ background: var(--raised); border: 1px solid var(--line);
119
+ padding: 0 4px; border-radius: 4px; white-space: nowrap;
120
+ }
121
+ a.file { text-decoration: none; border-bottom: 1px dashed var(--faint); }
122
+ a.file:hover { border-bottom-style: solid; color: var(--accent); }
123
+ .dim { color: var(--faint); }
124
+ .trunc {
125
+ display: inline-block; max-width: 34ch; overflow: hidden;
126
+ text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;
127
+ }
128
+ td.wide { min-width: 32ch; }
129
+
130
+ .tag {
131
+ display: inline-block; border-radius: 999px; padding: 1px 7px;
132
+ font-size: 10px; font-weight: 600; white-space: nowrap;
133
+ background: var(--accent-soft); color: var(--accent);
134
+ border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
135
+ }
136
+ .tag.warn { background: transparent; color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
137
+ .tag.error { background: transparent; color: var(--error); border-color: color-mix(in srgb, var(--error) 40%, transparent); }
138
+ .tag.ok { background: transparent; color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
139
+ .tag.muted { background: var(--raised); color: var(--muted); border-color: var(--line); }
140
+
141
+ form { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; align-items: center; }
142
+ input[type=text], input[type=search] {
143
+ flex: 1; min-width: 240px; padding: 6px 11px;
144
+ border: 1px solid var(--line); border-radius: var(--radius);
145
+ font: inherit; background: var(--bg); color: inherit;
146
+ }
147
+ .filterbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
148
+ .filterbar .count {
149
+ color: var(--muted); font-size: 11.5px;
150
+ font-variant-numeric: tabular-nums; white-space: nowrap;
151
+ }
152
+ input.filter {
153
+ flex: 0 1 340px; min-width: 0; margin: 0; padding-left: 31px;
154
+ background: var(--bg) no-repeat 10px 50% / 14px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239099a8' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.6-3.6'/%3E%3C/svg%3E");
155
+ }
156
+ input.filter:focus { border-color: var(--accent); }
157
+ input.filter::-webkit-search-cancel-button { cursor: pointer; }
158
+ input::placeholder { color: var(--faint); }
159
+ button {
160
+ padding: 6px 14px; border: 1px solid var(--line); border-radius: var(--radius);
161
+ background: var(--raised); font: inherit; font-weight: 500; cursor: pointer; color: inherit;
162
+ }
163
+ button:hover { border-color: var(--accent); color: var(--accent); }
164
+
165
+ ol.steps { margin: 0 0 18px; padding: 0; list-style: none; counter-reset: step; }
166
+ ol.steps li {
167
+ position: relative; counter-increment: step;
168
+ padding: 8px 0 8px 30px; border-left: 2px solid var(--line); margin-left: 9px;
169
+ }
170
+ ol.steps li::before {
171
+ content: counter(step);
172
+ position: absolute; left: -10px; top: 8px;
173
+ width: 18px; height: 18px; border-radius: 999px;
174
+ background: var(--raised); border: 1px solid var(--line);
175
+ font-size: 10px; font-weight: 600; color: var(--muted);
176
+ display: flex; align-items: center; justify-content: center;
177
+ }
178
+ ol.steps li:last-child { border-left-color: transparent; }
179
+
180
+ .note {
181
+ padding: 10px 13px; border-radius: var(--radius);
182
+ background: var(--raised); border: 1px solid var(--line);
183
+ color: var(--muted); margin-bottom: 16px; max-width: 90ch;
184
+ }
185
+ .note code { background: var(--bg); }
186
+
187
+ details { margin-bottom: 10px; }
188
+ details > summary {
189
+ cursor: pointer; padding: 7px 11px; border-radius: var(--radius);
190
+ background: var(--raised); border: 1px solid var(--line);
191
+ font-weight: 600; list-style-position: inside;
192
+ }
193
+ details > summary:hover { border-color: var(--accent); }
194
+ details[open] > summary { border-radius: var(--radius) var(--radius) 0 0; margin-bottom: -1px; }
195
+ details[open] > summary + .scroll { border-radius: 0 0 var(--radius) var(--radius); }
196
+ details[open] > summary + .filterbar {
197
+ margin: 0; padding: 8px 11px;
198
+ background: var(--raised); border: 1px solid var(--line); border-top: 0;
199
+ }
200
+ details[open] > .filterbar + .scroll {
201
+ border-radius: 0 0 var(--radius) var(--radius); margin-top: -1px;
202
+ }
203
+
204
+ /* A disclosure inside a table cell is not the panel-sized one above: no bar,
205
+ no border, no weight — just a triangle in front of the preview. */
206
+ details.json { margin: 0; }
207
+ details.json > summary {
208
+ padding: 0; background: none; border: 0; border-radius: 0; font-weight: inherit;
209
+ list-style-position: outside; margin-left: 12px;
210
+ }
211
+ details.json > summary::marker { color: var(--faint); }
212
+ details.json > summary:hover { border: 0; }
213
+ details.json[open] > summary { margin-bottom: 6px; border-radius: 0; }
214
+ details.json[open] > summary .peek { display: none; }
215
+ pre.json {
216
+ margin: 0 0 2px; padding: 9px 11px; max-height: 320px; overflow: auto;
217
+ background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
218
+ font: 11.5px/1.6 ui-monospace, 'SF Mono', Menlo, monospace;
219
+ white-space: pre; tab-size: 2;
220
+ }
221
+ .j-key { color: var(--accent); }
222
+ .j-num { color: var(--warn); }
223
+ .j-word { color: var(--muted); font-style: italic; }
224
+
225
+ .matrix th:first-child, .matrix td:first-child {
226
+ position: sticky; left: 0; background: var(--bg);
227
+ border-right: 1px solid var(--line);
228
+ }
229
+ .matrix thead th:first-child { background: var(--raised); }
230
+ .matrix tbody tr:hover td:first-child { background: var(--raised); }
231
+ .matrix td.mark { text-align: center; }
232
+ .matrix td.yes { color: var(--ok); font-weight: 700; }
233
+ .matrix td.no { color: var(--faint); }
234
+ </style></head><body>
235
+
236
+ <main>
237
+ <h1>Versions</h1>
238
+ <p class="hint">Which versions carry which page. The view the fallback is debugged from.</p>
239
+ <h2>acme/sdk <span class="tag muted">en</span></h2><div class="scroll"><table class="matrix">
240
+ <thead><tr><th scope="col">Page</th><th scope="col">v2 *</th><th scope="col">v1.9</th></tr></thead>
241
+ <tbody><tr><td><code>/</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/changelog</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/getting-started</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/getting-started/install</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/guides/caching</code></td><td class="mark yes">✓</td><td class="mark no">·</td></tr><tr><td><code>/guides/deploying</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/client</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/errors</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr></tbody>
242
+ </table></div><p class="hint">* the version served without a version segment.</p><h2>acme/sdk <span class="tag muted">de</span></h2><div class="scroll"><table class="matrix">
243
+ <thead><tr><th scope="col">Page</th><th scope="col">v2 *</th><th scope="col">v1.9</th></tr></thead>
244
+ <tbody><tr><td><code>/</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/getting-started</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/getting-started/install</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/guides/deploying</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/client</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/errors</code></td><td class="mark yes">✓</td><td class="mark no">·</td></tr></tbody>
245
+ </table></div><p class="hint">* the version served without a version segment.</p><h2>acme/sdk <span class="tag muted">v2</span></h2><div class="scroll"><table class="matrix">
246
+ <thead><tr><th scope="col">Page</th><th scope="col">en *</th><th scope="col">de</th></tr></thead>
247
+ <tbody><tr><td><code>/</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/changelog</code></td><td class="mark yes">✓</td><td class="mark no">·</td></tr><tr><td><code>/getting-started</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/getting-started/install</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/guides/caching</code></td><td class="mark yes">✓</td><td class="mark no">·</td></tr><tr><td><code>/guides/deploying</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/client</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/errors</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr></tbody>
248
+ </table></div><p class="hint">* the language served from the source path itself, without a folder.</p><h2>acme/sdk <span class="tag muted">v1.9</span></h2><div class="scroll"><table class="matrix">
249
+ <thead><tr><th scope="col">Page</th><th scope="col">en *</th><th scope="col">de</th></tr></thead>
250
+ <tbody><tr><td><code>/</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/changelog</code></td><td class="mark yes">✓</td><td class="mark no">·</td></tr><tr><td><code>/getting-started</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/getting-started/install</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/guides/deploying</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/client</code></td><td class="mark yes">✓</td><td class="mark yes">✓</td></tr><tr><td><code>/reference/errors</code></td><td class="mark yes">✓</td><td class="mark no">·</td></tr></tbody>
251
+ </table></div><p class="hint">* the language served from the source path itself, without a folder.</p>
252
+ </main>
253
+ <script>
254
+ document.addEventListener('input', (event) => {
255
+ const input = event.target;
256
+ if (!input.matches('[data-filter]')) return;
257
+
258
+ const term = input.value.trim().toLowerCase();
259
+ const scope = input.closest('details') ?? document;
260
+ const rows = scope.querySelectorAll('tbody tr');
261
+ let shown = 0;
262
+
263
+ for (const row of rows) {
264
+ row.hidden = Boolean(term) && !row.textContent.toLowerCase().includes(term);
265
+ if (!row.hidden) shown += 1;
266
+ }
267
+
268
+ const count = input.parentElement.querySelector('[data-filter-count]');
269
+ if (count) {
270
+ count.textContent = term
271
+ ? shown ? shown + ' of ' + rows.length : 'no matches'
272
+ : '';
273
+ }
274
+ });
275
+
276
+ // EVERY link and button, with no exception for the tab row — a preview has
277
+ // no tab row. Embedded in a documentation page, a reader who clicked one
278
+ // would land on another panel inside the frame while the prose around it went
279
+ // on explaining this one.
280
+ document.addEventListener('click', (event) => {
281
+ if (event.target.closest('a, button')) event.preventDefault();
282
+ });
283
+ document.addEventListener('submit', (event) => event.preventDefault());
284
+
285
+ // The page that framed this one owns the theme and the height: it knows
286
+ // which mode the reader picked, and only this document knows how tall it
287
+ // turned out. Two messages, one each way.
288
+ const theme = new URLSearchParams(location.search).get('theme');
289
+ if (theme) document.documentElement.dataset.theme = theme;
290
+
291
+ // Only from the frame that embedded this one, and only from its own origin.
292
+ // Both halves are needed: the origin alone would still accept a message from
293
+ // a sibling frame on the same site, and `event.source` alone would accept one
294
+ // from a parent on another origin. This document is served from `public/`, so
295
+ // the page framing it is always same-origin — a message from anywhere else is
296
+ // not a theme change, it is somebody else talking.
297
+ addEventListener('message', (event) => {
298
+ if (event.origin !== location.origin || event.source !== parent) return;
299
+
300
+ if (event.data && typeof event.data.duxtTheme === 'string') {
301
+ document.documentElement.dataset.theme = event.data.duxtTheme;
302
+ }
303
+ });
304
+
305
+ // Addressed rather than broadcast, for the same reason. `'*'` hands the
306
+ // height to whatever ends up framing this page, which on a consumer's site is
307
+ // not necessarily the page that asked for it.
308
+ const measure = () => parent.postMessage(
309
+ { duxtHeight: document.documentElement.scrollHeight },
310
+ location.origin
311
+ );
312
+
313
+ addEventListener('load', measure);
314
+ addEventListener('resize', measure);
315
+ document.addEventListener('toggle', measure, true);
316
+ document.addEventListener('input', () => setTimeout(measure, 0));
317
+ </script>
318
+ </body></html>
package/report.ts ADDED
@@ -0,0 +1,303 @@
1
+ import { join } from 'node:path';
2
+ import { contentCacheFile, readContentCacheAt } from './content-cache';
3
+ import { readDuxtBuildConfig } from './duxt-app-config';
4
+ import { duxtManifest, duxtSectionTypes } from './sections-resolve';
5
+ import { readSectionReports } from './section-reports';
6
+ import { resolveLatestRefs } from './sources-git';
7
+ import { redirectRules } from './modules/redirects';
8
+ import type { Collected, PageRecord } from './validate-report';
9
+ import { report, walk } from './validate-report';
10
+ import type { DuxtResolvedSource } from './sources-resolve';
11
+
12
+ /**
13
+ * The devtools tab's answers, as text a terminal — or a model — can read.
14
+ *
15
+ * The panels are HTML behind a route that exists only inside a dev server, and
16
+ * the validator's findings are lines in a build log that has scrolled away.
17
+ * Neither form survives being handed to somebody: "what is wrong with my
18
+ * documentation" had no answer that fits in a paste. This is that answer, and
19
+ * it is deliberately the SAME data rather than a second opinion — the manifest
20
+ * comes from `duxtManifest`, the findings from `report`, the redirects from
21
+ * `redirectRules`, exactly as the build and the panels take them.
22
+ *
23
+ * NO SERVER, and that is what makes it usable in CI and in a pipe. Everything
24
+ * here is a file read: the site's `app.config.ts` through jiti, the artefacts
25
+ * off disk, and the pages out of Content's parse cache — which is on disk after
26
+ * any build or dev run, remote sources included.
27
+ *
28
+ * What it therefore CANNOT show is the three panels that ask a running server:
29
+ * the search index, the path debugger and the cache directory's own listing.
30
+ * Those are questions about a process, and a command that answered them from
31
+ * the outside would be guessing.
32
+ */
33
+ export interface DuxtReportOptions {
34
+ /** The site being reported on — where `app.config.ts` and `.data/` live. */
35
+ rootDir?: string;
36
+ }
37
+
38
+ export interface DuxtReport {
39
+ rootDir: string;
40
+ sources: DuxtResolvedSource[];
41
+ /** Undefined when nothing has parsed this site yet — see `pages`. */
42
+ pages: PageRecord[] | undefined;
43
+ findings: { errors: string[]; warnings: string[]; notes: string[] };
44
+ redirects: { from: string; to: string }[];
45
+ locales: string[];
46
+ }
47
+
48
+ /** Everything the report says, before anything decides how to say it. */
49
+ export function duxtReport(options: DuxtReportOptions = {}): DuxtReport {
50
+ const rootDir = options.rootDir ?? process.cwd();
51
+
52
+ const config = readDuxtBuildConfig([rootDir, join(rootDir, 'app')]);
53
+ const types = duxtSectionTypes(config?.sectionTypes);
54
+
55
+ const sources = readSectionReports(
56
+ duxtManifest(
57
+ resolveLatestRefs(config?.sources ?? [{ path: 'docs' }]),
58
+ config?.sourceOptions ?? {},
59
+ types
60
+ ),
61
+ types
62
+ );
63
+
64
+ const locales = [
65
+ ...new Set(
66
+ sources
67
+ .map((source) => source.locale)
68
+ .filter((locale): locale is string => Boolean(locale))
69
+ )
70
+ ];
71
+
72
+ const cached = readContentCacheAt(
73
+ contentCacheFile(rootDir),
74
+ sources.map((source) => source.collection)
75
+ );
76
+
77
+ const pages = cached?.map((entry) => {
78
+ const collected: Collected = {
79
+ anchors: new Set<string>(),
80
+ links: [],
81
+ commands: []
82
+ };
83
+ walk(entry.content.body, collected);
84
+
85
+ const text = (key: string) =>
86
+ typeof entry.content[key] === 'string'
87
+ ? (entry.content[key] as string)
88
+ : undefined;
89
+
90
+ return {
91
+ collection: entry.collection,
92
+ path: entry.path,
93
+ file: entry.file,
94
+ title: text('title'),
95
+ description: text('description'),
96
+ anchors: collected.anchors,
97
+ links: collected.links,
98
+ commands: collected.commands,
99
+ lastUpdated: text('lastUpdated')
100
+ } satisfies PageRecord;
101
+ });
102
+
103
+ const byCollection = new Map(
104
+ sources.map((source) => [source.collection, source.prefix])
105
+ );
106
+
107
+ const rules = redirectRules(
108
+ (cached ?? [])
109
+ .filter((entry) => Array.isArray(entry.content.redirectFrom))
110
+ .map((entry) => ({
111
+ path: entry.path,
112
+ prefix: byCollection.get(entry.collection) ?? '',
113
+ redirectFrom: entry.content.redirectFrom as string[]
114
+ })),
115
+ locales
116
+ );
117
+
118
+ return {
119
+ rootDir,
120
+ sources,
121
+ pages,
122
+ // Over the pages there ARE. A site nobody has built yet is reported as
123
+ // exactly that further down, rather than as a site whose every collection
124
+ // is empty — which is what running the checks over nothing would say.
125
+ findings: pages
126
+ ? report(sources, pages)
127
+ : { errors: [], warnings: [], notes: [] },
128
+ redirects: Object.entries(rules).map(([from, rule]) => ({
129
+ from,
130
+ to: rule.redirect.to
131
+ })),
132
+ locales
133
+ };
134
+ }
135
+
136
+ /* -------------------------------------------------------------------------- */
137
+ /* Markdown */
138
+ /* -------------------------------------------------------------------------- */
139
+
140
+ /**
141
+ * The report as Markdown.
142
+ *
143
+ * Markdown rather than a drawn table, because the reader this exists for is as
144
+ * likely to be a model as a person, and a pipe-table is the one tabular form
145
+ * both read without ceremony. Nothing is truncated: a report that hides its
146
+ * long tail is a report you cannot act on, and the whole point is to paste it
147
+ * somewhere and ask.
148
+ */
149
+ /**
150
+ * The ref a source was read at.
151
+ *
152
+ * The kind is only printed when there IS one: a source that names its origin
153
+ * without saying whether it is a branch or a tag reaches the manifest with a
154
+ * `ref` and no `refKind`, and the obvious template then prints
155
+ * `main (undefined)`.
156
+ */
157
+ /**
158
+ * A figure and the thing it counts, in the number the figure asks for.
159
+ *
160
+ * Written inline four times it read `1 collections, 0 versions, 1 languages` —
161
+ * the kind of wrongness a reader stops trusting the rest of a report over.
162
+ */
163
+ const count = (many: number, noun: string) =>
164
+ `${many} ${noun}${many === 1 ? '' : 's'}`;
165
+
166
+ const ref = (source: DuxtResolvedSource) =>
167
+ source.ref
168
+ ? `${source.ref}${source.refKind ? ` (${source.refKind})` : ''}`
169
+ : 'checkout';
170
+
171
+ export function duxtReportMarkdown(data: DuxtReport): string {
172
+ const { sources, pages, findings } = data;
173
+ const out: string[] = [`# duxt report`, '', `Site: \`${data.rootDir}\``, ''];
174
+
175
+ const generated = sources.filter((source) => source.generated);
176
+ const versions = new Set(
177
+ sources
178
+ .filter((source) => source.version)
179
+ .map((source) => `${source.repo ?? ''}\u0000${source.version}`)
180
+ );
181
+
182
+ // A site with no `locales` has ONE language — the one it is written in — and
183
+ // reaches here with an empty list rather than a list of one.
184
+ const languages = data.locales.length || 1;
185
+
186
+ out.push(
187
+ '## Summary',
188
+ '',
189
+ `- ${count(sources.length, 'collection')}, ` +
190
+ `${count(versions.size, 'version')}, ${count(languages, 'language')}`,
191
+ `- ${count(generated.length, 'generated section')}`,
192
+ pages
193
+ ? `- ${pages.length} pages parsed`
194
+ : '- **no parse cache found** — run a build or a dev server first; ' +
195
+ 'everything below that needs pages is missing, not empty',
196
+ `- ${findings.errors.length} errors, ${findings.warnings.length} warnings`,
197
+ `- ${data.redirects.length} redirects from \`redirectFrom\``,
198
+ ''
199
+ );
200
+
201
+ out.push(
202
+ '## Sources',
203
+ '',
204
+ '| Prefix | Collection | Kind | Repository | Folder / artefact | Ref | Version | Status | Pages |',
205
+ '| :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | --: |'
206
+ );
207
+
208
+ const counted = new Map<string, number>();
209
+ for (const page of pages ?? []) {
210
+ counted.set(page.collection, (counted.get(page.collection) ?? 0) + 1);
211
+ }
212
+
213
+ for (const source of sources) {
214
+ out.push(
215
+ `| \`${source.prefix || '/'}\` | \`${source.collection}\` | ` +
216
+ `${source.generated ? source.generated.type : 'docs'}` +
217
+ `${source.locale ? ` · ${source.locale}` : ''} | ` +
218
+ `${source.repository ?? 'this repository'} | \`${source.path}\` | ` +
219
+ `${ref(source)} | ` +
220
+ `${source.version ?? '—'}${source.isDefault ? ' *default*' : ''} | ` +
221
+ `${source.status} | ${pages ? (counted.get(source.collection) ?? 0) : '?'} |`
222
+ );
223
+ }
224
+
225
+ out.push('');
226
+
227
+ for (const [heading, list] of [
228
+ ['Errors', findings.errors],
229
+ ['Warnings', findings.warnings]
230
+ ] as const) {
231
+ if (!list.length) continue;
232
+
233
+ out.push(`## ${heading}`, '');
234
+ for (const finding of list) out.push(`- ${finding}`);
235
+ out.push('');
236
+ }
237
+
238
+ if (!findings.errors.length && !findings.warnings.length && pages) {
239
+ out.push('## Findings', '', 'None.', '');
240
+ }
241
+
242
+ if (findings.notes.length) {
243
+ out.push('## Translations', '');
244
+ for (const note of findings.notes) {
245
+ // A note indented by two spaces belongs under the line above it — the
246
+ // report is one summary per language, then the files behind the figure.
247
+ out.push(note.startsWith(' ') ? ` - ${note.trim()}` : `- ${note}`);
248
+ }
249
+ out.push('');
250
+ }
251
+
252
+ if (data.redirects.length) {
253
+ out.push('## Redirects', '', '| From | To |', '| :-- | :-- |');
254
+ for (const rule of data.redirects) {
255
+ out.push(`| \`${rule.from}\` | \`${rule.to}\` |`);
256
+ }
257
+ out.push('');
258
+ }
259
+
260
+ return out.join('\n');
261
+ }
262
+
263
+ /* -------------------------------------------------------------------------- */
264
+ /* The command */
265
+ /* -------------------------------------------------------------------------- */
266
+
267
+ /**
268
+ * The command, as a function of its arguments and its data.
269
+ *
270
+ * HERE rather than in the bin, and that is not tidiness. The bin has to be
271
+ * plain JavaScript — Node refuses to strip types from a file under
272
+ * `node_modules`, so a `.ts` entry point works in this workspace, where the
273
+ * package is a symlink, and fails on the first real install with
274
+ * `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. Everything the command decides
275
+ * therefore lives on this side, where it is typed and tested, and the bin is
276
+ * left with nothing to get wrong.
277
+ *
278
+ * Returns the exit code rather than setting it: warnings do not fail, because
279
+ * they are what somebody else's repository going stale looks like, and a check
280
+ * that cannot survive that is a check that gets switched off.
281
+ */
282
+ export function runDuxtReportCli(
283
+ argv: string[],
284
+ data: DuxtReport
285
+ ): { output: string; exitCode: number } {
286
+ const output = argv.includes('--json')
287
+ ? JSON.stringify(
288
+ {
289
+ ...data,
290
+ // A Set does not survive `JSON.stringify` — it serialises as `{}`,
291
+ // which is a silently empty anchor list rather than an error.
292
+ pages: data.pages?.map((page) => ({
293
+ ...page,
294
+ anchors: [...page.anchors]
295
+ }))
296
+ },
297
+ undefined,
298
+ 2
299
+ )
300
+ : duxtReportMarkdown(data);
301
+
302
+ return { output, exitCode: data.findings.errors.length ? 1 : 0 };
303
+ }
@@ -0,0 +1,26 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+
4
+ /**
5
+ * Walk up to the repository root, so `docs/` resolves there and not in a
6
+ * subfolder.
7
+ *
8
+ * A FILE OF ITS OWN, for eight lines, because of what importing it drags along.
9
+ * It lived in `sources.ts`, which imports `@nuxt/content` and `@nuxtjs/sitemap`
10
+ * — types that resolve inside the Content config and not inside a Nuxt module,
11
+ * so a module reaching for this function pulled that whole graph into a
12
+ * typecheck it does not belong to. Node-only either way, and therefore still
13
+ * not beside the resolver: that half is read by `app.config.ts` and bundled for
14
+ * the browser.
15
+ */
16
+ export function repositoryRoot(): string {
17
+ let dir = process.cwd();
18
+
19
+ for (;;) {
20
+ if (existsSync(join(dir, '.git'))) return dir;
21
+
22
+ const parent = dirname(dir);
23
+ if (parent === dir) return process.cwd();
24
+ dir = parent;
25
+ }
26
+ }