@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,349 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { dirname, relative, sep } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import type { Nuxt } from '@nuxt/schema';
5
+ import { readDuxtBuildConfig } from '../duxt-app-config';
6
+ import { duxtManifest, duxtSectionTypes } from '../sections-resolve';
7
+ import { resolveLatestRefs } from '../sources-git';
8
+
9
+ /**
10
+ * "Last updated" and the contributor list, from the git history the file
11
+ * already has.
12
+ *
13
+ * Neither needs config: the `sources` entry names the repository, the ref and
14
+ * the folder, and the page names the file.
15
+ *
16
+ * A REMOTE source needs one thing more. Content clones it, but with
17
+ * `--depth 1`: the checkout on disk holds a single commit, so every file in it
18
+ * looks as if it were written by whoever cut the tip. That is wrong data, not
19
+ * missing data, and worse than nothing — so a remote source is read only when
20
+ * it says `history: true`, and the clone is unshallowed once before it is.
21
+ * A source read off disk is a full checkout already and needs no flag.
22
+ *
23
+ * Hooked on the parse, not on the build, so the answer is cached exactly as
24
+ * long as it is true: Content re-parses a file when its content changes, which
25
+ * is the same moment its last-modified date does.
26
+ */
27
+ interface AfterParseContext {
28
+ collection?: { name?: string };
29
+ content?: Record<string, unknown>;
30
+ file?: { path?: string; id?: string };
31
+ }
32
+
33
+ export interface DuxtContributor {
34
+ name: string;
35
+ commits: number;
36
+ username?: string;
37
+ }
38
+
39
+ export default function duxtGitMeta(_options: unknown, nuxt: Nuxt) {
40
+ const layerDir = fileURLToPath(new URL('..', import.meta.url));
41
+
42
+ const dirs = [
43
+ ...nuxt.options._layers.flatMap((entry) => [
44
+ entry.config.rootDir,
45
+ entry.config.srcDir
46
+ ]),
47
+ layerDir
48
+ ].filter(Boolean) as string[];
49
+
50
+ const config = readDuxtBuildConfig(dirs);
51
+ const sources = duxtManifest(
52
+ resolveLatestRefs(config?.sources ?? [{ path: 'docs' }]),
53
+ config?.sourceOptions ?? {},
54
+ duxtSectionTypes(config?.sectionTypes)
55
+ );
56
+
57
+ const wanted = new Map(
58
+ sources.map((source) => [source.collection, source.history])
59
+ );
60
+
61
+ nuxt.hook(
62
+ 'content:file:afterParse' as never,
63
+ ((ctx: AfterParseContext) => {
64
+ const file = ctx.file?.path;
65
+ const content = ctx.content;
66
+ if (!file || !content) return;
67
+
68
+ // A source that has not asked for its history is left alone — for a
69
+ // downloaded one that would otherwise answer out of a single-commit
70
+ // clone, which is wrong data rather than missing data.
71
+ if (!wanted.get(ctx.collection?.name ?? '')) return;
72
+
73
+ if (file.includes('/.data/content/')) unshallow(dirname(file));
74
+
75
+ const log = gitLog(file);
76
+ if (!log.length) return;
77
+
78
+ content.lastUpdated ??= log[0]!.date;
79
+ content.contributors ??= contributorsOf(log);
80
+ }) as never
81
+ );
82
+ }
83
+
84
+ interface Commit {
85
+ date: string;
86
+ name: string;
87
+ email: string;
88
+ }
89
+
90
+ /**
91
+ * Turn a `--depth 1` clone into one with a history, once per repository.
92
+ *
93
+ * Cached by the repository's own root rather than by the directory the file
94
+ * sits in: a fetch per page would download the same history once per page.
95
+ * A failure is silent on purpose — the caller falls back to no history, which
96
+ * is what the page showed before.
97
+ */
98
+ const unshallowed = new Set<string>();
99
+
100
+ function unshallow(dir: string) {
101
+ let root: string;
102
+
103
+ try {
104
+ root = execFileSync('git', ['-C', dir, 'rev-parse', '--show-toplevel'], {
105
+ encoding: 'utf8',
106
+ stdio: ['ignore', 'pipe', 'ignore']
107
+ }).trim();
108
+ } catch {
109
+ return;
110
+ }
111
+
112
+ if (!root || unshallowed.has(root)) return;
113
+ unshallowed.add(root);
114
+
115
+ try {
116
+ const shallow = execFileSync(
117
+ 'git',
118
+ ['-C', root, 'rev-parse', '--is-shallow-repository'],
119
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
120
+ ).trim();
121
+
122
+ if (shallow !== 'true') return;
123
+
124
+ execFileSync('git', ['-C', root, 'fetch', '--unshallow', '--quiet'], {
125
+ stdio: 'ignore'
126
+ });
127
+ } catch {
128
+ // No network, no remote, or a repository that cannot be deepened. The
129
+ // page keeps the fields it would have had without this module.
130
+ }
131
+ }
132
+
133
+ /**
134
+ * The unit separator, not a comma or a pipe.
135
+ *
136
+ * A commit message is free text and an author name may contain anything a
137
+ * person is called; the one byte that cannot appear in either is one git will
138
+ * emit for us with `%x1f`.
139
+ */
140
+ const SEPARATOR = '\u001f';
141
+
142
+ /**
143
+ * The record separator, marking where one commit's header begins.
144
+ *
145
+ * `--name-status -z` already ends every path with NUL, so the header needs a
146
+ * byte of its own to be told apart from one — and `%x01` is a byte no path and
147
+ * no author name can contain.
148
+ */
149
+ const RECORD = '\u0001';
150
+
151
+ /** What `-z` writes between the fields `--name-status` emits. */
152
+ const NUL = '\0';
153
+
154
+ /**
155
+ * ONE `git log` PER REPOSITORY, NOT ONE PER PAGE.
156
+ *
157
+ * This used to run `git log --follow` against each file as it was parsed. It
158
+ * was correct, and it cost 40 of the 55 seconds a cold content build took. The
159
+ * work is not the history — this repository's entire log reads in 70ms — it is
160
+ * spawning six hundred processes, which on a Linux that is really a virtual
161
+ * machine costs about 50ms each no matter how little the process then does.
162
+ *
163
+ * So the log is read once, whole, and indexed by path. Every page after the
164
+ * first is a map lookup.
165
+ *
166
+ * `--follow` is what that gives up, and it is bought back rather than dropped:
167
+ * `--name-status -M` reports a rename as `R100 <old> <new>`, so walking the log
168
+ * newest-first and remembering that `<old>` is now `<new>` lets a file keep the
169
+ * commits it earned under its former name. It is in fact the wider answer —
170
+ * git's own documentation calls `--follow` a single-file heuristic, and a file
171
+ * renamed twice defeats it.
172
+ */
173
+ export function parseGitLog(output: string): Map<string, Commit[]> {
174
+ const history = new Map<string, Commit[]>();
175
+
176
+ /**
177
+ * Where a path that no longer exists lives today.
178
+ *
179
+ * Filled walking backwards through time, so by the moment an older commit
180
+ * names `docs/old.md` the rename that made it `docs/new.md` has already been
181
+ * read, and the commit lands on the file the site actually serves.
182
+ */
183
+ const renamedTo = new Map<string, string>();
184
+ const today = (path: string) => renamedTo.get(path) ?? path;
185
+
186
+ for (const record of output.split(RECORD)) {
187
+ if (!record) continue;
188
+
189
+ const header = record.indexOf(NUL);
190
+ if (header === -1) continue;
191
+
192
+ const [date, name, email] = record.slice(0, header).split(SEPARATOR);
193
+ const commit: Commit = {
194
+ date: date ?? '',
195
+ name: name ?? '',
196
+ email: email ?? ''
197
+ };
198
+
199
+ // The newline git writes after the header belongs to the first token, not
200
+ // to a path — so every token is trimmed rather than the string patched.
201
+ const tokens = record
202
+ .slice(header + 1)
203
+ .split(NUL)
204
+ .map((token) => token.trim())
205
+ .filter(Boolean);
206
+
207
+ for (let index = 0; index < tokens.length;) {
208
+ const status = tokens[index]!;
209
+
210
+ // A status is a letter and an optional similarity score. Anything else
211
+ // means the walk has lost its place, and the rest of this commit is not
212
+ // worth guessing at.
213
+ if (!/^[A-Z]\d*$/.test(status)) break;
214
+
215
+ // R and C carry two paths; every other status carries one.
216
+ const pair = status.startsWith('R') || status.startsWith('C');
217
+ const from = tokens[index + 1];
218
+ const to = pair ? tokens[index + 2] : from;
219
+ index += pair ? 3 : 2;
220
+
221
+ if (!to) break;
222
+
223
+ const path = today(to);
224
+ const commits = history.get(path);
225
+
226
+ if (commits) commits.push(commit);
227
+ else history.set(path, [commit]);
228
+
229
+ // Only a rename carries a name forward. A copy leaves the original where
230
+ // it was, and that file's history stays its own.
231
+ if (pair && status.startsWith('R') && from) renamedTo.set(from, path);
232
+ }
233
+ }
234
+
235
+ return history;
236
+ }
237
+
238
+ /** One index per repository, built the first time a page asks for it. */
239
+ const histories = new Map<string, Map<string, Commit[]>>();
240
+
241
+ function historyOf(root: string): Map<string, Commit[]> {
242
+ const cached = histories.get(root);
243
+ if (cached) return cached;
244
+
245
+ let history: Map<string, Commit[]>;
246
+
247
+ try {
248
+ history = parseGitLog(
249
+ execFileSync(
250
+ 'git',
251
+ [
252
+ '-C',
253
+ root,
254
+ 'log',
255
+ `--format=${RECORD}%aI%x1f%an%x1f%ae`,
256
+ '--name-status',
257
+ '-M',
258
+ '-z'
259
+ ],
260
+ {
261
+ encoding: 'utf8',
262
+ stdio: ['ignore', 'pipe', 'ignore'],
263
+ // A documentation repository's whole log against execFileSync's
264
+ // 1 MB default is a truncated history reported as a git failure.
265
+ maxBuffer: Number.POSITIVE_INFINITY
266
+ }
267
+ )
268
+ );
269
+ } catch {
270
+ // Not a checkout, no git, or a history that cannot be read. All three mean
271
+ // the same thing here: nothing to show, which is not an error.
272
+ history = new Map();
273
+ }
274
+
275
+ histories.set(root, history);
276
+ return history;
277
+ }
278
+
279
+ /** The repository a directory belongs to, asked once per directory. */
280
+ const roots = new Map<string, string | undefined>();
281
+
282
+ function rootOf(dir: string): string | undefined {
283
+ if (roots.has(dir)) return roots.get(dir);
284
+
285
+ let root: string | undefined;
286
+
287
+ try {
288
+ root =
289
+ execFileSync('git', ['-C', dir, 'rev-parse', '--show-toplevel'], {
290
+ encoding: 'utf8',
291
+ stdio: ['ignore', 'pipe', 'ignore']
292
+ }).trim() || undefined;
293
+ } catch {
294
+ root = undefined;
295
+ }
296
+
297
+ roots.set(dir, root);
298
+ return root;
299
+ }
300
+
301
+ function gitLog(file: string): Commit[] {
302
+ const root = rootOf(dirname(file));
303
+ if (!root) return [];
304
+
305
+ // git speaks repository-relative paths with forward slashes, whatever the
306
+ // platform's own separator is.
307
+ const path = relative(root, file).split(sep).join('/');
308
+
309
+ return historyOf(root).get(path) ?? [];
310
+ }
311
+
312
+ /**
313
+ * One entry per person, most commits first.
314
+ *
315
+ * Identity is the email, not the name — the same person commits as "Titus
316
+ * Kirch" and as "titus" and would otherwise appear twice. The GitHub username
317
+ * is read out of a noreply address, which is the only place git actually
318
+ * carries one; without it there is a name and no avatar, which is the truth.
319
+ */
320
+ export function contributorsOf(commits: Commit[]): DuxtContributor[] {
321
+ const people = new Map<string, DuxtContributor>();
322
+
323
+ for (const commit of commits) {
324
+ const key = commit.email.toLowerCase();
325
+ const existing = people.get(key);
326
+
327
+ if (existing) {
328
+ existing.commits += 1;
329
+ continue;
330
+ }
331
+
332
+ people.set(key, {
333
+ name: commit.name,
334
+ commits: 1,
335
+ username: githubUsername(commit.email)
336
+ });
337
+ }
338
+
339
+ return [...people.values()].sort((a, b) => b.commits - a.commits);
340
+ }
341
+
342
+ /** `1234567+octocat@users.noreply.github.com` becomes `octocat`. */
343
+ export function githubUsername(email: string): string | undefined {
344
+ const match = /^(?:\d+\+)?([^@]+)@users\.noreply\.github\.com$/i.exec(
345
+ email.trim()
346
+ );
347
+
348
+ return match?.[1];
349
+ }
@@ -0,0 +1,152 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import type { Nuxt } from '@nuxt/schema';
3
+ import { readDuxtBuildConfig } from '../duxt-app-config';
4
+ import { duxtManifest, duxtSectionTypes } from '../sections-resolve';
5
+ import { resolveLatestRefs } from '../sources-git';
6
+ import { readContentCache } from '../content-cache';
7
+
8
+ /**
9
+ * `redirectFrom` in a page's frontmatter, turned into route rules.
10
+ *
11
+ * A moved page is the one kind of broken link a documentation site creates for
12
+ * itself, and the fix has always lived outside the site — an nginx rule, a
13
+ * Netlify `_redirects`, a Cloudflare list — written by whoever deploys it and
14
+ * never by whoever moved the page. Putting it in the frontmatter puts it in the
15
+ * same commit as the move.
16
+ *
17
+ * The layer is also the only thing that CAN solve it once. An old URL has to be
18
+ * redirected under every prefix the page is served at — the repository segment,
19
+ * the version segment and the locale segment — and only the resolved manifest
20
+ * knows which of those exist. A consumer writing the rule by hand would have to
21
+ * rediscover that scheme for every page.
22
+ */
23
+ export default function duxtRedirects(_options: unknown, nuxt: Nuxt) {
24
+ const layerDir = fileURLToPath(new URL('..', import.meta.url));
25
+
26
+ const dirs = [
27
+ ...nuxt.options._layers.flatMap((entry) => [
28
+ entry.config.rootDir,
29
+ entry.config.srcDir
30
+ ]),
31
+ layerDir
32
+ ].filter(Boolean) as string[];
33
+
34
+ const config = readDuxtBuildConfig(dirs);
35
+ const sources = duxtManifest(
36
+ resolveLatestRefs(config?.sources ?? [{ path: 'docs' }]),
37
+ config?.sourceOptions ?? {},
38
+ duxtSectionTypes(config?.sectionTypes)
39
+ );
40
+
41
+ // `nitro:config` rather than `build:done`: route rules are read when Nitro is
42
+ // configured, and Content has filled the cache by then (it parses in its own
43
+ // `modules:done` listener, which runs earlier).
44
+ nuxt.hook('nitro:config', (nitro) => {
45
+ const pages = readContentCache(
46
+ nuxt,
47
+ sources.map((source) => source.collection)
48
+ );
49
+
50
+ if (!pages?.length) return;
51
+
52
+ const prefixOf = new Map(
53
+ sources.map((source) => [source.collection, source.prefix])
54
+ );
55
+
56
+ const rules = redirectRules(
57
+ pages.map((page) => ({
58
+ path: page.path,
59
+ prefix: prefixOf.get(page.collection) ?? '',
60
+ redirectFrom: Array.isArray(page.content.redirectFrom)
61
+ ? (page.content.redirectFrom as string[])
62
+ : []
63
+ })),
64
+ localeSegments(nuxt)
65
+ );
66
+
67
+ if (!Object.keys(rules).length) return;
68
+
69
+ // Which of the shipped rules are the layer's own, recorded because nothing
70
+ // in a route rule says who wrote it. The devtools panel reads the rules
71
+ // back out of the running server rather than recomputing them — the honest
72
+ // source, and the reason it also lists foreign ones: @nuxtjs/sitemap ships
73
+ // `/sitemap.xml` the moment a site has more than one sitemap, and a reader
74
+ // debugging a moved page should not have to wonder which rules are theirs.
75
+ //
76
+ // Taken BEFORE the merge below and filtered by what was already claimed:
77
+ // an existing rule wins, so a path someone else had is not ours to own.
78
+ const owned = Object.keys(rules).filter(
79
+ (path) => !nitro.routeRules?.[path]
80
+ );
81
+
82
+ nitro.routeRules = { ...rules, ...nitro.routeRules };
83
+ nitro.runtimeConfig = {
84
+ ...nitro.runtimeConfig,
85
+ duxt: { ...(nitro.runtimeConfig?.duxt as object), redirects: owned }
86
+ };
87
+ });
88
+ }
89
+
90
+ /** Every locale that appears as a URL segment; the default one does not. */
91
+ function localeSegments(nuxt: Nuxt): string[] {
92
+ const i18n = (
93
+ nuxt.options as {
94
+ i18n?: {
95
+ locales?: (string | { code: string })[];
96
+ defaultLocale?: string;
97
+ strategy?: string;
98
+ };
99
+ }
100
+ ).i18n;
101
+
102
+ if (!i18n?.locales?.length) return [];
103
+
104
+ const codes = i18n.locales.map((locale) =>
105
+ typeof locale === 'string' ? locale : locale.code
106
+ );
107
+
108
+ return i18n.strategy === 'prefix'
109
+ ? codes
110
+ : codes.filter((code) => code !== i18n.defaultLocale);
111
+ }
112
+
113
+ /**
114
+ * The rules themselves, over data rather than over a build.
115
+ *
116
+ * A `redirectFrom` entry is written the way the page's own links are — relative
117
+ * to its source, without the prefix the site happens to serve it under. So each
118
+ * one becomes one rule per locale plus the unprefixed form, all pointing at the
119
+ * page's real path.
120
+ */
121
+ export function redirectRules(
122
+ pages: { path: string; prefix: string; redirectFrom: string[] }[],
123
+ locales: string[]
124
+ ): Record<string, { redirect: { to: string; statusCode: 301 } }> {
125
+ const rules: Record<string, { redirect: { to: string; statusCode: 301 } }> =
126
+ {};
127
+
128
+ for (const page of pages) {
129
+ for (const from of page.redirectFrom) {
130
+ if (!from.startsWith('/')) continue;
131
+
132
+ const source = isInsidePrefix(from, page.prefix)
133
+ ? from
134
+ : `${page.prefix}${from}`;
135
+
136
+ // A page redirecting to itself is a loop, not a redirect.
137
+ if (source === page.path) continue;
138
+
139
+ for (const locale of ['', ...locales]) {
140
+ const at = locale ? `/${locale}${source}` : source;
141
+ const to = locale ? `/${locale}${page.path}` : page.path;
142
+
143
+ rules[at] ??= { redirect: { to, statusCode: 301 } };
144
+ }
145
+ }
146
+ }
147
+
148
+ return rules;
149
+ }
150
+
151
+ const isInsidePrefix = (path: string, prefix: string) =>
152
+ Boolean(prefix) && (path === prefix || path.startsWith(`${prefix}/`));
@@ -0,0 +1,137 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import type { Nuxt } from '@nuxt/schema';
3
+ import { readDuxtBuildConfig } from '../duxt-app-config';
4
+ import { duxtManifest, duxtSectionTypes } from '../sections-resolve';
5
+ import { resolveLatestRefs } from '../sources-git';
6
+ import { readContentCache } from '../content-cache';
7
+ import type { Collected, PageRecord } from '../validate-report';
8
+ import { report, walk } from '../validate-report';
9
+
10
+ /**
11
+ * Catch the silent failures.
12
+ *
13
+ * Every bug this layer actually had was quiet: a collection Content dropped
14
+ * because its name was not an identifier, a page that 404ed because a link had
15
+ * gone stale, a navigation that came back empty. None of the three said
16
+ * anything — the build was green and the site was wrong.
17
+ *
18
+ * All four checks read ONE thing: the parse cache, through
19
+ * `readContentCache`. See that file for why the parse hook is the wrong seam.
20
+ *
21
+ * Severity is not uniform, on purpose:
22
+ *
23
+ * - a URL collision or an empty collection is an ERROR, because the page it
24
+ * costs is unreachable and no amount of reading the site would say why;
25
+ * - a broken link or a missing `title` is a WARNING naming the file, because
26
+ * the page still renders and a remote source can go stale between releases
27
+ * without that being this build's fault;
28
+ * - what each language carries, and what has stood still behind its original,
29
+ * is a NOTE — a state of the site rather than a defect in it. It is here
30
+ * because nothing else in the layer ever says it out loud, and a translation
31
+ * that quietly stopped moving is the failure this whole file is about.
32
+ */
33
+
34
+ export default function duxtValidate(_options: unknown, nuxt: Nuxt) {
35
+ const layerDir = fileURLToPath(new URL('..', import.meta.url));
36
+
37
+ const dirs = [
38
+ ...nuxt.options._layers.flatMap((entry) => [
39
+ entry.config.rootDir,
40
+ entry.config.srcDir
41
+ ]),
42
+ layerDir
43
+ ].filter(Boolean) as string[];
44
+
45
+ const config = readDuxtBuildConfig(dirs);
46
+ const sources = duxtManifest(
47
+ resolveLatestRefs(config?.sources ?? [{ path: 'docs' }]),
48
+ config?.sourceOptions ?? {},
49
+ duxtSectionTypes(config?.sectionTypes)
50
+ );
51
+
52
+ // `build:done` rather than `modules:done`: Content fills the cache in a
53
+ // `modules:done` listener of its own, and this module is registered before
54
+ // Content so its listener would run first and read the previous build's
55
+ // answer.
56
+ nuxt.hook('build:done', () => {
57
+ const cached = readContentCache(
58
+ nuxt,
59
+ sources.map((source) => source.collection)
60
+ );
61
+
62
+ if (!cached) return;
63
+
64
+ // The reports come from `modules/config.ts`, which read the artefacts once
65
+ // and left them on the manifest it wrote into `appConfig`. Merged rather
66
+ // than read again: a second parse of the same OpenAPI document, to reach
67
+ // the same answer, is a cost this build does not need — and two parses are
68
+ // two chances to disagree.
69
+ const reported = new Map(
70
+ (
71
+ (
72
+ nuxt.options.appConfig.duxt as
73
+ | { resolvedSources?: typeof sources }
74
+ | undefined
75
+ )?.resolvedSources ?? []
76
+ )
77
+ .filter((source) => source.generated?.report)
78
+ .map((source) => [source.collection, source.generated!.report] as const)
79
+ );
80
+
81
+ for (const source of sources) {
82
+ if (source.generated && reported.has(source.collection)) {
83
+ source.generated.report = reported.get(source.collection);
84
+ }
85
+ }
86
+
87
+ const pages: PageRecord[] = cached.map((entry) => {
88
+ const collected: Collected = {
89
+ anchors: new Set<string>(),
90
+ links: [],
91
+ commands: []
92
+ };
93
+ walk(entry.content.body, collected);
94
+
95
+ return {
96
+ collection: entry.collection,
97
+ path: entry.path,
98
+ file: entry.file,
99
+ title:
100
+ typeof entry.content.title === 'string'
101
+ ? entry.content.title
102
+ : undefined,
103
+ description:
104
+ typeof entry.content.description === 'string'
105
+ ? entry.content.description
106
+ : undefined,
107
+ anchors: collected.anchors,
108
+ links: collected.links,
109
+ commands: collected.commands,
110
+ lastUpdated:
111
+ typeof entry.content.lastUpdated === 'string'
112
+ ? entry.content.lastUpdated
113
+ : undefined
114
+ };
115
+ });
116
+
117
+ const problems = report(sources, pages);
118
+
119
+ // The translation report is INFORMATION, not a finding: an untranslated
120
+ // page is a state of the site rather than a defect in it, and printing it
121
+ // as a warning would train everyone to ignore the warnings.
122
+ if (problems.notes.length) {
123
+ console.info('[duxt] translations');
124
+ for (const note of problems.notes) console.info(`[duxt] ${note}`);
125
+ }
126
+
127
+ for (const warning of problems.warnings) {
128
+ console.warn(`[duxt] ${warning}`);
129
+ }
130
+
131
+ if (problems.errors.length) {
132
+ throw new Error(
133
+ `duxt: the documentation did not validate.\n - ${problems.errors.join('\n - ')}`
134
+ );
135
+ }
136
+ });
137
+ }