@kirchdev/duxt 0.3.4 → 0.3.5-dev-main.44-5337cc1

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 (219) hide show
  1. package/README.md +24 -9
  2. package/app/app.vue +17 -11
  3. package/app/assets/css/duxt.css +47 -6
  4. package/app/components/DuxtAnnouncements.vue +117 -0
  5. package/app/components/DuxtBrunoEntries.vue +62 -0
  6. package/app/components/DuxtCodeBlock.vue +18 -34
  7. package/app/components/DuxtCodeToolbar.vue +14 -0
  8. package/app/components/DuxtCopyButton.vue +33 -0
  9. package/app/components/DuxtCopyPage.vue +32 -30
  10. package/app/components/DuxtFooter.vue +39 -8
  11. package/app/components/DuxtHeader.vue +107 -48
  12. package/app/components/DuxtLandingProse.vue +1 -1
  13. package/app/components/DuxtLinkLabel.vue +36 -0
  14. package/app/components/DuxtLiveWindow.vue +8 -8
  15. package/app/components/DuxtLocale.vue +10 -1
  16. package/app/components/DuxtNavigation.vue +2 -2
  17. package/app/components/DuxtOpenApiClient.vue +285 -90
  18. package/app/components/DuxtOpenApiMedia.vue +30 -18
  19. package/app/components/DuxtOpenApiSchema.vue +10 -7
  20. package/app/components/DuxtPageFeedback.vue +7 -0
  21. package/app/components/DuxtPageInfo.vue +15 -12
  22. package/app/components/DuxtPageNav.vue +41 -27
  23. package/app/components/DuxtPreviewSkeleton.vue +4 -4
  24. package/app/components/DuxtProgress.vue +2 -14
  25. package/app/components/DuxtSearch.vue +230 -90
  26. package/app/components/DuxtSearchInput.vue +70 -0
  27. package/app/components/DuxtSections.vue +11 -1
  28. package/app/components/DuxtSegmented.vue +64 -0
  29. package/app/components/DuxtShortcuts.vue +16 -11
  30. package/app/components/DuxtShortcutsTrigger.vue +49 -0
  31. package/app/components/DuxtSkipLink.vue +1 -1
  32. package/app/components/DuxtToc.vue +16 -10
  33. package/app/components/DuxtTranslationBanner.vue +1 -1
  34. package/app/components/DuxtVersion.vue +53 -12
  35. package/app/components/DuxtVersionBanner.vue +5 -5
  36. package/app/components/content/AccordionItem.vue +2 -2
  37. package/app/components/content/BrunoOverview.vue +260 -0
  38. package/app/components/content/BrunoRequest.vue +141 -0
  39. package/app/components/content/BrunoRequests.vue +104 -0
  40. package/app/components/content/Callout.vue +5 -5
  41. package/app/components/content/ChangelogReleases.vue +161 -40
  42. package/app/components/content/CodeGroup.vue +41 -37
  43. package/app/components/content/Field.vue +1 -1
  44. package/app/components/content/FileTree.vue +1 -1
  45. package/app/components/content/OpenApiOperation.vue +4 -4
  46. package/app/components/content/OpenApiOperations.vue +1 -1
  47. package/app/components/content/OpenApiOverview.vue +1 -1
  48. package/app/components/content/PackageManagers.vue +42 -58
  49. package/app/components/content/PageCards.vue +7 -4
  50. package/app/components/content/Preview.vue +15 -39
  51. package/app/components/content/ProseImg.vue +596 -49
  52. package/app/components/content/Since.vue +1 -1
  53. package/app/components/content/Steps.vue +6 -4
  54. package/app/components/content/TabGroup.vue +13 -12
  55. package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +1 -1
  56. package/app/components/ui/command/CommandShortcut.vue +1 -1
  57. package/app/components/ui/dialog/DialogContent.vue +2 -2
  58. package/app/components/ui/dialog/DialogHeader.vue +1 -1
  59. package/app/components/ui/dialog/DialogScrollContent.vue +1 -1
  60. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +2 -2
  61. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  62. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +1 -1
  63. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +2 -2
  64. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +1 -1
  65. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +2 -2
  66. package/app/components/ui/navigation-menu/NavigationMenuContent.vue +1 -1
  67. package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +1 -1
  68. package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +1 -1
  69. package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
  70. package/app/components/ui/select/SelectItem.vue +2 -2
  71. package/app/components/ui/sheet/SheetContent.vue +29 -3
  72. package/app/components/ui/sidebar/Sidebar.vue +28 -4
  73. package/app/components/ui/sidebar/SidebarGroupAction.vue +1 -1
  74. package/app/components/ui/sidebar/SidebarInset.vue +1 -1
  75. package/app/components/ui/sidebar/SidebarMenuAction.vue +1 -1
  76. package/app/components/ui/sidebar/SidebarMenuBadge.vue +1 -1
  77. package/app/components/ui/sidebar/SidebarMenuSub.vue +1 -1
  78. package/app/components/ui/sidebar/SidebarMenuSubButton.vue +1 -1
  79. package/app/components/ui/sidebar/SidebarProvider.vue +7 -16
  80. package/app/components/ui/sidebar/index.ts +10 -2
  81. package/app/components/ui/sidebar/utils.ts +0 -1
  82. package/app/components/ui/sonner/Sonner.vue +18 -0
  83. package/app/components/ui/tabs/TabsList.vue +13 -3
  84. package/app/components/ui/tabs/TabsTrigger.vue +24 -3
  85. package/app/components/ui/tooltip/TooltipContent.vue +2 -2
  86. package/app/composables/useActiveHeading.ts +3 -25
  87. package/app/composables/useDuxtAnalytics.ts +50 -0
  88. package/app/composables/useDuxtAnimatedHeight.ts +8 -38
  89. package/app/composables/useDuxtAnnouncements.ts +153 -0
  90. package/app/composables/useDuxtCopy.ts +51 -0
  91. package/app/composables/useDuxtDirection.ts +23 -0
  92. package/app/composables/useDuxtNavigation.ts +28 -8
  93. package/app/composables/useDuxtPath.ts +3 -5
  94. package/app/composables/useDuxtSearch.ts +87 -35
  95. package/app/composables/useDuxtSection.ts +15 -1
  96. package/app/composables/useDuxtSectionRow.ts +6 -5
  97. package/app/composables/useDuxtShortcuts.ts +319 -20
  98. package/app/composables/useDuxtVersion.ts +57 -6
  99. package/app/composables/useDuxtViewportMeasure.ts +37 -0
  100. package/app/composables/useFuzzySearch.ts +9 -4
  101. package/app/composables/useRecentPages.ts +14 -17
  102. package/app/error.vue +112 -74
  103. package/app/layouts/changelog.vue +9 -1
  104. package/app/layouts/default.vue +9 -0
  105. package/app/layouts/docs.vue +9 -1
  106. package/app/layouts/reference.vue +9 -1
  107. package/app/middleware/duxt-section-layout.global.ts +1 -3
  108. package/app/pages/[...slug].vue +146 -35
  109. package/app/pages/index.vue +15 -23
  110. package/app/types/duxt.d.ts +312 -4
  111. package/app/utils/analytics.ts +91 -0
  112. package/app/utils/announcements.ts +205 -0
  113. package/app/utils/changelog.ts +0 -28
  114. package/app/utils/code-highlight.ts +3 -1
  115. package/app/utils/contributor-avatar.ts +27 -0
  116. package/app/utils/direction.ts +43 -0
  117. package/app/utils/duxt-config.ts +43 -0
  118. package/app/utils/duxt-text.ts +10 -0
  119. package/app/utils/format-date.ts +28 -0
  120. package/app/utils/generated-sections.ts +68 -12
  121. package/app/utils/generated-toc.ts +14 -7
  122. package/app/utils/locale-path.ts +43 -0
  123. package/app/utils/navigation-tree.ts +51 -1
  124. package/app/utils/nearest-page.ts +8 -0
  125. package/app/utils/openapi.ts +16 -11
  126. package/app/utils/page-controls.ts +215 -0
  127. package/app/utils/pill.ts +61 -0
  128. package/app/utils/prose-image.ts +128 -0
  129. package/app/utils/search-context.ts +110 -0
  130. package/app/utils/search-display.ts +248 -0
  131. package/app/utils/section-scope.ts +65 -4
  132. package/app/utils/stored-json.ts +31 -0
  133. package/app/utils/version-choices.ts +6 -13
  134. package/app/utils/version-paths.ts +21 -0
  135. package/bin/duxt-cache-key.mjs +61 -0
  136. package/bin/duxt-og-cache.mjs +58 -0
  137. package/bin/{duxt-report.mjs → duxt.mjs} +13 -7
  138. package/bruno-model.ts +169 -0
  139. package/bruno-parse.ts +656 -0
  140. package/bruno-zip.ts +195 -0
  141. package/cli.ts +240 -0
  142. package/duxt-app-config.ts +18 -1
  143. package/frontmatter.ts +49 -0
  144. package/git-contributors.ts +322 -0
  145. package/i18n/locales/de/duxt/announcement.json +7 -0
  146. package/i18n/locales/de/duxt/bruno.json +25 -0
  147. package/i18n/locales/de/duxt/changelog.json +1 -1
  148. package/i18n/locales/de/duxt/code.json +2 -1
  149. package/i18n/locales/de/duxt/page.json +9 -0
  150. package/i18n/locales/de/duxt/shortcuts.json +1 -0
  151. package/i18n/locales/de/duxt/theme.json +2 -1
  152. package/i18n/locales/en/duxt/announcement.json +7 -0
  153. package/i18n/locales/en/duxt/bruno.json +25 -0
  154. package/i18n/locales/en/duxt/changelog.json +1 -1
  155. package/i18n/locales/en/duxt/code.json +2 -1
  156. package/i18n/locales/en/duxt/page.json +9 -0
  157. package/i18n/locales/en/duxt/shortcuts.json +1 -0
  158. package/i18n/locales/en/duxt/theme.json +2 -1
  159. package/i18n/locales/es/duxt/announcement.json +7 -0
  160. package/i18n/locales/es/duxt/bruno.json +25 -0
  161. package/i18n/locales/es/duxt/changelog.json +1 -1
  162. package/i18n/locales/es/duxt/code.json +2 -1
  163. package/i18n/locales/es/duxt/page.json +9 -0
  164. package/i18n/locales/es/duxt/shortcuts.json +1 -0
  165. package/i18n/locales/es/duxt/theme.json +2 -1
  166. package/i18n/locales/fr/duxt/announcement.json +7 -0
  167. package/i18n/locales/fr/duxt/bruno.json +25 -0
  168. package/i18n/locales/fr/duxt/changelog.json +1 -1
  169. package/i18n/locales/fr/duxt/code.json +2 -1
  170. package/i18n/locales/fr/duxt/page.json +9 -0
  171. package/i18n/locales/fr/duxt/shortcuts.json +1 -0
  172. package/i18n/locales/fr/duxt/theme.json +2 -1
  173. package/i18n/locales/pt/duxt/announcement.json +7 -0
  174. package/i18n/locales/pt/duxt/bruno.json +25 -0
  175. package/i18n/locales/pt/duxt/changelog.json +1 -1
  176. package/i18n/locales/pt/duxt/code.json +2 -1
  177. package/i18n/locales/pt/duxt/page.json +9 -0
  178. package/i18n/locales/pt/duxt/shortcuts.json +1 -0
  179. package/i18n/locales/pt/duxt/theme.json +2 -1
  180. package/modules/bruno.ts +118 -0
  181. package/modules/config.ts +26 -0
  182. package/modules/devtools.ts +2 -3
  183. package/modules/git-meta.ts +16 -47
  184. package/modules/search-records.ts +132 -0
  185. package/nuxt.config.ts +81 -7
  186. package/og-image-cache.ts +742 -0
  187. package/package.json +42 -10
  188. package/public/devtools/checks.html +2 -2
  189. package/public/devtools/config.html +59 -4
  190. package/public/devtools/pages.html +8 -1
  191. package/public/devtools/paths.html +1 -1
  192. package/public/devtools/sources.html +2 -2
  193. package/public/devtools/versions.html +3 -0
  194. package/report.ts +28 -19
  195. package/search-records.ts +380 -0
  196. package/section-input.ts +142 -0
  197. package/section-markdown.ts +67 -0
  198. package/section-reports.ts +10 -4
  199. package/sections-bruno.ts +571 -0
  200. package/sections-changelog.ts +76 -43
  201. package/sections-openapi.ts +12 -83
  202. package/sections-resolve.ts +174 -8
  203. package/sections.ts +33 -10
  204. package/server/devtools/shell.ts +1 -1
  205. package/server/mcp/tools/list-pages.ts +77 -36
  206. package/server/mcp/tools/list-versions.ts +84 -0
  207. package/server/mcp/tools/read-page.ts +80 -26
  208. package/server/mcp/tools/search-docs.ts +87 -53
  209. package/server/middleware/raw-markdown.ts +1 -1
  210. package/server/routes/llms-full.txt.get.ts +6 -8
  211. package/server/routes/llms.txt.get.ts +1 -1
  212. package/server/utils/duxt-server-text.ts +0 -13
  213. package/server/utils/llms-pages.ts +24 -9
  214. package/server/utils/mcp-docs.ts +577 -0
  215. package/sources-cache.ts +376 -0
  216. package/sources-git.ts +186 -31
  217. package/sources-resolve.ts +59 -1
  218. package/sources.ts +122 -13
  219. package/tfplugindocs.ts +158 -0
@@ -13,6 +13,15 @@
13
13
  "no": "Non",
14
14
  "thanks": "Merci."
15
15
  },
16
+ "image": {
17
+ "close": "Fermer la visionneuse d’image",
18
+ "original": "Ouvrir l’original dans un nouvel onglet",
19
+ "resetZoom": "Réinitialiser le zoom",
20
+ "zoom": "Agrandir l'image",
21
+ "zoomIn": "Zoom avant",
22
+ "zoomNamed": "Agrandir l'image : {alt}",
23
+ "zoomOut": "Zoom arrière"
24
+ },
16
25
  "tabs": "Options",
17
26
  "since": "Nouveau dans {version}",
18
27
  "copy": {
@@ -2,6 +2,7 @@
2
2
  "duxt": {
3
3
  "shortcuts": {
4
4
  "title": "Raccourcis clavier",
5
+ "open": "Afficher les raccourcis clavier",
5
6
  "search": "Rechercher dans la documentation",
6
7
  "help": "Afficher cette liste",
7
8
  "previous": "Page précédente",
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "duxt": {
3
3
  "theme": {
4
- "toggle": "Changer de thème"
4
+ "toLight": "Passer au thème clair",
5
+ "toDark": "Passer au thème sombre"
5
6
  }
6
7
  }
7
8
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "duxt": {
3
+ "announcement": {
4
+ "dismiss": "Dispensar este anúncio"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "duxt": {
3
+ "bruno": {
4
+ "auth": "Autenticação",
5
+ "body": "Corpo",
6
+ "download": "Transferir a coleção",
7
+ "downloadHint": "A coleção exatamente como esta versão e este idioma a descrevem.",
8
+ "environments": "Ambientes",
9
+ "fetch": "Abrir no Bruno",
10
+ "fetchHint": "Clona o repositório no Bruno. Segue o ramo predefinido e não está fixado nesta versão.",
11
+ "folders": "Pastas",
12
+ "format": "Formato",
13
+ "getIt": "Obter a coleção",
14
+ "hasCode": "Este pedido também tem scripts ou testes. Aqui não são mostrados nem executados; estão na transferência.",
15
+ "headers": "Cabeçalhos",
16
+ "name": "Nome",
17
+ "notPublished": "Ambientes, credenciais, scripts e testes nunca são publicados neste site nem executados no seu navegador.",
18
+ "parameters": "Parâmetros",
19
+ "requests": "Pedidos",
20
+ "shared": "Comum a todos os pedidos",
21
+ "value": "Valor",
22
+ "withheld": "retido"
23
+ }
24
+ }
25
+ }
@@ -5,7 +5,7 @@
5
5
  "filter": "Filtrar por tipo de alteração",
6
6
  "latest": "Mais recente",
7
7
  "changes": "Alterações",
8
- "kinds": "Tipos de alteração",
8
+ "contributors": "Colaboradores",
9
9
  "last": "Última versão",
10
10
  "more": "e mais {count}",
11
11
  "kind": "Tipo de alteração",
@@ -7,7 +7,8 @@
7
7
  "preview": "Pré-visualização",
8
8
  "source": "Código",
9
9
  "previewTabs": "Exemplo e código-fonte",
10
- "copiedToast": "Copiado para a área de transferência"
10
+ "copiedToast": "Copiado para a área de transferência",
11
+ "copyFailed": "Não foi possível copiar para a área de transferência."
11
12
  }
12
13
  }
13
14
  }
@@ -13,6 +13,15 @@
13
13
  "no": "Não",
14
14
  "thanks": "Obrigado."
15
15
  },
16
+ "image": {
17
+ "close": "Fechar o visualizador de imagens",
18
+ "original": "Abrir o original num novo separador",
19
+ "resetZoom": "Repor o zoom",
20
+ "zoom": "Ampliar imagem",
21
+ "zoomIn": "Aumentar o zoom",
22
+ "zoomNamed": "Ampliar imagem: {alt}",
23
+ "zoomOut": "Diminuir o zoom"
24
+ },
16
25
  "tabs": "Opções",
17
26
  "since": "Novo em {version}",
18
27
  "copy": {
@@ -2,6 +2,7 @@
2
2
  "duxt": {
3
3
  "shortcuts": {
4
4
  "title": "Atalhos de teclado",
5
+ "open": "Mostrar os atalhos de teclado",
5
6
  "search": "Pesquisar na documentação",
6
7
  "help": "Mostrar esta lista",
7
8
  "previous": "Página anterior",
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "duxt": {
3
3
  "theme": {
4
- "toggle": "Mudar o tema"
4
+ "toLight": "Mudar para o tema claro",
5
+ "toDark": "Mudar para o tema escuro"
5
6
  }
6
7
  }
7
8
  }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * The downloadable archive of every Bruno collection the site publishes.
3
+ *
4
+ * WHY A MODULE AND NOT A ROUTE. The `.bru` files exist at BUILD TIME and
5
+ * nowhere else: a site deployed to a Worker has no filesystem, and one deployed
6
+ * anywhere at all has prerendered its pages long before a reader clicks the
7
+ * download. So the archive is written during the build and registered as a
8
+ * public asset — a real file under `.output/public`, served by the assets
9
+ * binding without the server being invoked, which is what every other
10
+ * prerendered thing on these sites already is.
11
+ *
12
+ * LOCAL COLLECTIONS ONLY, and the limit is the one `section-reports.ts` already
13
+ * states for the same reason: a remote artefact lives wherever Content's
14
+ * hash-cached checkout put it, which is a directory known only inside the
15
+ * collection that declared it, and recomputing it here is exactly the second
16
+ * implementation `remoteCollection` refuses to write. `sections-bruno.ts` knows
17
+ * this — a remote section's overview links no archive at all, so there is no
18
+ * dead link rather than a link to a file this module never wrote.
19
+ *
20
+ * The page's link and this file's name are composed from the same two halves —
21
+ * `DUXT_BRUNO_ASSETS` and `brunoZipName` — so they cannot drift: two agreeing by
22
+ * coincidence is how a download 404s after somebody renames one.
23
+ */
24
+ import { mkdirSync, writeFileSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ import type { Nuxt } from '@nuxt/schema';
27
+ import {
28
+ DUXT_BRUNO_ASSETS,
29
+ brunoZipEntries,
30
+ brunoZipName,
31
+ zipStore
32
+ } from '../bruno-zip';
33
+ import { readDuxtBuildConfig } from '../duxt-app-config';
34
+ import { duxtManifest, duxtSectionTypes } from '../sections-resolve';
35
+ import { diskSectionInput, sectionArtefactExists } from '../section-input';
36
+ import { repositoryRoot } from '../repository-root';
37
+ import { resolveLatestRefs } from '../sources-git';
38
+
39
+ export default function duxtBruno(_options: unknown, nuxt: Nuxt) {
40
+ const dirs = nuxt.options._layers
41
+ .flatMap((entry) => [entry.config.rootDir, entry.config.srcDir])
42
+ .filter(Boolean) as string[];
43
+
44
+ const config = readDuxtBuildConfig(dirs);
45
+ const types = duxtSectionTypes(config?.sectionTypes);
46
+
47
+ const entries = duxtManifest(
48
+ resolveLatestRefs(config?.sources ?? [{ path: 'docs' }]),
49
+ config?.sourceOptions ?? {},
50
+ types
51
+ ).filter(
52
+ (entry) => entry.generated?.type === 'bruno' && !entry.generated.remote
53
+ );
54
+
55
+ if (!entries.length) return;
56
+
57
+ // BUILT ONCE, HELD AS BYTES. Both writes below come from this array rather
58
+ // than one of them reading the other's directory back: `buildDir` is not the
59
+ // same path at module setup and at the end of the Nitro build, and a hook
60
+ // that re-read it found nothing there — an empty asset directory and a 404
61
+ // behind the one control the overview is built around.
62
+ const archives = entries.flatMap((entry) => {
63
+ const source = join(repositoryRoot(), entry.path);
64
+
65
+ // Silently skipped rather than thrown on: a declared path that is not there
66
+ // already fails the build where the collection is declared, with a message
67
+ // naming the section. Throwing a second, worse one from here would only
68
+ // decide which of the two a reader sees.
69
+ if (!sectionArtefactExists(source, 'directory')) return [];
70
+
71
+ return [
72
+ {
73
+ name: brunoZipName(entry.collection),
74
+ data: zipStore(
75
+ brunoZipEntries(diskSectionInput(source, entry.path, 'directory'))
76
+ )
77
+ }
78
+ ];
79
+ });
80
+
81
+ if (!archives.length) return;
82
+
83
+ const dir = join(nuxt.options.buildDir, 'duxt-bruno');
84
+
85
+ write(dir, archives);
86
+
87
+ // TWO REGISTRATIONS, because dev and build ask different questions.
88
+ //
89
+ // `publicAssets` is what the DEV server reads: it serves the directory off
90
+ // disk, and it is also what teaches Nitro that `/_duxt/bruno/` is a public
91
+ // URL rather than a route.
92
+ nuxt.hook('nitro:config', (nitro) => {
93
+ nitro.publicAssets ??= [];
94
+ nitro.publicAssets.push({ dir, baseURL: DUXT_BRUNO_ASSETS, maxAge: 0 });
95
+ });
96
+
97
+ // The BUILD is the other half, and it is not the same thing said twice:
98
+ // Nitro's own copy pass does not carry a directory inside `.nuxt` into
99
+ // `.output/public`, so registering alone produced a site that knew the URL
100
+ // was an asset and had no file behind it — a 404 on the one control the
101
+ // overview is built around. Copied here, after that pass has run, which is
102
+ // what this hook exists for.
103
+ nuxt.hook('nitro:build:public-assets', (nitro) => {
104
+ write(
105
+ join(nitro.options.output.publicDir, ...DUXT_BRUNO_ASSETS.split('/')),
106
+ archives
107
+ );
108
+ });
109
+ }
110
+
111
+ /** The archives, into one directory. */
112
+ function write(dir: string, archives: { name: string; data: Uint8Array }[]) {
113
+ mkdirSync(dir, { recursive: true });
114
+
115
+ for (const archive of archives) {
116
+ writeFileSync(join(dir, archive.name), archive.data);
117
+ }
118
+ }
package/modules/config.ts CHANGED
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import type { Nuxt } from '@nuxt/schema';
5
+ import { bundledLanguagesInfo } from 'shiki/langs';
5
6
  import { enableWriteAheadLog } from '../content-cache';
6
7
  import type { DuxtBuildConfig } from '../duxt-app-config';
7
8
  import { readDuxtBuildConfig } from '../duxt-app-config';
@@ -170,6 +171,12 @@ function checkSourceLocales(
170
171
  * The three at the bottom are not samples: the package-manager block, the
171
172
  * response body and the example bodies are highlighted at runtime too, and they
172
173
  * are the same three whatever a site configures.
174
+ *
175
+ * THE LANDING DEMOS TOO. A `code` demo in `landing.showcase` goes through the
176
+ * same highlighter, and a file whose language was not in this map rendered as
177
+ * flat text — `openapi.yaml` and `widget.md` beside a coloured `nuxt.config.ts`
178
+ * read as the highlighter being off. So the languages the showcase names are
179
+ * added, and a site pays for them only if its landing page uses them.
173
180
  */
174
181
  function writeGrammars(nuxt: Nuxt, config?: DuxtBuildConfig): void {
175
182
  const shipped = new Map(
@@ -192,6 +199,25 @@ function writeGrammars(nuxt: Nuxt, config?: DuxtBuildConfig): void {
192
199
  if (language) languages.add(language);
193
200
  }
194
201
 
202
+ // Aliases resolve to the grammar's id: `yml` is a name Shiki answers to, not
203
+ // a module under `shiki/langs/`.
204
+ const grammarIds = new Map(
205
+ bundledLanguagesInfo.flatMap((info) =>
206
+ [info.id, ...(info.aliases ?? [])].map((name) => [name, info.id] as const)
207
+ )
208
+ );
209
+
210
+ for (const band of config?.landing?.showcase ?? []) {
211
+ for (const file of band?.demo?.files ?? []) {
212
+ // Only names with a module behind them: `text` and anything Shiki does
213
+ // not ship would be an `import()` Vite cannot resolve, and that fails
214
+ // the build rather than degrading one block.
215
+ const id = grammarIds.get(file?.language?.toLowerCase() ?? '');
216
+
217
+ if (id) languages.add(id);
218
+ }
219
+ }
220
+
195
221
  const loaders = [...languages]
196
222
  .sort()
197
223
  .map(
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import type { Nuxt } from '@nuxt/schema';
5
+ import { localeCodesOf } from '../app/utils/locale-path';
5
6
 
6
7
  /**
7
8
  * A window into the layer that has been hardest to debug.
@@ -120,7 +121,5 @@ function localeCodes(nuxt: Nuxt): string[] {
120
121
  }
121
122
  ).i18n?.locales;
122
123
 
123
- return (locales ?? []).map((locale) =>
124
- typeof locale === 'string' ? locale : locale.code
125
- );
124
+ return localeCodesOf(locales);
126
125
  }
@@ -2,9 +2,12 @@ import { execFileSync } from 'node:child_process';
2
2
  import { dirname, relative, sep } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import type { Nuxt } from '@nuxt/schema';
5
+ import type { DuxtContributor } from '../git-contributors';
6
+ import { contributorsOf } from '../git-contributors';
5
7
  import { readDuxtBuildConfig } from '../duxt-app-config';
6
8
  import { duxtManifest, duxtSectionTypes } from '../sections-resolve';
7
9
  import { resolveLatestRefs } from '../sources-git';
10
+ import { normaliseTfplugindocsPage } from '../tfplugindocs';
8
11
 
9
12
  /**
10
13
  * "Last updated" and the contributor list, from the git history the file
@@ -30,11 +33,7 @@ interface AfterParseContext {
30
33
  file?: { path?: string; id?: string };
31
34
  }
32
35
 
33
- export interface DuxtContributor {
34
- name: string;
35
- commits: number;
36
- username?: string;
37
- }
36
+ export type { DuxtContributor };
38
37
 
39
38
  export default function duxtGitMeta(_options: unknown, nuxt: Nuxt) {
40
39
  const layerDir = fileURLToPath(new URL('..', import.meta.url));
@@ -54,8 +53,8 @@ export default function duxtGitMeta(_options: unknown, nuxt: Nuxt) {
54
53
  duxtSectionTypes(config?.sectionTypes)
55
54
  );
56
55
 
57
- const wanted = new Map(
58
- sources.map((source) => [source.collection, source.history])
56
+ const sourceByCollection = new Map(
57
+ sources.map((source) => [source.collection, source])
59
58
  );
60
59
 
61
60
  nuxt.hook(
@@ -65,10 +64,19 @@ export default function duxtGitMeta(_options: unknown, nuxt: Nuxt) {
65
64
  const content = ctx.content;
66
65
  if (!file || !content) return;
67
66
 
67
+ const source = sourceByCollection.get(ctx.collection?.name ?? '');
68
+ if (!source) return;
69
+
70
+ // Source flavours alter metadata only. Their Markdown remains portable to
71
+ // the generator's own publisher, so the body is never rewritten here.
72
+ if (source.flavor === 'tfplugindocs') {
73
+ normaliseTfplugindocsPage(content, ctx.file?.id ?? file);
74
+ }
75
+
68
76
  // A source that has not asked for its history is left alone — for a
69
77
  // downloaded one that would otherwise answer out of a single-commit
70
78
  // clone, which is wrong data rather than missing data.
71
- if (!wanted.get(ctx.collection?.name ?? '')) return;
79
+ if (!source.history) return;
72
80
 
73
81
  if (file.includes('/.data/content/')) unshallow(dirname(file));
74
82
 
@@ -308,42 +316,3 @@ function gitLog(file: string): Commit[] {
308
316
 
309
317
  return historyOf(root).get(path) ?? [];
310
318
  }
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,132 @@
1
+ import type { Nuxt } from '@nuxt/schema';
2
+ import { readContentCache } from '../content-cache';
3
+ import { duxtSearchRecords } from '../search-records';
4
+ import type { DuxtSearchRecordOptions } from '../search-records';
5
+ import type { DuxtResolvedSource } from '../sources-resolve';
6
+
7
+ /**
8
+ * duxt's one build-time extension point, and the whole of it.
9
+ *
10
+ * Why there is exactly one hook rather than a registry of them, and why a
11
+ * `duxt.plugins` API is not coming, is written where the contract is:
12
+ * `search-records.ts`. This half only decides WHEN the payload is built and
13
+ * makes sure it is built for nobody as rarely as possible.
14
+ *
15
+ * `build:done`, and the timing is the whole reason the hook can promise
16
+ * anything:
17
+ *
18
+ * - Content fills its parse cache from a `modules:done` listener, so at
19
+ * `build:done` every source, version, locale and generated section has been
20
+ * parsed — including the repositories Content downloaded into `.data/`,
21
+ * which nothing else on disk describes. Same seam `modules/validate.ts`
22
+ * reads, and for the same reason: `content:file:afterParse` fires only for
23
+ * files whose checksum CHANGED, so a listener built on it reports an empty
24
+ * site on every second build.
25
+ * - `@nuxt/nitro-server` runs the entire Nitro build — prerender and rollup —
26
+ * from a `build:done` listener it registers while bundling, which is long
27
+ * after every module has registered its own. Hooks run in registration
28
+ * order, so this one fires BEFORE Nitro produces anything, and a listener
29
+ * may still write a file into `public/` and have it copied into the output.
30
+ */
31
+ const HOOK = 'duxt:search:records';
32
+
33
+ export default function duxtSearchRecordsModule(_options: unknown, nuxt: Nuxt) {
34
+ nuxt.hook('build:done', async () => {
35
+ if (!listening(nuxt)) return;
36
+
37
+ // The manifest `modules/config.ts` already resolved, rather than a second
38
+ // pass of its own: resolving it means `git ls-remote` for every source that
39
+ // tracks a moving ref, and asking twice is both a second network round trip
40
+ // and a second chance for the two answers to differ.
41
+ const sources =
42
+ (
43
+ nuxt.options.appConfig.duxt as
44
+ | { resolvedSources?: DuxtResolvedSource[] }
45
+ | undefined
46
+ )?.resolvedSources ?? [];
47
+
48
+ if (!sources.length) return;
49
+
50
+ const cached = readContentCache(
51
+ nuxt,
52
+ sources.map((source) => source.collection)
53
+ );
54
+
55
+ // No cache at all is not "an empty site": it is a run that parsed nothing.
56
+ // Handing a provider zero records there would tell it to empty its index.
57
+ if (!cached) return;
58
+
59
+ await nuxt.callHook(
60
+ HOOK,
61
+ duxtSearchRecords(sources, cached, routing(nuxt))
62
+ );
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Is anybody listening?
68
+ *
69
+ * The payload is a pass over every parsed page of every language, and on a site
70
+ * with no provider layer — which is every site until one is installed — nobody
71
+ * ever reads it. Building it anyway would be the cost this layer charges for a
72
+ * feature nobody turned on.
73
+ *
74
+ * Hookable keeps its registry private, so this reads a field it is not offered.
75
+ * FAILING OPEN is the point of the shape: a registry that cannot be read costs
76
+ * one wasted pass, while a wrong `false` means the hook silently never fires —
77
+ * and a hook that never fires is a bug with no symptom, which is the failure
78
+ * mode this layer spends most of its comments avoiding.
79
+ */
80
+ function listening(nuxt: Nuxt): boolean {
81
+ const registry = (
82
+ nuxt.hooks as unknown as {
83
+ _hooks?: Record<string, unknown[] | undefined>;
84
+ }
85
+ )._hooks;
86
+
87
+ return !registry || Boolean(registry[HOOK]?.length);
88
+ }
89
+
90
+ /**
91
+ * What the site's routing does to a path, read where it is actually true.
92
+ *
93
+ * `nuxt.options.i18n.locales` is the WRONG list and looks like the right one.
94
+ * @nuxtjs/i18n collects `i18n.locales` from every layer itself and never reads
95
+ * the merged copy, which is why `restrictLocales` in `modules/config.ts`
96
+ * filters the per-layer configs instead — and therefore why a site that narrowed
97
+ * itself to two languages still has seven in `nuxt.options.i18n`. Reading that
98
+ * would index every translation the layer ships at URLs the site does not route.
99
+ */
100
+ function routing(nuxt: Nuxt): DuxtSearchRecordOptions {
101
+ const codeOf = (locale: unknown) =>
102
+ typeof locale === 'string'
103
+ ? locale
104
+ : (locale as { code?: string } | null)?.code;
105
+
106
+ const locales = [
107
+ ...new Set(
108
+ nuxt.options._layers
109
+ .flatMap(
110
+ (entry) =>
111
+ (entry.config as { i18n?: { locales?: unknown[] } }).i18n
112
+ ?.locales ?? []
113
+ )
114
+ .map(codeOf)
115
+ .filter(Boolean) as string[]
116
+ )
117
+ ];
118
+
119
+ const i18n = nuxt.options.i18n as
120
+ | { defaultLocale?: string; strategy?: string }
121
+ | undefined;
122
+
123
+ // A consumer may have dropped i18n entirely. One language, no segment, and
124
+ // the content is whatever language it was written in.
125
+ const defaultLocale = i18n?.defaultLocale ?? locales[0] ?? 'en';
126
+
127
+ return {
128
+ locales: locales.length ? locales : [defaultLocale],
129
+ defaultLocale,
130
+ strategy: i18n?.strategy as DuxtSearchRecordOptions['strategy']
131
+ };
132
+ }