@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.
- 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 +42 -10
- 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
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `bruno` type: a Bruno collection, published as reference pages.
|
|
3
|
+
*
|
|
4
|
+
* The third type in the registry and the second API one, and the question it
|
|
5
|
+
* was filed to answer is whether a second API type is a second RENDERING PATH.
|
|
6
|
+
* It is not: a Bruno request page renders in the same `reference` layout an
|
|
7
|
+
* OpenAPI operation does, and the try-it column is the same component fed a
|
|
8
|
+
* different parser's output. What this file adds is a parser, two policies and
|
|
9
|
+
* an adapter — which is the registry doing the job it was built for.
|
|
10
|
+
*
|
|
11
|
+
* WHAT A BRUNO COLLECTION IS NOT is why the pages are shaped the way they are.
|
|
12
|
+
* It is a client artefact: requests, folders, headers, bodies, scripts and
|
|
13
|
+
* environments, with no response schemas, no reusable components and no
|
|
14
|
+
* statement of what a field means. So these pages say what the collection
|
|
15
|
+
* actually says — method, URL, headers, parameters, body — and invent nothing.
|
|
16
|
+
* The issue that decided this type rejected converting Bruno to OpenAPI for
|
|
17
|
+
* exactly that reason: the conversion invents schemas it does not have, which
|
|
18
|
+
* makes the thinness invisible rather than honest.
|
|
19
|
+
*
|
|
20
|
+
* ONE PAGE PER REQUEST, a page per folder, and the collection's own overview —
|
|
21
|
+
* the same shape `openapi` uses, for the same reason: everything the layer can
|
|
22
|
+
* do hangs off pages, and a deep link to a request has to survive being pasted
|
|
23
|
+
* into a ticket.
|
|
24
|
+
*
|
|
25
|
+
* THE ARCHIVE IS THE POINT OF THE OVERVIEW. A reference tells you what the
|
|
26
|
+
* requests are; a collection is a thing you OPEN. The download is built from
|
|
27
|
+
* the exact version and locale the reader is looking at — see `bruno-zip.ts` —
|
|
28
|
+
* which is what neither a link to the repository's default branch nor a "Fetch
|
|
29
|
+
* in Bruno" clone of today's HEAD can offer.
|
|
30
|
+
*/
|
|
31
|
+
import { stringify as stringifyYaml } from 'yaml';
|
|
32
|
+
import { frontmatterBlock } from './frontmatter';
|
|
33
|
+
import { firstLine, pageOrder, urlSegment } from './section-markdown';
|
|
34
|
+
import type {
|
|
35
|
+
DuxtBrunoCollection,
|
|
36
|
+
DuxtBrunoEntry,
|
|
37
|
+
DuxtBrunoFolder,
|
|
38
|
+
DuxtBrunoRequest
|
|
39
|
+
} from './bruno-model';
|
|
40
|
+
import { parseBrunoCollection } from './bruno-parse';
|
|
41
|
+
import { brunoZipPath } from './bruno-zip';
|
|
42
|
+
import type {
|
|
43
|
+
DuxtSectionContext,
|
|
44
|
+
DuxtSectionInput,
|
|
45
|
+
DuxtSectionOptions,
|
|
46
|
+
DuxtSectionPage,
|
|
47
|
+
DuxtSectionType
|
|
48
|
+
} from './sections-resolve';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The layout this type binds — the SAME one `openapi` binds.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately not a name of its own. The two types produce the same kind of
|
|
54
|
+
* page and want the same layout: the full column width, the request list on the
|
|
55
|
+
* left, no reading measure. A second layout would be a copy that drifts, and a
|
|
56
|
+
* consumer who overrode `reference` to brand their API pages would find half of
|
|
57
|
+
* them unbranded.
|
|
58
|
+
*/
|
|
59
|
+
export const DUXT_BRUNO_LAYOUT = 'reference';
|
|
60
|
+
|
|
61
|
+
/** Bruno's own deep link, which needs no third-party script on the page. */
|
|
62
|
+
const FETCH_IN_BRUNO = 'https://fetch.usebruno.com/?url=';
|
|
63
|
+
|
|
64
|
+
/** The variable a Bruno collection conventionally puts its host in. */
|
|
65
|
+
const DEFAULT_BASE_VARIABLE = 'baseUrl';
|
|
66
|
+
|
|
67
|
+
export const brunoSectionType: DuxtSectionType = {
|
|
68
|
+
parse: parseBrunoSection,
|
|
69
|
+
/**
|
|
70
|
+
* A DIRECTORY, which is the contract change this type is the reason for.
|
|
71
|
+
*
|
|
72
|
+
* `bruno.json`, a tree of `.bru` files, `folder.bru` ordering and an
|
|
73
|
+
* `environments/` directory — there is no one file to open, and a type that
|
|
74
|
+
* had to be handed one would have to be handed the wrong thing.
|
|
75
|
+
*/
|
|
76
|
+
input: 'directory',
|
|
77
|
+
/**
|
|
78
|
+
* PER VERSION, for the reason `openapi` is: an API belongs to the release it
|
|
79
|
+
* describes, and a reader on `v1` must not be shown `v2`'s endpoints.
|
|
80
|
+
*/
|
|
81
|
+
versioning: 'per-version',
|
|
82
|
+
/**
|
|
83
|
+
* PER LOCALE, also matching `openapi`. A `.bru` file's `docs` blocks are
|
|
84
|
+
* written prose — the same prose an OpenAPI `description` is — so a
|
|
85
|
+
* translated collection is a real artefact rather than the same files
|
|
86
|
+
* relabelled. A locale that declares none builds nothing and falls through to
|
|
87
|
+
* the original with the translation banner saying so.
|
|
88
|
+
*/
|
|
89
|
+
localisation: 'per-locale',
|
|
90
|
+
layout: DUXT_BRUNO_LAYOUT,
|
|
91
|
+
/**
|
|
92
|
+
* `lucide`, not `simple-icons`, and the repository's icon rule says why: this
|
|
93
|
+
* entry names a SECTION OF THE DOCUMENTATION, not the Bruno platform. The one
|
|
94
|
+
* place `simple-icons:bruno` belongs is the "Fetch in Bruno" action, which
|
|
95
|
+
* hands the collection to Bruno itself.
|
|
96
|
+
*/
|
|
97
|
+
icon: 'lucide:send'
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** What a site turned on, as this type reads it. */
|
|
101
|
+
interface BrunoOptions {
|
|
102
|
+
tryIt?: {
|
|
103
|
+
/** The public base URL requests are sent to. */
|
|
104
|
+
baseUrl?: string;
|
|
105
|
+
/** The Bruno variable that base URL stands in for. */
|
|
106
|
+
baseVariable?: string;
|
|
107
|
+
/** Variables with a public value, and nothing else, may be substituted. */
|
|
108
|
+
variables?: Record<string, string>;
|
|
109
|
+
};
|
|
110
|
+
/** A public git URL, which turns on the "Fetch in Bruno" action. */
|
|
111
|
+
fetch?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseBrunoSection(
|
|
115
|
+
input: DuxtSectionInput,
|
|
116
|
+
context: DuxtSectionContext
|
|
117
|
+
): DuxtSectionPage[] {
|
|
118
|
+
const collection = parseBrunoCollection(input);
|
|
119
|
+
|
|
120
|
+
// Into the report rather than onto the console, for the reason every other
|
|
121
|
+
// type does it: a collection is parsed while the config is loading, so a
|
|
122
|
+
// printed warning has scrolled away before the dev server has started.
|
|
123
|
+
for (const warning of collection.warnings) context.warn?.(warning);
|
|
124
|
+
|
|
125
|
+
const options = readOptions(context.options);
|
|
126
|
+
|
|
127
|
+
// ONE NAMESPACE FOR BOTH. A folder and a request at the same level are
|
|
128
|
+
// siblings in the URL — `/api/shipments` and `/api/ping` — so the slugs have
|
|
129
|
+
// to be unique across the two lists together, not within each.
|
|
130
|
+
const level = slugsFor(collection.folders, collection.requests);
|
|
131
|
+
|
|
132
|
+
return [
|
|
133
|
+
overview(collection, context, options, level),
|
|
134
|
+
...collection.folders.flatMap((folder, index) =>
|
|
135
|
+
folderPages(
|
|
136
|
+
folder,
|
|
137
|
+
options,
|
|
138
|
+
level.slugs[index]!,
|
|
139
|
+
`${pageOrder(index, level.width)}.`,
|
|
140
|
+
'',
|
|
141
|
+
context.prefix
|
|
142
|
+
)
|
|
143
|
+
),
|
|
144
|
+
...collection.requests.map((request, index) =>
|
|
145
|
+
requestPage(
|
|
146
|
+
request,
|
|
147
|
+
options,
|
|
148
|
+
level.slugs[collection.folders.length + index]!,
|
|
149
|
+
`${pageOrder(collection.folders.length + index, level.width)}.`,
|
|
150
|
+
''
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Unique URL segments for one level of the tree.
|
|
158
|
+
*
|
|
159
|
+
* Two requests called `Get user` and `Get User` slugify to the same segment,
|
|
160
|
+
* and without this the second one's page would overwrite the first's — a
|
|
161
|
+
* request silently missing from the reference, which is the failure mode the
|
|
162
|
+
* OpenAPI type already answers for tags and operations. Numbered in the order
|
|
163
|
+
* they are read, so the first keeps the clean name.
|
|
164
|
+
*/
|
|
165
|
+
function slugsFor(
|
|
166
|
+
folders: DuxtBrunoFolder[],
|
|
167
|
+
requests: DuxtBrunoRequest[]
|
|
168
|
+
): { slugs: string[]; width: number } {
|
|
169
|
+
const taken = new Set<string>();
|
|
170
|
+
|
|
171
|
+
const slugs = [
|
|
172
|
+
...folders.map((folder) => folder.name || folder.dir),
|
|
173
|
+
...requests.map((request) => request.name)
|
|
174
|
+
].map((name) => {
|
|
175
|
+
const base = urlSegment(name, 'request');
|
|
176
|
+
|
|
177
|
+
let candidate = base;
|
|
178
|
+
let attempt = 2;
|
|
179
|
+
while (taken.has(candidate)) candidate = `${base}-${attempt++}`;
|
|
180
|
+
taken.add(candidate);
|
|
181
|
+
|
|
182
|
+
return candidate;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return { slugs, width: folders.length + requests.length };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ------------------------------------------------------------------ pages */
|
|
189
|
+
|
|
190
|
+
/** The collection's own page: what it is, what is in it, how to get it. */
|
|
191
|
+
function overview(
|
|
192
|
+
collection: DuxtBrunoCollection,
|
|
193
|
+
context: DuxtSectionContext,
|
|
194
|
+
options: BrunoOptions,
|
|
195
|
+
level: { slugs: string[] }
|
|
196
|
+
): DuxtSectionPage {
|
|
197
|
+
const props = {
|
|
198
|
+
name: collection.name,
|
|
199
|
+
version: collection.version,
|
|
200
|
+
auth: collection.auth,
|
|
201
|
+
headers: collection.headers,
|
|
202
|
+
environments: collection.environments,
|
|
203
|
+
// A REMOTE collection gets no archive, and the limit is real rather than
|
|
204
|
+
// chosen: Content's checkout lands in a hash-cached directory known only
|
|
205
|
+
// inside the collection that declared it, so the module that writes these
|
|
206
|
+
// files cannot read the artefact a second time. A link to a file nothing
|
|
207
|
+
// wrote would be worse than the link being absent.
|
|
208
|
+
download: context.remote ? undefined : brunoZipPath(context.collection),
|
|
209
|
+
fetch: options.fetch
|
|
210
|
+
? `${FETCH_IN_BRUNO}${encodeURIComponent(options.fetch)}`
|
|
211
|
+
: undefined,
|
|
212
|
+
groups: collection.folders.map((folder, index) => ({
|
|
213
|
+
name: folder.name,
|
|
214
|
+
description: firstLine(folder.docs),
|
|
215
|
+
requests: count(folder),
|
|
216
|
+
to: `${context.prefix}/${level.slugs[index]}`
|
|
217
|
+
})),
|
|
218
|
+
requests: collection.requests.map((request, index) => ({
|
|
219
|
+
name: request.name,
|
|
220
|
+
method: request.method,
|
|
221
|
+
url: request.url,
|
|
222
|
+
to: `${context.prefix}/${level.slugs[collection.folders.length + index]}`
|
|
223
|
+
}))
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
file: 'index.md',
|
|
228
|
+
body: page(
|
|
229
|
+
{
|
|
230
|
+
title: collection.name,
|
|
231
|
+
description: firstLine(collection.docs) ?? collection.name
|
|
232
|
+
},
|
|
233
|
+
[component('bruno-overview', props, collection.docs)]
|
|
234
|
+
)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** A folder's index page, and a page for everything under it. */
|
|
239
|
+
function folderPages(
|
|
240
|
+
folder: DuxtBrunoFolder,
|
|
241
|
+
options: BrunoOptions,
|
|
242
|
+
slug: string,
|
|
243
|
+
prefix: string,
|
|
244
|
+
parent: string,
|
|
245
|
+
parentUrl: string
|
|
246
|
+
): DuxtSectionPage[] {
|
|
247
|
+
// The FILE path carries the `NN.` ordering prefixes; the URL does not,
|
|
248
|
+
// because Content strips them. Two values rather than one derived from the
|
|
249
|
+
// other by a regex — which is what this was, and a folder legitimately named
|
|
250
|
+
// `1.x` walked straight into it.
|
|
251
|
+
const dir = `${parent}${prefix}${slug}`;
|
|
252
|
+
const to = `${parentUrl}/${slug}`;
|
|
253
|
+
|
|
254
|
+
const level = slugsFor(folder.folders, folder.requests);
|
|
255
|
+
|
|
256
|
+
const children = folder.folders.flatMap((child, index) =>
|
|
257
|
+
folderPages(
|
|
258
|
+
child,
|
|
259
|
+
options,
|
|
260
|
+
level.slugs[index]!,
|
|
261
|
+
`${pageOrder(index, level.width)}.`,
|
|
262
|
+
`${dir}/`,
|
|
263
|
+
to
|
|
264
|
+
)
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const requests = folder.requests.map((request, index) =>
|
|
268
|
+
requestPage(
|
|
269
|
+
request,
|
|
270
|
+
options,
|
|
271
|
+
level.slugs[folder.folders.length + index]!,
|
|
272
|
+
`${pageOrder(folder.folders.length + index, level.width)}.`,
|
|
273
|
+
`${dir}/`
|
|
274
|
+
)
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const index: DuxtSectionPage = {
|
|
278
|
+
file: `${dir}/index.md`,
|
|
279
|
+
body: page({ title: folder.name, description: firstLine(folder.docs) }, [
|
|
280
|
+
component(
|
|
281
|
+
'bruno-requests',
|
|
282
|
+
{
|
|
283
|
+
requests: folder.requests.map((request, position) => ({
|
|
284
|
+
name: request.name,
|
|
285
|
+
method: request.method,
|
|
286
|
+
url: request.url,
|
|
287
|
+
to: `${to}/${level.slugs[folder.folders.length + position]}`
|
|
288
|
+
})),
|
|
289
|
+
groups: folder.folders.map((child, position) => ({
|
|
290
|
+
name: child.name,
|
|
291
|
+
description: firstLine(child.docs),
|
|
292
|
+
requests: count(child),
|
|
293
|
+
to: `${to}/${level.slugs[position]}`
|
|
294
|
+
}))
|
|
295
|
+
},
|
|
296
|
+
folder.docs
|
|
297
|
+
)
|
|
298
|
+
])
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
return [index, ...children, ...requests];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** One request, as one page. */
|
|
305
|
+
function requestPage(
|
|
306
|
+
request: DuxtBrunoRequest,
|
|
307
|
+
options: BrunoOptions,
|
|
308
|
+
slug: string,
|
|
309
|
+
prefix: string,
|
|
310
|
+
parent: string
|
|
311
|
+
): DuxtSectionPage {
|
|
312
|
+
// The docs travel in the SLOT rather than in the props, so what the file
|
|
313
|
+
// wrote as Markdown is rendered as Markdown — the same rule `openapi`
|
|
314
|
+
// follows for a description.
|
|
315
|
+
const { docs, ...rest } = request;
|
|
316
|
+
|
|
317
|
+
const client = tryIt(request, options);
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
file: `${parent}${prefix}${slug}.md`,
|
|
321
|
+
body: page(
|
|
322
|
+
{
|
|
323
|
+
title: request.name,
|
|
324
|
+
description: firstLine(docs) ?? `${request.method} ${request.url}`
|
|
325
|
+
},
|
|
326
|
+
[component('bruno-request', { request: rest, ...client }, docs)]
|
|
327
|
+
)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* ----------------------------------------------------------------- try-it */
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The request as the existing try-it client takes it — or nothing at all.
|
|
335
|
+
*
|
|
336
|
+
* OFF UNTIL A SITE TURNS IT ON, which is the decision this whole function
|
|
337
|
+
* implements. A Bruno collection is written against whatever the author could
|
|
338
|
+
* reach, which is routinely a staging host behind a VPN or a `localhost` port;
|
|
339
|
+
* a documentation site that pointed a send button at it would be offering
|
|
340
|
+
* readers a request that cannot work, and a site that guessed a public host
|
|
341
|
+
* from the collection would be inventing one. So the site states the public
|
|
342
|
+
* base URL and the variables that have a public value, and anything it did not
|
|
343
|
+
* state leaves the page static.
|
|
344
|
+
*
|
|
345
|
+
* ADAPTED TO `DuxtOpenApiOperation` rather than given a client of its own: the
|
|
346
|
+
* existing one already answers "where do the credentials go" in a way this
|
|
347
|
+
* layer has committed to — in the component's own state, never stored, never
|
|
348
|
+
* proxied — and a second client would be a second place to get that wrong.
|
|
349
|
+
*/
|
|
350
|
+
function tryIt(
|
|
351
|
+
request: DuxtBrunoRequest,
|
|
352
|
+
options: BrunoOptions
|
|
353
|
+
): { operation: Record<string, unknown>; servers: { url: string }[] } | object {
|
|
354
|
+
const base = options.tryIt?.baseUrl;
|
|
355
|
+
if (!base) return {};
|
|
356
|
+
|
|
357
|
+
const variable = options.tryIt?.baseVariable ?? DEFAULT_BASE_VARIABLE;
|
|
358
|
+
|
|
359
|
+
const resolved = resolve(request.url, {
|
|
360
|
+
[variable]: '',
|
|
361
|
+
...options.tryIt?.variables
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// A URL still holding `{{something}}` cannot be sent, and a send button that
|
|
365
|
+
// builds a wrong URL is worse than no button: the reader gets a failure the
|
|
366
|
+
// page cannot explain.
|
|
367
|
+
if (resolved === undefined) return {};
|
|
368
|
+
|
|
369
|
+
const [withoutQuery] = resolved.split('?');
|
|
370
|
+
|
|
371
|
+
// Bruno writes a path parameter as `:name`; OpenAPI writes it as `{name}`,
|
|
372
|
+
// which is the spelling the client draws a box for.
|
|
373
|
+
const path = (withoutQuery || '/').replace(/:([A-Za-z_][\w-]*)/g, '{$1}');
|
|
374
|
+
|
|
375
|
+
const parameters = [
|
|
376
|
+
...request.params
|
|
377
|
+
.filter((entry) => !entry.disabled)
|
|
378
|
+
.map((entry) => parameter(entry, entry.in)),
|
|
379
|
+
...request.headers
|
|
380
|
+
.filter((entry) => !entry.disabled)
|
|
381
|
+
.map((entry) => parameter(entry, 'header'))
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
servers: [{ url: base }],
|
|
386
|
+
operation: {
|
|
387
|
+
kind: 'operation',
|
|
388
|
+
method: request.method,
|
|
389
|
+
path: path.startsWith('/') ? path : `/${path}`,
|
|
390
|
+
deprecated: false,
|
|
391
|
+
...(parameters.length ? { parameters } : {}),
|
|
392
|
+
...(request.body?.text
|
|
393
|
+
? {
|
|
394
|
+
requestBody: {
|
|
395
|
+
required: false,
|
|
396
|
+
content: [
|
|
397
|
+
{
|
|
398
|
+
type: mediaType(request.body.type),
|
|
399
|
+
examples: [
|
|
400
|
+
{ name: 'default', value: example(request.body.text) }
|
|
401
|
+
]
|
|
402
|
+
}
|
|
403
|
+
]
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
: {})
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* One box in the client.
|
|
413
|
+
*
|
|
414
|
+
* A REDACTED value carries no example at all, so the box opens empty and the
|
|
415
|
+
* reader supplies their own credential. Prefilling it would put the collection
|
|
416
|
+
* author's live token into every reader's form — which is the leak the
|
|
417
|
+
* redaction exists to stop, arriving by the other door.
|
|
418
|
+
*/
|
|
419
|
+
function parameter(entry: DuxtBrunoEntry, where: string) {
|
|
420
|
+
return {
|
|
421
|
+
name: entry.name,
|
|
422
|
+
in: where,
|
|
423
|
+
required: where === 'path',
|
|
424
|
+
...(entry.redacted || !entry.value
|
|
425
|
+
? {}
|
|
426
|
+
: { examples: [{ name: 'default', value: entry.value }] })
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* A URL with its `{{variables}}` filled in, or nothing where one is unknown.
|
|
432
|
+
*
|
|
433
|
+
* Only what the site DECLARED is substituted. A variable the site said nothing
|
|
434
|
+
* about is not guessed at and not left in: the whole URL is abandoned, because
|
|
435
|
+
* half a URL is a request to the wrong place.
|
|
436
|
+
*/
|
|
437
|
+
function resolve(
|
|
438
|
+
url: string,
|
|
439
|
+
values: Record<string, string>
|
|
440
|
+
): string | undefined {
|
|
441
|
+
let unknown = false;
|
|
442
|
+
|
|
443
|
+
const out = url.replaceAll(
|
|
444
|
+
/\{\{\s*([^{}]+?)\s*\}\}/g,
|
|
445
|
+
(_match, name: string) => {
|
|
446
|
+
const value = values[name];
|
|
447
|
+
|
|
448
|
+
if (value === undefined) {
|
|
449
|
+
unknown = true;
|
|
450
|
+
return '';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
return unknown ? undefined : out;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** The body's media type, as a header value. */
|
|
461
|
+
function mediaType(type: string): string {
|
|
462
|
+
if (type === 'json') return 'application/json';
|
|
463
|
+
if (type === 'xml') return 'application/xml';
|
|
464
|
+
if (type === 'graphql') return 'application/json';
|
|
465
|
+
if (type === 'sparql') return 'application/sparql-query';
|
|
466
|
+
|
|
467
|
+
return 'text/plain';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** A JSON body as a value the client's editor can open; anything else as text. */
|
|
471
|
+
function example(text: string): unknown {
|
|
472
|
+
try {
|
|
473
|
+
return JSON.parse(text);
|
|
474
|
+
} catch {
|
|
475
|
+
return text;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/* ---------------------------------------------------------------- helpers */
|
|
480
|
+
|
|
481
|
+
function readOptions(options: DuxtSectionOptions): BrunoOptions {
|
|
482
|
+
const tryItOption = options.tryIt;
|
|
483
|
+
const fetchOption = options.fetch;
|
|
484
|
+
|
|
485
|
+
const tryIt =
|
|
486
|
+
tryItOption && typeof tryItOption === 'object'
|
|
487
|
+
? (tryItOption as BrunoOptions['tryIt'])
|
|
488
|
+
: undefined;
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
...(tryIt?.baseUrl ? { tryIt } : {}),
|
|
492
|
+
...(typeof fetchOption === 'string' && fetchOption
|
|
493
|
+
? { fetch: fetchOption }
|
|
494
|
+
: {})
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Every request under a folder, its own and its children's. */
|
|
499
|
+
function count(folder: DuxtBrunoFolder): number {
|
|
500
|
+
return (
|
|
501
|
+
folder.requests.length +
|
|
502
|
+
folder.folders.reduce((total, child) => total + count(child), 0)
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** An MDC block component with YAML props and Markdown inside it. */
|
|
507
|
+
function component(
|
|
508
|
+
name: string,
|
|
509
|
+
props: Record<string, unknown>,
|
|
510
|
+
slot?: string
|
|
511
|
+
): string {
|
|
512
|
+
const body = slot?.trim() ?? '';
|
|
513
|
+
|
|
514
|
+
const longest = Math.max(
|
|
515
|
+
2,
|
|
516
|
+
...body.split('\n').map((line) => /^\s*(:+)/.exec(line)?.[1]?.length ?? 0)
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const fence = ':'.repeat(Math.max(3, longest + 1));
|
|
520
|
+
|
|
521
|
+
return [
|
|
522
|
+
`${fence}${name}`,
|
|
523
|
+
'---',
|
|
524
|
+
stringifyYaml(compact(props)).trimEnd(),
|
|
525
|
+
'---',
|
|
526
|
+
...(body ? [body] : []),
|
|
527
|
+
fence
|
|
528
|
+
].join('\n');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* The props, with every absent thing actually absent.
|
|
533
|
+
*
|
|
534
|
+
* `undefined` is not YAML, and a key written as `null` is a key a component has
|
|
535
|
+
* to test for a second way. Empty lists and maps go too — the model's own rule,
|
|
536
|
+
* stated in `openapi-model.ts` and followed here so the two types' props read
|
|
537
|
+
* the same.
|
|
538
|
+
*/
|
|
539
|
+
function compact(value: unknown): unknown {
|
|
540
|
+
if (Array.isArray(value)) return value.map(compact);
|
|
541
|
+
|
|
542
|
+
if (value && typeof value === 'object') {
|
|
543
|
+
const out: Record<string, unknown> = {};
|
|
544
|
+
|
|
545
|
+
for (const [key, item] of Object.entries(value)) {
|
|
546
|
+
if (item === undefined || item === null) continue;
|
|
547
|
+
if (Array.isArray(item) && !item.length) continue;
|
|
548
|
+
if (
|
|
549
|
+
typeof item === 'object' &&
|
|
550
|
+
!Array.isArray(item) &&
|
|
551
|
+
!Object.keys(item).length
|
|
552
|
+
) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
out[key] = compact(item);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return value;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** A page: frontmatter, then the lines. */
|
|
566
|
+
function page(
|
|
567
|
+
fields: Record<string, string | undefined>,
|
|
568
|
+
lines: string[]
|
|
569
|
+
): string {
|
|
570
|
+
return [frontmatterBlock(fields), '', ...lines, ''].join('\n');
|
|
571
|
+
}
|