@kirchdev/duxt 0.3.4-dev-main.41-23ded41 → 0.3.5-dev-main.43-3310f4b

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 +41 -9
  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
package/bruno-zip.ts ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * The collection, as the ZIP a reader downloads.
3
+ *
4
+ * THE CANONICAL ACQUISITION ROUTE, and the reason it exists: the pages are a
5
+ * reference, and a Bruno collection is a thing you OPEN. A reader who wants the
6
+ * requests wants the files, at the exact version and locale the page they are
7
+ * reading was built from — which a link to the repository's default branch
8
+ * cannot give them and a "Fetch in Bruno" link, which clones whatever HEAD is
9
+ * today, cannot either.
10
+ *
11
+ * WRITTEN BY HAND, and the ~90 lines are the point. A ZIP with no compression
12
+ * is a header, the bytes, and a table saying where each one started; the three
13
+ * archivers that would have supplied it are all a dependency this layer's
14
+ * consumers would carry into every build, for a file that is already a tree of
15
+ * small text files where compression buys a few kilobytes. The repository's own
16
+ * rule about a heavy browser dependency says the same thing one level up.
17
+ *
18
+ * STORED, NOT DEFLATED, for that reason: method 0 needs no compressor at all,
19
+ * and `unzip`, macOS Archive Utility, Windows Explorer and Bruno's own import
20
+ * all read it.
21
+ *
22
+ * DETERMINISTIC. Every timestamp is the DOS epoch rather than "now", so a site
23
+ * that rebuilds nightly does not publish a new download for a collection nobody
24
+ * changed — the archive is a function of the files, and of nothing else.
25
+ */
26
+ import type { DuxtSectionInput } from './sections-resolve';
27
+ import { BRUNO_UNPUBLISHED } from './bruno-parse';
28
+
29
+ /** One file in the archive. */
30
+ export interface DuxtZipEntry {
31
+ /** The path inside the archive, `/`-separated. */
32
+ name: string;
33
+ data: string;
34
+ }
35
+
36
+ /**
37
+ * Where a section's archive is served.
38
+ *
39
+ * By COLLECTION rather than by prefix: two languages deliberately claim one
40
+ * prefix, so a prefix names two archives and would serve the wrong one to the
41
+ * second. The collection name is unique per version and per locale, and both
42
+ * the type writing the link and the module emitting the file compute it from
43
+ * here so the two cannot drift.
44
+ */
45
+ export function brunoZipPath(collection: string): string {
46
+ return `${DUXT_BRUNO_ASSETS}/${brunoZipName(collection)}`;
47
+ }
48
+
49
+ /**
50
+ * The directory the archives are served from.
51
+ *
52
+ * Exported because the module that WRITES them needs the same two halves the
53
+ * page's link is built from. Composed rather than spelled out twice: the two
54
+ * agreeing by coincidence is how a download 404s after somebody renames one.
55
+ */
56
+ export const DUXT_BRUNO_ASSETS = '/_duxt/bruno';
57
+
58
+ /** One archive's file name inside that directory. */
59
+ export function brunoZipName(collection: string): string {
60
+ return `${collection}.zip`;
61
+ }
62
+
63
+ /**
64
+ * The files of the collection that are published, in order.
65
+ *
66
+ * `environments/` is left out, and that is not a default a consumer can turn
67
+ * off. An environment file is where a Bruno collection keeps its hosts, tokens
68
+ * and keys — the real one in the wild that this type was written against has
69
+ * `JWT_SECRET` in it — and this archive is served on the open web. The pages
70
+ * already refuse to read one; an archive that shipped it would be the same leak
71
+ * by the other route.
72
+ */
73
+ export function brunoZipEntries(input: DuxtSectionInput): DuxtZipEntry[] {
74
+ return input
75
+ .files()
76
+ .filter((file) => !file.startsWith(`${BRUNO_UNPUBLISHED}/`))
77
+ .map((file) => ({ name: file, data: input.read(file) }));
78
+ }
79
+
80
+ /** The archive, as bytes. */
81
+ export function zipStore(entries: DuxtZipEntry[]): Uint8Array {
82
+ const encoder = new TextEncoder();
83
+ const locals: Uint8Array[] = [];
84
+ const central: Uint8Array[] = [];
85
+
86
+ let offset = 0;
87
+
88
+ for (const entry of entries) {
89
+ const name = encoder.encode(entry.name);
90
+ const data = encoder.encode(entry.data);
91
+ const crc = crc32(data);
92
+
93
+ const local = new Uint8Array(30 + name.length);
94
+ const view = new DataView(local.buffer);
95
+
96
+ view.setUint32(0, 0x04034b50, true); // local file header
97
+ view.setUint16(4, 20, true); // version needed — 2.0, stored
98
+ view.setUint16(6, 0x0800, true); // the name is UTF-8
99
+ view.setUint16(8, 0, true); // method 0: stored
100
+ view.setUint16(10, 0, true); // time — the DOS epoch, see the file header
101
+ view.setUint16(12, 0x0021, true); // date — 1980-01-01
102
+ view.setUint32(14, crc, true);
103
+ view.setUint32(18, data.length, true);
104
+ view.setUint32(22, data.length, true);
105
+ view.setUint16(26, name.length, true);
106
+ view.setUint16(28, 0, true); // no extra field
107
+
108
+ local.set(name, 30);
109
+ locals.push(local, data);
110
+
111
+ const record = new Uint8Array(46 + name.length);
112
+ const entryView = new DataView(record.buffer);
113
+
114
+ entryView.setUint32(0, 0x02014b50, true); // central directory header
115
+ entryView.setUint16(4, 20, true); // version made by
116
+ entryView.setUint16(6, 20, true); // version needed
117
+ entryView.setUint16(8, 0x0800, true);
118
+ entryView.setUint16(10, 0, true);
119
+ entryView.setUint16(12, 0, true);
120
+ entryView.setUint16(14, 0x0021, true);
121
+ entryView.setUint32(16, crc, true);
122
+ entryView.setUint32(20, data.length, true);
123
+ entryView.setUint32(24, data.length, true);
124
+ entryView.setUint16(28, name.length, true);
125
+ entryView.setUint16(30, 0, true); // extra
126
+ entryView.setUint16(32, 0, true); // comment
127
+ entryView.setUint16(34, 0, true); // disk number
128
+ entryView.setUint16(36, 0, true); // internal attributes
129
+ entryView.setUint32(38, 0o100644 << 16, true); // external: a regular file
130
+ entryView.setUint32(42, offset, true);
131
+
132
+ record.set(name, 46);
133
+ central.push(record);
134
+
135
+ offset += local.length + data.length;
136
+ }
137
+
138
+ const directory = concat(central);
139
+ const end = new Uint8Array(22);
140
+ const endView = new DataView(end.buffer);
141
+
142
+ endView.setUint32(0, 0x06054b50, true); // end of central directory
143
+ endView.setUint16(8, entries.length, true);
144
+ endView.setUint16(10, entries.length, true);
145
+ endView.setUint32(12, directory.length, true);
146
+ endView.setUint32(16, offset, true);
147
+
148
+ return concat([...locals, directory, end]);
149
+ }
150
+
151
+ function concat(parts: Uint8Array[]): Uint8Array {
152
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
153
+ const out = new Uint8Array(total);
154
+
155
+ let at = 0;
156
+ for (const part of parts) {
157
+ out.set(part, at);
158
+ at += part.length;
159
+ }
160
+
161
+ return out;
162
+ }
163
+
164
+ /**
165
+ * The table, built once.
166
+ *
167
+ * A ZIP entry carries a CRC-32 of its own contents and readers check it, so
168
+ * this is not optional — an archive with a wrong one extracts with a warning in
169
+ * the best case and is refused in the worst.
170
+ */
171
+ const CRC_TABLE = (() => {
172
+ const table = new Uint32Array(256);
173
+
174
+ for (let index = 0; index < 256; index++) {
175
+ let value = index;
176
+
177
+ for (let bit = 0; bit < 8; bit++) {
178
+ value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
179
+ }
180
+
181
+ table[index] = value >>> 0;
182
+ }
183
+
184
+ return table;
185
+ })();
186
+
187
+ function crc32(data: Uint8Array): number {
188
+ let crc = 0xffffffff;
189
+
190
+ for (const byte of data) {
191
+ crc = CRC_TABLE[(crc ^ byte) & 0xff]! ^ (crc >>> 8);
192
+ }
193
+
194
+ return (crc ^ 0xffffffff) >>> 0;
195
+ }
package/cli.ts ADDED
@@ -0,0 +1,240 @@
1
+ import { defineCommand, renderUsage } from 'citty';
2
+ import type { ArgsDef, CommandDef } from 'citty';
3
+ import { duxtReportOutput } from './report';
4
+ import type { DuxtReport, DuxtReportFormat, DuxtReportOptions } from './report';
5
+
6
+ /**
7
+ * The `duxt` command line, as a function of its arguments.
8
+ *
9
+ * NOT citty's `runMain`, and that is the design rather than an oversight. Its
10
+ * entry point reads `process.argv`, prints to the console and exits the
11
+ * process, which makes every question worth asking here — what did it print,
12
+ * what did it exit with, did it touch the site at all — unaskable from a test.
13
+ * So citty is used for the two things it is genuinely good at, holding the
14
+ * command tree and rendering usage out of it, and the dispatch is ours.
15
+ *
16
+ * The other half of the design is that NOTHING here reads the site. The report
17
+ * arrives through a loader the caller passes in, so every path that prints
18
+ * usage answers before a config or a parse cache is touched — which is the
19
+ * point of a `--help` on a command whose reader has, by definition, not got a
20
+ * working site yet.
21
+ */
22
+ export interface DuxtCliResult {
23
+ stdout: string;
24
+ stderr: string;
25
+ exitCode: number;
26
+ }
27
+
28
+ /** How the command gets its data — a seam, so help never needs a built site. */
29
+ export type DuxtReportLoader = (options?: DuxtReportOptions) => DuxtReport;
30
+
31
+ /**
32
+ * `2` is the command line, `1` is the documentation.
33
+ *
34
+ * Conflating them is what makes a report command unusable in CI: a pipeline
35
+ * that treats every non-zero exit as "the docs are broken" reports a typo in
36
+ * its own script as a documentation failure, and the person who wrote the
37
+ * script is not the person who gets paged.
38
+ */
39
+ const USAGE_ERROR = 2;
40
+
41
+ /* -------------------------------------------------------------------------- */
42
+ /* The command tree */
43
+ /* -------------------------------------------------------------------------- */
44
+
45
+ const help = {
46
+ type: 'boolean',
47
+ alias: 'h',
48
+ description: 'Show this help.'
49
+ } as const;
50
+
51
+ const reportCommand = defineCommand({
52
+ meta: {
53
+ name: 'report',
54
+ description:
55
+ 'Print what your sources became, and what is wrong with them, as Markdown.'
56
+ },
57
+ args: {
58
+ json: {
59
+ type: 'boolean',
60
+ description: 'Print the same data unrendered, as JSON.'
61
+ },
62
+ help
63
+ }
64
+ });
65
+
66
+ const rootCommand = defineCommand({
67
+ meta: {
68
+ name: 'duxt',
69
+ description: 'The duxt documentation layer, from the command line.'
70
+ },
71
+ args: { help },
72
+ subCommands: { report: reportCommand }
73
+ });
74
+
75
+ /**
76
+ * What the usage citty renders from the tree above cannot say.
77
+ *
78
+ * citty renders a command's arguments; it has no section for the two facts
79
+ * that actually stop somebody — what an exit code meant, and why the report is
80
+ * empty on a site nobody has built. Both are the reason this command grew a
81
+ * `--help` at all, so they are appended here rather than left to the README,
82
+ * which is exactly where the reader of this text has already failed to look.
83
+ */
84
+ const REPORT_NOTES = `EXIT CODES
85
+
86
+ 0 the documentation has no errors — warnings and translation notes
87
+ are what a source going stale looks like, and do not fail
88
+ 1 the report found errors in the documentation
89
+ 2 the command line could not be understood
90
+
91
+ THE PARSE CACHE
92
+
93
+ The pages come from Content's parse cache, so the site has to have been
94
+ parsed once: run \`nuxt build\` or \`nuxt dev\` first. Without it the sources,
95
+ the collections and the redirects are still printed, and everything that
96
+ needs pages is reported as missing rather than as empty.
97
+ `;
98
+
99
+ const usage = async (command: CommandDef, notes = ''): Promise<string> => {
100
+ const rendered = await renderUsage(
101
+ command,
102
+ command === rootCommand ? undefined : rootCommand
103
+ );
104
+
105
+ return notes ? `${rendered}\n\n${notes}` : rendered;
106
+ };
107
+
108
+ /* -------------------------------------------------------------------------- */
109
+ /* Reading the command line */
110
+ /* -------------------------------------------------------------------------- */
111
+
112
+ const toArray = (value: string | string[] | undefined): string[] =>
113
+ value === undefined ? [] : Array.isArray(value) ? value : [value];
114
+
115
+ /**
116
+ * Every spelling a command accepts, mapped to the one it means.
117
+ *
118
+ * Read off the command tree rather than written out a second time: the usage
119
+ * a reader is shown and the options the parser accepts are then the same list
120
+ * by construction, and cannot drift into a flag that is documented but refused
121
+ * or — the worse direction — accepted but undocumented.
122
+ */
123
+ function spellings(command: CommandDef): Map<string, string> {
124
+ const declared = (command.args ?? {}) as ArgsDef;
125
+ const found = new Map<string, string>();
126
+
127
+ for (const [name, definition] of Object.entries(declared)) {
128
+ found.set(`--${name}`, name);
129
+ for (const alias of toArray(
130
+ (definition as { alias?: string | string[] }).alias
131
+ )) {
132
+ found.set(`-${alias}`, name);
133
+ }
134
+ }
135
+
136
+ return found;
137
+ }
138
+
139
+ /**
140
+ * The flags an invocation set, or the first thing wrong with it.
141
+ *
142
+ * Strict on purpose, and the strictness is the feature: citty — like every
143
+ * other argument parser in this family — collects what it does not recognise
144
+ * rather than refusing it, so `--jsno` parsed, was ignored, and the command
145
+ * printed a confident Markdown report that was not the one that had been asked
146
+ * for. An option that does nothing has to say so.
147
+ */
148
+ function readOptions(
149
+ tokens: string[],
150
+ command: CommandDef
151
+ ): { flags: Set<string> } | { diagnostic: string } {
152
+ const accepted = spellings(command);
153
+ const flags = new Set<string>();
154
+
155
+ for (const token of tokens) {
156
+ if (!token.startsWith('-') || token === '-') {
157
+ return { diagnostic: `Unexpected argument '${token}'.` };
158
+ }
159
+
160
+ // `--json=true` on a flag that carries no value is not a spelling of
161
+ // `--json`; it is somebody expecting a different option to exist.
162
+ const [spelling, ...value] = token.split('=');
163
+ const name = accepted.get(spelling!);
164
+
165
+ if (!name) return { diagnostic: `Unknown option '${spelling}'.` };
166
+ if (value.length) {
167
+ return { diagnostic: `Option '${spelling}' takes no value.` };
168
+ }
169
+ if (flags.has(name)) {
170
+ return { diagnostic: `Duplicate option '${spelling}'.` };
171
+ }
172
+
173
+ flags.add(name);
174
+ }
175
+
176
+ return { flags };
177
+ }
178
+
179
+ const refuse = async (
180
+ diagnostic: string,
181
+ command: CommandDef
182
+ ): Promise<DuxtCliResult> => {
183
+ const name = command === rootCommand ? 'duxt' : 'duxt report';
184
+
185
+ return {
186
+ stdout: '',
187
+ // The diagnostic, not the usage. A wall of text under a one-line mistake
188
+ // buries the line that says what the mistake was, and the reader is one
189
+ // documented keystroke away from the wall if they want it.
190
+ stderr: `${diagnostic}\nRun \`${name} --help\` to see what it takes.`,
191
+ exitCode: USAGE_ERROR
192
+ };
193
+ };
194
+
195
+ /* -------------------------------------------------------------------------- */
196
+ /* The command */
197
+ /* -------------------------------------------------------------------------- */
198
+
199
+ export async function runDuxtCli(
200
+ argv: string[],
201
+ load: DuxtReportLoader
202
+ ): Promise<DuxtCliResult> {
203
+ const [name] = argv;
204
+
205
+ if (name !== undefined && !name.startsWith('-') && name !== 'report') {
206
+ return refuse(`Unknown command '${name}'.`, rootCommand);
207
+ }
208
+
209
+ const command = name === 'report' ? reportCommand : rootCommand;
210
+ const read = readOptions(name === 'report' ? argv.slice(1) : argv, command);
211
+
212
+ if ('diagnostic' in read) return refuse(read.diagnostic, command);
213
+
214
+ // Before `--help` is acted on rather than after: an invocation has to be
215
+ // well formed before anything is done with it, and `--json --help` asks for
216
+ // two different outputs at once. Silently dropping one of them is the
217
+ // behaviour this command was reported for.
218
+ if (read.flags.has('help') && read.flags.size > 1) {
219
+ return refuse(`'--help' cannot be combined with another option.`, command);
220
+ }
221
+
222
+ if (command === rootCommand) {
223
+ // Bare `duxt` prints usage rather than an error: a command run with no
224
+ // arguments at all is somebody asking what it does.
225
+ return { stdout: await usage(rootCommand), stderr: '', exitCode: 0 };
226
+ }
227
+
228
+ if (read.flags.has('help')) {
229
+ return {
230
+ stdout: await usage(reportCommand, REPORT_NOTES),
231
+ stderr: '',
232
+ exitCode: 0
233
+ };
234
+ }
235
+
236
+ const format: DuxtReportFormat = read.flags.has('json') ? 'json' : 'markdown';
237
+ const { output, exitCode } = duxtReportOutput(load(), format);
238
+
239
+ return { stdout: output, stderr: '', exitCode };
240
+ }
@@ -54,6 +54,16 @@ export interface DuxtBuildConfig {
54
54
  * pays for that one grammar.
55
55
  */
56
56
  sampleLanguages?: string[];
57
+ /**
58
+ * The landing page — read here for the LANGUAGES of its code demos only.
59
+ *
60
+ * They are highlighted by the same runtime highlighter as the samples, so
61
+ * `modules/config.ts` adds each named language to its grammar set. Typed
62
+ * structurally for the reason `requestSamples` is.
63
+ */
64
+ landing?: {
65
+ showcase?: { demo?: { files?: { language?: string }[] } }[];
66
+ };
57
67
  }
58
68
 
59
69
  /**
@@ -76,7 +86,14 @@ export interface DuxtBuildConfig {
76
86
  export function readDuxtBuildConfig(
77
87
  dirs: string[]
78
88
  ): DuxtBuildConfig | undefined {
79
- const jiti = createJiti(import.meta.url, { interopDefault: true });
89
+ // NO MODULE CACHE. `nuxi dev` restarts Nuxt inside the same process when a
90
+ // config file changes, and jiti's default hands every later read the module
91
+ // it loaded first — so an edited `app.config.ts` kept building collections
92
+ // from the options the dev server started with, until the process was killed.
93
+ const jiti = createJiti(import.meta.url, {
94
+ interopDefault: true,
95
+ moduleCache: false
96
+ });
80
97
 
81
98
  // `defineAppConfig` is a Nuxt auto-import that does not exist in plain Node.
82
99
  // It is the identity function, so a stub is the whole of it.
package/frontmatter.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A frontmatter block, written and taken off again.
3
+ *
4
+ * Both halves live here because both sides of the layer need one of them and
5
+ * neither may drift from the other: the generated sections WRITE every page's
6
+ * block, and the copy button, `llms-full.txt`, the `.md` twin and the MCP tools
7
+ * READ the prose back out from under it. Before this the writer existed three
8
+ * times — one copy already disagreed about empty values — and the reader twice,
9
+ * once with a comment claiming it was the other.
10
+ *
11
+ * Pure and dependency-free, so the app bundle can import it as well as Nitro
12
+ * and the build.
13
+ */
14
+
15
+ /**
16
+ * A frontmatter block YAML can read back.
17
+ *
18
+ * Every value is written as a JSON string, which is also a YAML double-quoted
19
+ * scalar — so a title carrying a colon cannot end the mapping early, the exact
20
+ * failure `tests/frontmatter-yaml.test.ts` exists over. `GET /pets/{petId}: not
21
+ * found` is the shape that breaks an unquoted one.
22
+ *
23
+ * An absent or empty value writes no key at all: `title: ""` is not a title,
24
+ * and Content would read it as one rather than falling back to the heading.
25
+ */
26
+ export function frontmatterBlock(
27
+ fields: Record<string, string | undefined>
28
+ ): string {
29
+ return [
30
+ '---',
31
+ ...Object.entries(fields)
32
+ .filter(([, value]) => value !== undefined && value !== '')
33
+ .map(([key, value]) => `${key}: ${JSON.stringify(value)}`),
34
+ '---'
35
+ ].join('\n');
36
+ }
37
+
38
+ /**
39
+ * The Markdown body without its frontmatter block.
40
+ *
41
+ * Content's `rawbody` is the file as it sits on disk, frontmatter included. A
42
+ * reader wants the prose; a model handed `---\ntitle: …\n---` reads a page
43
+ * whose first heading is a YAML fence.
44
+ */
45
+ export function stripFrontmatter(body: string): string {
46
+ const match = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/.exec(body);
47
+
48
+ return match ? body.slice(match[0].length) : body;
49
+ }