@kirchdev/duxt 0.3.4 → 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.
- package/README.md +24 -9
- package/app/app.vue +17 -11
- package/app/assets/css/duxt.css +47 -6
- package/app/components/DuxtAnnouncements.vue +117 -0
- package/app/components/DuxtBrunoEntries.vue +62 -0
- package/app/components/DuxtCodeBlock.vue +18 -34
- package/app/components/DuxtCodeToolbar.vue +14 -0
- package/app/components/DuxtCopyButton.vue +33 -0
- package/app/components/DuxtCopyPage.vue +32 -30
- package/app/components/DuxtFooter.vue +39 -8
- package/app/components/DuxtHeader.vue +107 -48
- package/app/components/DuxtLandingProse.vue +1 -1
- package/app/components/DuxtLinkLabel.vue +36 -0
- package/app/components/DuxtLiveWindow.vue +8 -8
- package/app/components/DuxtLocale.vue +10 -1
- package/app/components/DuxtNavigation.vue +2 -2
- package/app/components/DuxtOpenApiClient.vue +285 -90
- package/app/components/DuxtOpenApiMedia.vue +30 -18
- package/app/components/DuxtOpenApiSchema.vue +10 -7
- package/app/components/DuxtPageFeedback.vue +7 -0
- package/app/components/DuxtPageInfo.vue +15 -12
- package/app/components/DuxtPageNav.vue +41 -27
- package/app/components/DuxtPreviewSkeleton.vue +4 -4
- package/app/components/DuxtProgress.vue +2 -14
- package/app/components/DuxtSearch.vue +230 -90
- package/app/components/DuxtSearchInput.vue +70 -0
- package/app/components/DuxtSections.vue +11 -1
- package/app/components/DuxtSegmented.vue +64 -0
- package/app/components/DuxtShortcuts.vue +16 -11
- package/app/components/DuxtShortcutsTrigger.vue +49 -0
- package/app/components/DuxtSkipLink.vue +1 -1
- package/app/components/DuxtToc.vue +16 -10
- package/app/components/DuxtTranslationBanner.vue +1 -1
- package/app/components/DuxtVersion.vue +53 -12
- package/app/components/DuxtVersionBanner.vue +5 -5
- package/app/components/content/AccordionItem.vue +2 -2
- package/app/components/content/BrunoOverview.vue +260 -0
- package/app/components/content/BrunoRequest.vue +141 -0
- package/app/components/content/BrunoRequests.vue +104 -0
- package/app/components/content/Callout.vue +5 -5
- package/app/components/content/ChangelogReleases.vue +161 -40
- package/app/components/content/CodeGroup.vue +41 -37
- package/app/components/content/Field.vue +1 -1
- package/app/components/content/FileTree.vue +1 -1
- package/app/components/content/OpenApiOperation.vue +4 -4
- package/app/components/content/OpenApiOperations.vue +1 -1
- package/app/components/content/OpenApiOverview.vue +1 -1
- package/app/components/content/PackageManagers.vue +42 -58
- package/app/components/content/PageCards.vue +7 -4
- package/app/components/content/Preview.vue +15 -39
- package/app/components/content/ProseImg.vue +596 -49
- package/app/components/content/Since.vue +1 -1
- package/app/components/content/Steps.vue +6 -4
- package/app/components/content/TabGroup.vue +13 -12
- package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +1 -1
- package/app/components/ui/command/CommandShortcut.vue +1 -1
- package/app/components/ui/dialog/DialogContent.vue +2 -2
- package/app/components/ui/dialog/DialogHeader.vue +1 -1
- package/app/components/ui/dialog/DialogScrollContent.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +2 -2
- package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +2 -2
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +2 -2
- package/app/components/ui/navigation-menu/NavigationMenuContent.vue +1 -1
- package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +1 -1
- package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +1 -1
- package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
- package/app/components/ui/select/SelectItem.vue +2 -2
- package/app/components/ui/sheet/SheetContent.vue +29 -3
- package/app/components/ui/sidebar/Sidebar.vue +28 -4
- package/app/components/ui/sidebar/SidebarGroupAction.vue +1 -1
- package/app/components/ui/sidebar/SidebarInset.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuAction.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuBadge.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuSub.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuSubButton.vue +1 -1
- package/app/components/ui/sidebar/SidebarProvider.vue +7 -16
- package/app/components/ui/sidebar/index.ts +10 -2
- package/app/components/ui/sidebar/utils.ts +0 -1
- package/app/components/ui/sonner/Sonner.vue +18 -0
- package/app/components/ui/tabs/TabsList.vue +13 -3
- package/app/components/ui/tabs/TabsTrigger.vue +24 -3
- package/app/components/ui/tooltip/TooltipContent.vue +2 -2
- package/app/composables/useActiveHeading.ts +3 -25
- package/app/composables/useDuxtAnalytics.ts +50 -0
- package/app/composables/useDuxtAnimatedHeight.ts +8 -38
- package/app/composables/useDuxtAnnouncements.ts +153 -0
- package/app/composables/useDuxtCopy.ts +51 -0
- package/app/composables/useDuxtDirection.ts +23 -0
- package/app/composables/useDuxtNavigation.ts +28 -8
- package/app/composables/useDuxtPath.ts +3 -5
- package/app/composables/useDuxtSearch.ts +87 -35
- package/app/composables/useDuxtSection.ts +15 -1
- package/app/composables/useDuxtSectionRow.ts +6 -5
- package/app/composables/useDuxtShortcuts.ts +319 -20
- package/app/composables/useDuxtVersion.ts +57 -6
- package/app/composables/useDuxtViewportMeasure.ts +37 -0
- package/app/composables/useFuzzySearch.ts +9 -4
- package/app/composables/useRecentPages.ts +14 -17
- package/app/error.vue +112 -74
- package/app/layouts/changelog.vue +9 -1
- package/app/layouts/default.vue +9 -0
- package/app/layouts/docs.vue +9 -1
- package/app/layouts/reference.vue +9 -1
- package/app/middleware/duxt-section-layout.global.ts +1 -3
- package/app/pages/[...slug].vue +146 -35
- package/app/pages/index.vue +15 -23
- package/app/types/duxt.d.ts +312 -4
- package/app/utils/analytics.ts +91 -0
- package/app/utils/announcements.ts +205 -0
- package/app/utils/changelog.ts +0 -28
- package/app/utils/code-highlight.ts +3 -1
- package/app/utils/contributor-avatar.ts +27 -0
- package/app/utils/direction.ts +43 -0
- package/app/utils/duxt-config.ts +43 -0
- package/app/utils/duxt-text.ts +10 -0
- package/app/utils/format-date.ts +28 -0
- package/app/utils/generated-sections.ts +68 -12
- package/app/utils/generated-toc.ts +14 -7
- package/app/utils/locale-path.ts +43 -0
- package/app/utils/navigation-tree.ts +51 -1
- package/app/utils/nearest-page.ts +8 -0
- package/app/utils/openapi.ts +16 -11
- package/app/utils/page-controls.ts +215 -0
- package/app/utils/pill.ts +61 -0
- package/app/utils/prose-image.ts +128 -0
- package/app/utils/search-context.ts +110 -0
- package/app/utils/search-display.ts +248 -0
- package/app/utils/section-scope.ts +65 -4
- package/app/utils/stored-json.ts +31 -0
- package/app/utils/version-choices.ts +6 -13
- package/app/utils/version-paths.ts +21 -0
- package/bin/duxt-cache-key.mjs +61 -0
- package/bin/duxt-og-cache.mjs +58 -0
- package/bin/{duxt-report.mjs → duxt.mjs} +13 -7
- package/bruno-model.ts +169 -0
- package/bruno-parse.ts +656 -0
- package/bruno-zip.ts +195 -0
- package/cli.ts +240 -0
- package/duxt-app-config.ts +18 -1
- package/frontmatter.ts +49 -0
- package/git-contributors.ts +322 -0
- package/i18n/locales/de/duxt/announcement.json +7 -0
- package/i18n/locales/de/duxt/bruno.json +25 -0
- package/i18n/locales/de/duxt/changelog.json +1 -1
- package/i18n/locales/de/duxt/code.json +2 -1
- package/i18n/locales/de/duxt/page.json +9 -0
- package/i18n/locales/de/duxt/shortcuts.json +1 -0
- package/i18n/locales/de/duxt/theme.json +2 -1
- package/i18n/locales/en/duxt/announcement.json +7 -0
- package/i18n/locales/en/duxt/bruno.json +25 -0
- package/i18n/locales/en/duxt/changelog.json +1 -1
- package/i18n/locales/en/duxt/code.json +2 -1
- package/i18n/locales/en/duxt/page.json +9 -0
- package/i18n/locales/en/duxt/shortcuts.json +1 -0
- package/i18n/locales/en/duxt/theme.json +2 -1
- package/i18n/locales/es/duxt/announcement.json +7 -0
- package/i18n/locales/es/duxt/bruno.json +25 -0
- package/i18n/locales/es/duxt/changelog.json +1 -1
- package/i18n/locales/es/duxt/code.json +2 -1
- package/i18n/locales/es/duxt/page.json +9 -0
- package/i18n/locales/es/duxt/shortcuts.json +1 -0
- package/i18n/locales/es/duxt/theme.json +2 -1
- package/i18n/locales/fr/duxt/announcement.json +7 -0
- package/i18n/locales/fr/duxt/bruno.json +25 -0
- package/i18n/locales/fr/duxt/changelog.json +1 -1
- package/i18n/locales/fr/duxt/code.json +2 -1
- package/i18n/locales/fr/duxt/page.json +9 -0
- package/i18n/locales/fr/duxt/shortcuts.json +1 -0
- package/i18n/locales/fr/duxt/theme.json +2 -1
- package/i18n/locales/pt/duxt/announcement.json +7 -0
- package/i18n/locales/pt/duxt/bruno.json +25 -0
- package/i18n/locales/pt/duxt/changelog.json +1 -1
- package/i18n/locales/pt/duxt/code.json +2 -1
- package/i18n/locales/pt/duxt/page.json +9 -0
- package/i18n/locales/pt/duxt/shortcuts.json +1 -0
- package/i18n/locales/pt/duxt/theme.json +2 -1
- package/modules/bruno.ts +118 -0
- package/modules/config.ts +26 -0
- package/modules/devtools.ts +2 -3
- package/modules/git-meta.ts +16 -47
- package/modules/search-records.ts +132 -0
- package/nuxt.config.ts +81 -7
- package/og-image-cache.ts +742 -0
- package/package.json +41 -9
- package/public/devtools/checks.html +2 -2
- package/public/devtools/config.html +59 -4
- package/public/devtools/pages.html +8 -1
- package/public/devtools/paths.html +1 -1
- package/public/devtools/sources.html +2 -2
- package/public/devtools/versions.html +3 -0
- package/report.ts +28 -19
- package/search-records.ts +380 -0
- package/section-input.ts +142 -0
- package/section-markdown.ts +67 -0
- package/section-reports.ts +10 -4
- package/sections-bruno.ts +571 -0
- package/sections-changelog.ts +76 -43
- package/sections-openapi.ts +12 -83
- package/sections-resolve.ts +174 -8
- package/sections.ts +33 -10
- package/server/devtools/shell.ts +1 -1
- package/server/mcp/tools/list-pages.ts +77 -36
- package/server/mcp/tools/list-versions.ts +84 -0
- package/server/mcp/tools/read-page.ts +80 -26
- package/server/mcp/tools/search-docs.ts +87 -53
- package/server/middleware/raw-markdown.ts +1 -1
- package/server/routes/llms-full.txt.get.ts +6 -8
- package/server/routes/llms.txt.get.ts +1 -1
- package/server/utils/duxt-server-text.ts +0 -13
- package/server/utils/llms-pages.ts +24 -9
- package/server/utils/mcp-docs.ts +577 -0
- package/sources-cache.ts +376 -0
- package/sources-git.ts +186 -31
- package/sources-resolve.ts +59 -1
- package/sources.ts +122 -13
- package/tfplugindocs.ts +158 -0
package/sections-changelog.ts
CHANGED
|
@@ -18,15 +18,29 @@
|
|
|
18
18
|
* the one page it was written as, for a project that just wants it shown. The
|
|
19
19
|
* cost of two rendering paths was weighed and accepted, and the difference runs
|
|
20
20
|
* further than the page count: a flat changelog is an ORDINARY docs page and
|
|
21
|
-
* keeps the docs
|
|
21
|
+
* keeps the docs shell, which is why `layout` is answered per declaration.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
23
|
+
* Text in, files out — plus ONE FACT THE FILE DOES NOT HOLD. A release is a
|
|
24
|
+
* range of commits, and who wrote them is in the repository rather than in the
|
|
25
|
+
* changelog; the authors of the changelog FILE are whoever ran the release
|
|
26
|
+
* tool, which is not the same set of people and is the wrong answer given
|
|
27
|
+
* confidently. So where an input names a checkout the build may read
|
|
28
|
+
* (`DuxtSectionInput.root`) the people behind each tag are read out of git;
|
|
29
|
+
* where it does not — a remote clone, a test, a fixture — nothing is read and
|
|
30
|
+
* no release names anybody. What the split history LOOKS like — the timeline,
|
|
24
31
|
* the group badges, the filters — belongs to `layouts/changelog.vue` and the
|
|
25
32
|
* two components this file writes calls to.
|
|
26
33
|
*/
|
|
27
34
|
import { stringify as stringifyYaml } from 'yaml';
|
|
35
|
+
import { frontmatterBlock } from './frontmatter';
|
|
36
|
+
import type { DuxtContributor } from './git-contributors';
|
|
37
|
+
import {
|
|
38
|
+
contributorsForVersion,
|
|
39
|
+
releaseContributors
|
|
40
|
+
} from './git-contributors';
|
|
28
41
|
import type {
|
|
29
42
|
DuxtSectionContext,
|
|
43
|
+
DuxtSectionInput,
|
|
30
44
|
DuxtSectionOptions,
|
|
31
45
|
DuxtSectionPage,
|
|
32
46
|
DuxtSectionType
|
|
@@ -39,7 +53,7 @@ import { slugify } from './sources-resolve';
|
|
|
39
53
|
* A layout and two MDC components, because that is the whole of what a
|
|
40
54
|
* consumer can override: dropping a `ChangelogGroup.vue` of their own into
|
|
41
55
|
* `app/components/content/` replaces the layer's, exactly as it does for a
|
|
42
|
-
* callout, and a `layouts/changelog.vue` replaces the
|
|
56
|
+
* callout, and a `layouts/changelog.vue` replaces the shell around it.
|
|
43
57
|
*/
|
|
44
58
|
export const DUXT_CHANGELOG_LAYOUT = 'changelog';
|
|
45
59
|
export const DUXT_CHANGELOG_RELEASES = 'changelog-releases';
|
|
@@ -119,14 +133,22 @@ interface Group {
|
|
|
119
133
|
export const changelogSectionType: DuxtSectionType = {
|
|
120
134
|
parse: parseChangelog,
|
|
121
135
|
/**
|
|
122
|
-
*
|
|
136
|
+
* PER VERSION, like the documentation and the API reference beside it.
|
|
123
137
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* the
|
|
138
|
+
* Every documentation version publishes the CHANGELOG.md it shipped with, at
|
|
139
|
+
* a route inside that version — `/v2.x/releases` beside `/v2.x/guides`. It
|
|
140
|
+
* used to be one global history at a version-neutral URL, and that is what
|
|
141
|
+
* made the switcher special: it could not keep a reader in the changelog
|
|
142
|
+
* when they chose another version. Each copy holds everything its own file
|
|
143
|
+
* holds, earlier releases included, which is exactly what that version knew.
|
|
128
144
|
*/
|
|
129
|
-
versioning: '
|
|
145
|
+
versioning: 'per-version',
|
|
146
|
+
/**
|
|
147
|
+
* REQUIRED in every published version. A version with no CHANGELOG.md is a
|
|
148
|
+
* build error: leaving the version out, or falling back to another version's
|
|
149
|
+
* history, would both publish something the version never shipped.
|
|
150
|
+
*/
|
|
151
|
+
artefact: 'required',
|
|
130
152
|
/**
|
|
131
153
|
* The original, in every language.
|
|
132
154
|
*
|
|
@@ -141,7 +163,7 @@ export const changelogSectionType: DuxtSectionType = {
|
|
|
141
163
|
*
|
|
142
164
|
* The split history is a timeline: the releases in the sidebar, a page per
|
|
143
165
|
* release, no table of contents over four bullet points. The flat file is a
|
|
144
|
-
* long ordinary page, and the docs
|
|
166
|
+
* long ordinary page, and the docs shell is exactly what it wants — a
|
|
145
167
|
* contents column listing the releases most of all. Same type, two products,
|
|
146
168
|
* so the question is answered from the declaration's own options.
|
|
147
169
|
*/
|
|
@@ -188,10 +210,10 @@ function granularityOf(options: DuxtSectionOptions): Granularity {
|
|
|
188
210
|
}
|
|
189
211
|
|
|
190
212
|
function parseChangelog(
|
|
191
|
-
|
|
213
|
+
input: DuxtSectionInput,
|
|
192
214
|
context: DuxtSectionContext
|
|
193
215
|
): DuxtSectionPage[] {
|
|
194
|
-
const lines =
|
|
216
|
+
const lines = input.text().split(/\r?\n/);
|
|
195
217
|
const headings = headingsOf(lines);
|
|
196
218
|
const starts = headings.filter((heading) => release(heading.text));
|
|
197
219
|
|
|
@@ -222,7 +244,15 @@ function parseChangelog(
|
|
|
222
244
|
const width = String(releases.length).length;
|
|
223
245
|
|
|
224
246
|
return [
|
|
225
|
-
|
|
247
|
+
// Read once for the whole file, not once per release: the answer is a map
|
|
248
|
+
// over every tag the checkout has, and asking git per release would be one
|
|
249
|
+
// process per release for the same bytes.
|
|
250
|
+
index(
|
|
251
|
+
preamble,
|
|
252
|
+
releases,
|
|
253
|
+
context,
|
|
254
|
+
input.root ? releaseContributors(input.root) : undefined
|
|
255
|
+
),
|
|
226
256
|
...releases.map((entry, position) =>
|
|
227
257
|
page(entry, String(position + 1).padStart(width, '0'))
|
|
228
258
|
)
|
|
@@ -246,7 +276,7 @@ function flat(
|
|
|
246
276
|
return {
|
|
247
277
|
file: 'index.md',
|
|
248
278
|
body: [
|
|
249
|
-
|
|
279
|
+
frontmatterBlock({ title: context.label, release: newest }),
|
|
250
280
|
'',
|
|
251
281
|
...trim(withoutTitle(lines)),
|
|
252
282
|
''
|
|
@@ -424,20 +454,40 @@ function entries(lines: string[]): number {
|
|
|
424
454
|
function index(
|
|
425
455
|
preamble: string[],
|
|
426
456
|
releases: Release[],
|
|
427
|
-
context: DuxtSectionContext
|
|
457
|
+
context: DuxtSectionContext,
|
|
458
|
+
contributors?: Map<string, DuxtContributor[]>
|
|
428
459
|
): DuxtSectionPage {
|
|
429
460
|
const body = trim(withoutTitle(preamble));
|
|
430
461
|
|
|
431
462
|
const props = {
|
|
432
|
-
releases: releases.map((entry) =>
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
463
|
+
releases: releases.map((entry) => {
|
|
464
|
+
const people = contributorsForVersion(contributors, entry.version);
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
version: entry.version,
|
|
468
|
+
date: entry.date,
|
|
469
|
+
to: `${context.prefix}/${segment(entry.version)}`,
|
|
470
|
+
groups: groupsOf(trim(entry.body)).groups.map((group) => ({
|
|
471
|
+
name: group.name,
|
|
472
|
+
count: group.count
|
|
473
|
+
})),
|
|
474
|
+
// A NAME AND, WHERE GIT CARRIES ONE, A HANDLE. Not the address the
|
|
475
|
+
// identity was computed from: these props are written into a page that
|
|
476
|
+
// is prerendered, crawled, indexed, put into `llms-full.txt` and handed
|
|
477
|
+
// to a model on request, and an email that reaches all of that is a
|
|
478
|
+
// different object from the same email inside a commit. Not the commit
|
|
479
|
+
// COUNT either — the order already carries it, and a per-release tally
|
|
480
|
+
// is a number nobody asked this page for.
|
|
481
|
+
...(people?.length
|
|
482
|
+
? {
|
|
483
|
+
contributors: people.map((person) => ({
|
|
484
|
+
name: person.name,
|
|
485
|
+
...(person.username ? { username: person.username } : {})
|
|
486
|
+
}))
|
|
487
|
+
}
|
|
488
|
+
: {})
|
|
489
|
+
};
|
|
490
|
+
})
|
|
441
491
|
};
|
|
442
492
|
|
|
443
493
|
return {
|
|
@@ -447,7 +497,7 @@ function index(
|
|
|
447
497
|
// props below. Keep the newest release beside the page title instead:
|
|
448
498
|
// Content exposes frontmatter on the page query that `DuxtVersion` uses,
|
|
449
499
|
// and parsing the file again there would make the two answers drift.
|
|
450
|
-
|
|
500
|
+
frontmatterBlock({ title: context.label, release: releases[0]?.version }),
|
|
451
501
|
'',
|
|
452
502
|
// NO `<h1>` OF ITS OWN. The page draws the docs header — breadcrumb,
|
|
453
503
|
// title, description, the copy control beside it — for exactly the
|
|
@@ -479,7 +529,7 @@ function page(entry: Release, order: string): DuxtSectionPage {
|
|
|
479
529
|
return {
|
|
480
530
|
file: `${order}.${segment(entry.version)}.md`,
|
|
481
531
|
body: [
|
|
482
|
-
|
|
532
|
+
frontmatterBlock({
|
|
483
533
|
title: entry.version,
|
|
484
534
|
date: entry.date,
|
|
485
535
|
compare: entry.compare
|
|
@@ -635,20 +685,3 @@ function component(
|
|
|
635
685
|
fence
|
|
636
686
|
].join('\n');
|
|
637
687
|
}
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* A frontmatter block YAML can read back.
|
|
641
|
-
*
|
|
642
|
-
* Every value is written as a JSON string, which is also a YAML double-quoted
|
|
643
|
-
* scalar — so a release title carrying a colon cannot end the mapping early,
|
|
644
|
-
* which is the exact failure `tests/frontmatter-yaml.test.ts` exists over.
|
|
645
|
-
*/
|
|
646
|
-
function frontmatter(fields: Record<string, string | undefined>): string {
|
|
647
|
-
return [
|
|
648
|
-
'---',
|
|
649
|
-
...Object.entries(fields)
|
|
650
|
-
.filter(([, value]) => value !== undefined)
|
|
651
|
-
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`),
|
|
652
|
-
'---'
|
|
653
|
-
].join('\n');
|
|
654
|
-
}
|
package/sections-openapi.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The second type in the registry, and the first one that answers the questions
|
|
5
5
|
* the registry was built to ask. A changelog is one global history in the
|
|
6
6
|
* language it was written in; a reference is **per version** and **per locale**,
|
|
7
|
-
* and it needs a **layout** the docs
|
|
7
|
+
* and it needs a **layout** the docs shell does not give it. Those three
|
|
8
8
|
* differences are policies on the type rather than branches in the scaffold,
|
|
9
9
|
* which is exactly what #9 claimed they would be — so this file adds a parser
|
|
10
10
|
* and three declarations, and nothing else.
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
* responses, the try-it client — travels as props.
|
|
25
25
|
*/
|
|
26
26
|
import { stringify as stringifyYaml } from 'yaml';
|
|
27
|
+
import { frontmatterBlock } from './frontmatter';
|
|
28
|
+
import { firstLine, pageOrder, urlSegment } from './section-markdown';
|
|
27
29
|
import type {
|
|
28
30
|
DuxtOpenApiOperation,
|
|
29
31
|
DuxtOpenApiSecurity,
|
|
@@ -34,6 +36,7 @@ import type {
|
|
|
34
36
|
import { compactOpenApi, parseOpenApiDocument } from './openapi-parse';
|
|
35
37
|
import type {
|
|
36
38
|
DuxtSectionContext,
|
|
39
|
+
DuxtSectionInput,
|
|
37
40
|
DuxtSectionPage,
|
|
38
41
|
DuxtSectionType
|
|
39
42
|
} from './sections-resolve';
|
|
@@ -75,13 +78,13 @@ export const openapiSectionType: DuxtSectionType = {
|
|
|
75
78
|
};
|
|
76
79
|
|
|
77
80
|
function parseOpenApiSection(
|
|
78
|
-
|
|
81
|
+
input: DuxtSectionInput,
|
|
79
82
|
context: DuxtSectionContext
|
|
80
83
|
): DuxtSectionPage[] {
|
|
81
84
|
let spec: DuxtOpenApiSpec;
|
|
82
85
|
|
|
83
86
|
try {
|
|
84
|
-
spec = parseOpenApiDocument(
|
|
87
|
+
spec = parseOpenApiDocument(input.text());
|
|
85
88
|
} catch (error) {
|
|
86
89
|
// Rethrown rather than swallowed into an empty section: the scaffold turns
|
|
87
90
|
// "no pages" into the right severity for local and remote alike, but a
|
|
@@ -103,8 +106,8 @@ function parseOpenApiSection(
|
|
|
103
106
|
|
|
104
107
|
const groups = spec.tags.map((tag, index) => ({
|
|
105
108
|
tag,
|
|
106
|
-
slug:
|
|
107
|
-
order:
|
|
109
|
+
slug: urlSegment(tag.name),
|
|
110
|
+
order: pageOrder(index, spec.tags!.length)
|
|
108
111
|
}));
|
|
109
112
|
|
|
110
113
|
// A tag whose name slugifies to the same segment as another's would serve
|
|
@@ -228,7 +231,7 @@ function pagesFor(
|
|
|
228
231
|
group,
|
|
229
232
|
operation,
|
|
230
233
|
slugs[position]!,
|
|
231
|
-
|
|
234
|
+
pageOrder(position, width)
|
|
232
235
|
)
|
|
233
236
|
)
|
|
234
237
|
];
|
|
@@ -322,8 +325,8 @@ function operationSlugs(operations: DuxtOpenApiOperation[]): string[] {
|
|
|
322
325
|
|
|
323
326
|
return operations.map((operation) => {
|
|
324
327
|
const base =
|
|
325
|
-
|
|
326
|
-
|
|
328
|
+
urlSegment(operation.operationId ?? '') ||
|
|
329
|
+
urlSegment(`${operation.method}-${operation.path}`) ||
|
|
327
330
|
operation.method;
|
|
328
331
|
|
|
329
332
|
let candidate = base;
|
|
@@ -335,62 +338,6 @@ function operationSlugs(operations: DuxtOpenApiOperation[]): string[] {
|
|
|
335
338
|
});
|
|
336
339
|
}
|
|
337
340
|
|
|
338
|
-
/**
|
|
339
|
-
* A URL segment: lowercase, no dots.
|
|
340
|
-
*
|
|
341
|
-
* The dots matter. Content reads a name made of digits and dots as a version
|
|
342
|
-
* and stops refining it, which would leave the `NN.` ordering prefix in the
|
|
343
|
-
* URL — the same trap `sections-changelog.ts` answers with a leading `v`. An
|
|
344
|
-
* endpoint at `/v1.0/pets` walks straight into it, so the dot goes.
|
|
345
|
-
*/
|
|
346
|
-
function segment(value: string): string {
|
|
347
|
-
return value
|
|
348
|
-
.toLowerCase()
|
|
349
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
350
|
-
.replace(/^-+|-+$/g, '');
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** Zero-padded so ten pages still sort the way they read. */
|
|
354
|
-
function order(index: number, total: number): string {
|
|
355
|
-
return String(index + 1).padStart(String(total).length, '0');
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* The first paragraph of a description, as one line of PLAIN text.
|
|
360
|
-
*
|
|
361
|
-
* Plain matters because of where this goes: a page's `description`
|
|
362
|
-
* frontmatter, which becomes the meta description, the og tag and the card in
|
|
363
|
-
* a search result, and the summary on a group card. A CommonMark description
|
|
364
|
-
* carrying `*emphasis*` or a `[link](url)` renders as prose everywhere it is
|
|
365
|
-
* rendered as prose — and as its own punctuation everywhere it is not.
|
|
366
|
-
*
|
|
367
|
-
* Deliberately small. It undoes the four inline constructs a one-line summary
|
|
368
|
-
* actually meets — emphasis, code spans, links and images — and leaves
|
|
369
|
-
* everything else alone rather than growing into a second Markdown parser
|
|
370
|
-
* whose disagreements with the first would be invisible.
|
|
371
|
-
*/
|
|
372
|
-
function firstLine(value?: string): string | undefined {
|
|
373
|
-
const line = plain(value?.split(/\n\s*\n/)[0] ?? '')
|
|
374
|
-
.replace(/\s+/g, ' ')
|
|
375
|
-
.trim();
|
|
376
|
-
|
|
377
|
-
return line || undefined;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** Inline Markdown, as the text it renders to. */
|
|
381
|
-
function plain(value: string): string {
|
|
382
|
-
return (
|
|
383
|
-
value
|
|
384
|
-
// An image before a link: `` is a link with a `!` in front, and
|
|
385
|
-
// taking the link first would leave the `!` behind.
|
|
386
|
-
.replaceAll(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
387
|
-
.replaceAll(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
388
|
-
.replaceAll(/`([^`]*)`/g, '$1')
|
|
389
|
-
.replaceAll(/(\*\*|__)(.+?)\1/g, '$2')
|
|
390
|
-
.replaceAll(/(\*|_)(.+?)\1/g, '$2')
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
341
|
/**
|
|
395
342
|
* An MDC block component with YAML props and Markdown inside it.
|
|
396
343
|
*
|
|
@@ -428,23 +375,5 @@ function page(
|
|
|
428
375
|
fields: Record<string, string | undefined>,
|
|
429
376
|
lines: string[]
|
|
430
377
|
): string {
|
|
431
|
-
return [
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* A frontmatter block YAML can read back.
|
|
436
|
-
*
|
|
437
|
-
* Every value is a JSON string, which is also a YAML double-quoted scalar — so
|
|
438
|
-
* a summary carrying a colon cannot end the mapping early. The rule
|
|
439
|
-
* `tests/frontmatter-yaml.test.ts` exists over, and a title like
|
|
440
|
-
* `GET /pets/{petId}: not found` is exactly the shape that breaks it.
|
|
441
|
-
*/
|
|
442
|
-
function frontmatter(fields: Record<string, string | undefined>): string {
|
|
443
|
-
return [
|
|
444
|
-
'---',
|
|
445
|
-
...Object.entries(fields)
|
|
446
|
-
.filter(([, value]) => value !== undefined && value !== '')
|
|
447
|
-
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`),
|
|
448
|
-
'---'
|
|
449
|
-
].join('\n');
|
|
378
|
+
return [frontmatterBlock(fields), '', ...lines, ''].join('\n');
|
|
450
379
|
}
|
package/sections-resolve.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
resolveSources,
|
|
29
29
|
slugify
|
|
30
30
|
} from './sources-resolve';
|
|
31
|
+
import { brunoSectionType } from './sections-bruno';
|
|
31
32
|
import { changelogSectionType } from './sections-changelog';
|
|
32
33
|
import { openapiSectionType } from './sections-openapi';
|
|
33
34
|
|
|
@@ -167,10 +168,129 @@ export interface DuxtSectionPage {
|
|
|
167
168
|
body: string;
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
/**
|
|
172
|
+
* The artefact a type reads, as the type reads it.
|
|
173
|
+
*
|
|
174
|
+
* A LAZY HANDLE rather than a string, and that is the whole of this seam's
|
|
175
|
+
* second version. `parse` took `artefact: string`, which said one thing the
|
|
176
|
+
* registry had never meant to promise: that an artefact is one file. A
|
|
177
|
+
* changelog is, an OpenAPI document is — a Bruno collection is `bruno.json`
|
|
178
|
+
* beside a directory of `.bru` files, `folder.bru` ordering and an
|
|
179
|
+
* `environments/` folder nobody should publish, and no single string carries
|
|
180
|
+
* it.
|
|
181
|
+
*
|
|
182
|
+
* So the type is handed WHERE its input is and is left to read it. The central
|
|
183
|
+
* resolver keeps the severity policy, the report and the prefix; what a tree
|
|
184
|
+
* means is the type's, which is the only place the knowledge belongs — a
|
|
185
|
+
* resolver that walked a Bruno collection would have to walk the next type's
|
|
186
|
+
* too.
|
|
187
|
+
*
|
|
188
|
+
* `text` throws rather than returning `''` for a directory: a type that asks a
|
|
189
|
+
* directory for its contents has a bug, and an empty string turns it into an
|
|
190
|
+
* empty section reported as a missing artefact three files away.
|
|
191
|
+
*/
|
|
192
|
+
export interface DuxtSectionInput {
|
|
193
|
+
/** The declared path, relative to the source's own root. */
|
|
194
|
+
path: string;
|
|
195
|
+
/**
|
|
196
|
+
* The checkout `path` resolves against, where the artefact was read out of a
|
|
197
|
+
* repository whose history this build is allowed to read.
|
|
198
|
+
*
|
|
199
|
+
* ABSENT IS THE DEFAULT, AND MEANS "DO NOT LOOK". It is filled in only for an
|
|
200
|
+
* artefact read out of the repository being built. A remote source is a
|
|
201
|
+
* `--depth 1` clone, where every commit looks as though one person wrote it
|
|
202
|
+
* — wrong data rather than missing data, which is the line
|
|
203
|
+
* `modules/git-meta.ts` already draws and for the same reason. The one
|
|
204
|
+
* exception is a remote source that IS the repository being built: the
|
|
205
|
+
* artefact still comes from the download, but this points at the local
|
|
206
|
+
* checkout, which carries the same tags with their whole history — see
|
|
207
|
+
* `localHistoryFor`. An in-memory
|
|
208
|
+
* input leaves it unset too, so a type under test never reaches a real
|
|
209
|
+
* repository and a fixture version that happens to match a real tag cannot
|
|
210
|
+
* quietly pick up real people.
|
|
211
|
+
*
|
|
212
|
+
* A type that only turns a file into pages never asks. The release history
|
|
213
|
+
* does: which people a release carries is a fact about the commits between
|
|
214
|
+
* two tags, and no changelog file holds it.
|
|
215
|
+
*/
|
|
216
|
+
root?: string;
|
|
217
|
+
/** Whether the declared path is one file or a tree. */
|
|
218
|
+
kind: 'file' | 'directory';
|
|
219
|
+
/** The whole file. Throws when the input is a directory. */
|
|
220
|
+
text: () => string;
|
|
221
|
+
/**
|
|
222
|
+
* Every file under a directory input, as `/`-separated relative paths.
|
|
223
|
+
*
|
|
224
|
+
* Sorted, so a type that walks them produces the same pages on every machine
|
|
225
|
+
* — a directory listing is not ordered, and a section whose page order
|
|
226
|
+
* depended on the filesystem would reorder itself on a different one.
|
|
227
|
+
*
|
|
228
|
+
* Empty for a file input.
|
|
229
|
+
*/
|
|
230
|
+
files: () => string[];
|
|
231
|
+
/** One file under a directory input, by its relative path. */
|
|
232
|
+
read: (file: string) => string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* An input over files already in memory.
|
|
237
|
+
*
|
|
238
|
+
* The pure half of what `sections.ts` builds out of `node:fs`, so a type's
|
|
239
|
+
* parser is testable without a fixture directory — and so a consumer's own type
|
|
240
|
+
* can be given one.
|
|
241
|
+
*/
|
|
242
|
+
export function duxtSectionInput(
|
|
243
|
+
path: string,
|
|
244
|
+
files: Record<string, string> | string
|
|
245
|
+
): DuxtSectionInput {
|
|
246
|
+
if (typeof files === 'string') {
|
|
247
|
+
return {
|
|
248
|
+
path,
|
|
249
|
+
kind: 'file',
|
|
250
|
+
text: () => files,
|
|
251
|
+
files: () => [],
|
|
252
|
+
read: () => ''
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const names = Object.keys(files).sort();
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
path,
|
|
260
|
+
kind: 'directory',
|
|
261
|
+
text: () => {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`duxt: "${path}" is a directory, and this type asked it for a file.`
|
|
264
|
+
);
|
|
265
|
+
},
|
|
266
|
+
files: () => [...names],
|
|
267
|
+
read: (file) => files[file] ?? ''
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
170
271
|
/** What the parser is told about the section it is filling. */
|
|
171
272
|
export interface DuxtSectionContext {
|
|
172
273
|
/** The declared label, which is also the index page's title. */
|
|
173
274
|
label: string;
|
|
275
|
+
/**
|
|
276
|
+
* The Content collection these pages land in.
|
|
277
|
+
*
|
|
278
|
+
* The one identity that is unique per version AND per locale — a prefix is
|
|
279
|
+
* not, since two languages deliberately claim the same one. A type that has
|
|
280
|
+
* to name a build-time artefact of its own (the Bruno collection's ZIP) needs
|
|
281
|
+
* a name the module emitting it can compute the same way, and this is it.
|
|
282
|
+
*/
|
|
283
|
+
collection: string;
|
|
284
|
+
/**
|
|
285
|
+
* Whether the artefact came out of a repository Content downloads.
|
|
286
|
+
*
|
|
287
|
+
* A type that only turns a file into pages never asks. One whose section
|
|
288
|
+
* offers something built BESIDE the pages does: `section-reports.ts` states
|
|
289
|
+
* the limit this reflects — a remote checkout lands wherever Content's
|
|
290
|
+
* hash-cache put it, which is a directory known only inside the collection
|
|
291
|
+
* that declared it, so nothing outside can read the artefact a second time.
|
|
292
|
+
*/
|
|
293
|
+
remote: boolean;
|
|
174
294
|
/** The URL prefix its pages are served at, e.g. `/releases`. */
|
|
175
295
|
prefix: string;
|
|
176
296
|
/** The declaration's own `options`, empty where it named none. */
|
|
@@ -223,15 +343,42 @@ export interface DuxtSectionReport {
|
|
|
223
343
|
*/
|
|
224
344
|
export interface DuxtSectionType {
|
|
225
345
|
/** The artefact, split into pages carrying their own frontmatter. */
|
|
226
|
-
parse: (
|
|
346
|
+
parse: (
|
|
347
|
+
input: DuxtSectionInput,
|
|
348
|
+
context: DuxtSectionContext
|
|
349
|
+
) => DuxtSectionPage[];
|
|
350
|
+
/**
|
|
351
|
+
* Whether the declared path names a file or a directory.
|
|
352
|
+
*
|
|
353
|
+
* `file` is the default and what the first two types are: the path is opened
|
|
354
|
+
* and its contents handed over. `directory` hands the type the tree instead —
|
|
355
|
+
* `input.files()` and `input.read()` — and is what a client-side collection
|
|
356
|
+
* like Bruno's needs, since there is no one file to open.
|
|
357
|
+
*
|
|
358
|
+
* A POLICY rather than a guess at the path: a type knows what it reads, and a
|
|
359
|
+
* resolver that stat'ed the path would report a missing directory as a
|
|
360
|
+
* missing file and say the wrong thing about both.
|
|
361
|
+
*/
|
|
362
|
+
input?: 'file' | 'directory';
|
|
227
363
|
/**
|
|
228
364
|
* How this type behaves against the one-collection-per-version mechanic.
|
|
229
365
|
*
|
|
230
366
|
* `global` is one history read from the default version and served at a
|
|
231
|
-
* version-neutral URL, with the switcher suppressed
|
|
232
|
-
*
|
|
367
|
+
* version-neutral URL, with the switcher suppressed. `per-version` is a
|
|
368
|
+
* section per version, like any other page — what the API reference and the
|
|
369
|
+
* changelog are.
|
|
233
370
|
*/
|
|
234
371
|
versioning: 'global' | 'per-version';
|
|
372
|
+
/**
|
|
373
|
+
* Whether every published version has to have the artefact.
|
|
374
|
+
*
|
|
375
|
+
* `optional`, the default, is the rule `missingSectionArtefact` states for a
|
|
376
|
+
* source: a local path that does not exist fails the build, a remote one is
|
|
377
|
+
* recorded and its section simply not built. `required` fails the build for
|
|
378
|
+
* a remote version too — for a type whose missing file cannot be quietly
|
|
379
|
+
* skipped without publishing the wrong thing, as a changelog cannot.
|
|
380
|
+
*/
|
|
381
|
+
artefact?: 'required' | 'optional';
|
|
235
382
|
/**
|
|
236
383
|
* What a localised site shows when the artefact has one language.
|
|
237
384
|
*
|
|
@@ -251,11 +398,11 @@ export interface DuxtSectionType {
|
|
|
251
398
|
* The layout its pages render in, through the shared slot.
|
|
252
399
|
*
|
|
253
400
|
* A layout name a type binds is PUBLIC SURFACE — renaming one later is a
|
|
254
|
-
* `feat!:`. Unset renders the section in the ordinary docs
|
|
401
|
+
* `feat!:`. Unset renders the section in the ordinary docs shell.
|
|
255
402
|
*
|
|
256
403
|
* A FUNCTION where the declaration's own options decide: the same type can
|
|
257
404
|
* produce two different things, and a changelog is the case that proves it —
|
|
258
|
-
* split into a page per release it is a timeline with
|
|
405
|
+
* split into a page per release it is a timeline with a layout of its own,
|
|
259
406
|
* rendered as the one file it was written as it is an ordinary docs page and
|
|
260
407
|
* wants the sidebar, the breadcrumb and the table of contents back.
|
|
261
408
|
*/
|
|
@@ -269,6 +416,7 @@ export type DuxtSectionTypes = Record<string, DuxtSectionType>;
|
|
|
269
416
|
|
|
270
417
|
/** The types the layer ships. */
|
|
271
418
|
export const duxtBuiltinSectionTypes: DuxtSectionTypes = {
|
|
419
|
+
bruno: brunoSectionType,
|
|
272
420
|
changelog: changelogSectionType,
|
|
273
421
|
openapi: openapiSectionType
|
|
274
422
|
};
|
|
@@ -485,6 +633,10 @@ export function resolveGeneratedSections(
|
|
|
485
633
|
}`,
|
|
486
634
|
prefix,
|
|
487
635
|
repo: base.entry.repo,
|
|
636
|
+
// The SOURCE's name, not the artefact's. A generated section is
|
|
637
|
+
// published by the project its source is, and the artefact has a
|
|
638
|
+
// label of its own that the search caption draws beside this.
|
|
639
|
+
name: base.entry.name,
|
|
488
640
|
// `global` is version-NEUTRAL, so it carries no version at all: the
|
|
489
641
|
// switcher lists what has a version, and a changelog must not be
|
|
490
642
|
// offered as one of the versions of the documentation beside it.
|
|
@@ -639,12 +791,14 @@ export function generatedSectionRef(
|
|
|
639
791
|
export function sectionPages(
|
|
640
792
|
entry: DuxtResolvedSource,
|
|
641
793
|
type: DuxtSectionType,
|
|
642
|
-
|
|
794
|
+
input: DuxtSectionInput
|
|
643
795
|
): DuxtSectionPage[] {
|
|
644
796
|
const warnings: string[] = [];
|
|
645
797
|
|
|
646
|
-
const pages = type.parse(
|
|
798
|
+
const pages = type.parse(input, {
|
|
647
799
|
label: entry.generated!.label,
|
|
800
|
+
collection: entry.collection,
|
|
801
|
+
remote: entry.generated!.remote,
|
|
648
802
|
prefix: entry.prefix,
|
|
649
803
|
options: entry.generated!.options ?? {},
|
|
650
804
|
warn: (message) => {
|
|
@@ -687,10 +841,22 @@ export function sectionPages(
|
|
|
687
841
|
*/
|
|
688
842
|
export function missingSectionArtefact(
|
|
689
843
|
entry: DuxtResolvedSource,
|
|
690
|
-
file: string
|
|
844
|
+
file: string,
|
|
845
|
+
type?: Pick<DuxtSectionType, 'artefact'>
|
|
691
846
|
): DuxtSectionPage[] {
|
|
692
847
|
entry.generated!.report = { pages: 0, warnings: [], missing: true };
|
|
693
848
|
|
|
849
|
+
// A type that needs its artefact in EVERY published version fails the build
|
|
850
|
+
// for a remote version too — see `DuxtSectionType.artefact`.
|
|
851
|
+
if (entry.generated!.remote && type?.artefact === 'required') {
|
|
852
|
+
throw new Error(
|
|
853
|
+
`duxt: the generated section "${entry.generated!.label}" needs ` +
|
|
854
|
+
`${entry.path} in every published version, and ` +
|
|
855
|
+
`${entry.version ?? entry.ref ?? 'this version'} has none ` +
|
|
856
|
+
`(looked in ${file}).`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
694
860
|
// Remote: recorded, not printed. `validate-report.ts` reads the report and
|
|
695
861
|
// says it once, in the same list as every other finding — see `sectionPages`.
|
|
696
862
|
if (entry.generated!.remote) return [];
|