blume 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cli/index.js +2260 -1100
  3. package/dist/cli/index.js.map +123 -117
  4. package/dist/types/ai/component-markdown.d.ts +14 -4
  5. package/dist/types/core/base-path.d.ts +8 -0
  6. package/dist/types/core/config-input.d.ts +87 -27
  7. package/dist/types/core/config.d.ts +2 -1
  8. package/dist/types/core/data.d.ts +16 -1
  9. package/dist/types/core/diagnostics.d.ts +5 -1
  10. package/dist/types/core/i18n-ui.d.ts +12 -0
  11. package/dist/types/core/schema.d.ts +116 -15
  12. package/dist/types/core/sources/types.d.ts +11 -1
  13. package/dist/types/core/standard-schema.d.ts +7 -3
  14. package/dist/types/core/types.d.ts +43 -2
  15. package/dist/types/core/ui-packs/index.d.ts +9 -1
  16. package/dist/types/openapi/references.d.ts +14 -7
  17. package/dist/types/seo/x-handle.d.ts +3 -2
  18. package/docs/advanced/api-reference.mdx +8 -6
  19. package/docs/configuration/ai.mdx +26 -8
  20. package/docs/configuration/search.mdx +2 -0
  21. package/docs/configuration/seo.mdx +1 -1
  22. package/docs/content/i18n.mdx +1 -1
  23. package/docs/content/meta.mdx +2 -1
  24. package/docs/content/meta.ts +1 -0
  25. package/docs/content/navigation.mdx +35 -1
  26. package/docs/content/sources.mdx +1 -1
  27. package/docs/content/versioning.mdx +106 -0
  28. package/docs/reference/cli.mdx +1 -0
  29. package/docs/reference/frontmatter.mdx +3 -0
  30. package/package.json +13 -1
  31. package/skills/blume-migrate/SKILL.md +2 -2
  32. package/skills/blume-migrate/references/docusaurus.md +1 -1
  33. package/skills/blume-migrate/references/fumadocs.md +1 -1
  34. package/skills/blume-migrate/references/mintlify.md +1 -1
  35. package/src/ai/agent-readability.ts +40 -12
  36. package/src/ai/api-catalog.ts +2 -2
  37. package/src/ai/ask-context.ts +49 -12
  38. package/src/ai/ask.ts +10 -1
  39. package/src/ai/component-markdown.ts +80 -43
  40. package/src/ai/llms.ts +42 -17
  41. package/src/ai/mcp/data.ts +48 -12
  42. package/src/ai/mcp/discovery.ts +52 -16
  43. package/src/ai/mcp/server.ts +280 -125
  44. package/src/ai/mcp/tools.ts +3 -3
  45. package/src/ai/skills.ts +32 -9
  46. package/src/ai/tar.ts +29 -70
  47. package/src/ai/visibility.ts +2 -2
  48. package/src/astro/component-slots.ts +2 -0
  49. package/src/astro/examples.ts +13 -5
  50. package/src/astro/generate.ts +113 -63
  51. package/src/astro/integration.ts +13 -2
  52. package/src/astro/islands.ts +23 -12
  53. package/src/astro/templates.ts +185 -41
  54. package/src/audit/agent.ts +16 -31
  55. package/src/audit/checks/content.ts +26 -11
  56. package/src/audit/checks/dns-aid.ts +3 -0
  57. package/src/audit/checks/indexability.ts +24 -6
  58. package/src/audit/checks/llms.ts +9 -4
  59. package/src/audit/checks/network.ts +2 -0
  60. package/src/audit/checks/social.ts +18 -10
  61. package/src/audit/crawl.ts +78 -25
  62. package/src/audit/report.ts +20 -19
  63. package/src/audit/run.ts +15 -5
  64. package/src/audit/snapshot.ts +29 -6
  65. package/src/audit/types.ts +25 -3
  66. package/src/blume-modules.d.ts +5 -1
  67. package/src/cli/commands/audit.ts +21 -21
  68. package/src/cli/commands/build.ts +30 -16
  69. package/src/cli/commands/dev.ts +15 -15
  70. package/src/cli/commands/doctor.ts +2 -0
  71. package/src/cli/commands/eject.ts +4 -4
  72. package/src/cli/commands/eval.ts +24 -30
  73. package/src/cli/commands/init.ts +9 -9
  74. package/src/cli/commands/mcp-stdio.ts +3 -0
  75. package/src/cli/commands/translate.ts +14 -3
  76. package/src/cli/commands/version.ts +85 -0
  77. package/src/cli/dev-lock.ts +31 -10
  78. package/src/cli/eject-scripts.ts +17 -2
  79. package/src/cli/env.ts +13 -30
  80. package/src/cli/index.ts +2 -0
  81. package/src/cli/init/questions.ts +1 -1
  82. package/src/cli/init/scaffold.ts +41 -13
  83. package/src/cli/internal-error.ts +1 -0
  84. package/src/cli/report-format.ts +22 -0
  85. package/src/components/content/AccordionItem.astro +2 -9
  86. package/src/components/content/ColorItem.astro +5 -13
  87. package/src/components/content/Component.astro +12 -8
  88. package/src/components/content/Frame.astro +2 -12
  89. package/src/components/content/Prompt.astro +12 -31
  90. package/src/components/content/Tab.astro +2 -9
  91. package/src/components/content/Tooltip.astro +1 -9
  92. package/src/components/content/Update.astro +2 -9
  93. package/src/components/content/auto-type-table.ts +3 -0
  94. package/src/components/content/diff.ts +9 -5
  95. package/src/components/content/github-info.ts +2 -0
  96. package/src/components/content/inline-markdown.ts +28 -0
  97. package/src/components/copy-feedback.ts +96 -0
  98. package/src/components/islands/ask-ai.tsx +111 -34
  99. package/src/components/islands/hooks.ts +5 -1
  100. package/src/components/islands/webmcp.ts +49 -12
  101. package/src/components/layout/Header.astro +25 -1
  102. package/src/components/layout/NavSelector.astro +11 -2
  103. package/src/components/layout/NavTree.astro +4 -2
  104. package/src/components/layout/PageActions.astro +20 -32
  105. package/src/components/layout/PageLayout.astro +8 -28
  106. package/src/components/layout/RootLayout.astro +24 -48
  107. package/src/components/layout/Search.astro +133 -22
  108. package/src/components/layout/VersionBanner.astro +39 -0
  109. package/src/components/layout/analytics-client.ts +8 -5
  110. package/src/components/layout/drawer-inert.ts +31 -0
  111. package/src/components/layout/hydration-hint.ts +1 -1
  112. package/src/components/layout/nav-utils.ts +1 -4
  113. package/src/components/layout/overrides.ts +25 -12
  114. package/src/components/layout/search/algolia.ts +18 -5
  115. package/src/components/layout/search/endpoint.ts +3 -0
  116. package/src/components/layout/search/flexsearch.ts +23 -7
  117. package/src/components/layout/search/orama-cloud.ts +1 -1
  118. package/src/components/layout/search/orama.ts +4 -1
  119. package/src/components/layout/search/pagefind.ts +8 -5
  120. package/src/components/layout/search/types.ts +45 -1
  121. package/src/components/layout/search/typesense.ts +19 -3
  122. package/src/components/openapi/ApiOverview.astro +32 -6
  123. package/src/components/openapi/AsyncApiOperation.astro +237 -0
  124. package/src/components/openapi/Bindings.astro +89 -0
  125. package/src/components/openapi/MethodBadge.astro +3 -0
  126. package/src/components/openapi/Operation.astro +7 -2
  127. package/src/components/openapi/PanelTabs.astro +131 -0
  128. package/src/components/openapi/ParametersTable.astro +2 -0
  129. package/src/components/openapi/RequestPanel.astro +12 -119
  130. package/src/components/openapi/async-snippets.ts +174 -0
  131. package/src/components/openapi/async.ts +348 -0
  132. package/src/components/openapi/helpers.ts +52 -20
  133. package/src/components/openapi/panel.ts +11 -8
  134. package/src/components/openapi/security.ts +102 -29
  135. package/src/components/openapi/snippets.ts +11 -11
  136. package/src/components/raf-throttle.ts +21 -0
  137. package/src/components/slug.ts +14 -0
  138. package/src/core/base-path.ts +18 -1
  139. package/src/core/component-overrides.ts +28 -23
  140. package/src/core/config-input.ts +96 -27
  141. package/src/core/config.ts +20 -7
  142. package/src/core/content.ts +3 -1
  143. package/src/core/data.ts +16 -1
  144. package/src/core/define-components.ts +5 -0
  145. package/src/core/diagnostics.ts +46 -38
  146. package/src/core/frontmatter.ts +74 -4
  147. package/src/core/graph.ts +137 -53
  148. package/src/core/i18n-ui.ts +15 -0
  149. package/src/core/i18n.ts +16 -8
  150. package/src/core/load-module.ts +1 -0
  151. package/src/core/manifest.ts +92 -3
  152. package/src/core/meta.ts +44 -14
  153. package/src/core/nav-diagnostics.ts +3 -3
  154. package/src/core/navigation.ts +247 -67
  155. package/src/core/probe.ts +7 -19
  156. package/src/core/project-graph.ts +27 -4
  157. package/src/core/schema.ts +219 -67
  158. package/src/core/site-url.ts +27 -0
  159. package/src/core/sources/assets.ts +2 -0
  160. package/src/core/sources/cache.ts +16 -8
  161. package/src/core/sources/github-releases.ts +39 -11
  162. package/src/core/sources/mdx-remote.ts +4 -0
  163. package/src/core/sources/normalize.ts +93 -22
  164. package/src/core/sources/notion.ts +76 -22
  165. package/src/core/sources/portable-text.ts +48 -12
  166. package/src/core/sources/resolve.ts +1 -0
  167. package/src/core/sources/sanity.ts +68 -14
  168. package/src/core/sources/types.ts +17 -1
  169. package/src/core/sources/watch.ts +1 -1
  170. package/src/core/standard-schema.ts +9 -3
  171. package/src/core/text-width.ts +26 -0
  172. package/src/core/tsconfig-aliases.ts +9 -5
  173. package/src/core/types.ts +45 -2
  174. package/src/core/ui-packs/index.ts +9 -1
  175. package/src/core/version-cut.ts +301 -0
  176. package/src/core/version.ts +2 -0
  177. package/src/core/versions.ts +170 -0
  178. package/src/deploy/adapter-output.ts +5 -2
  179. package/src/deploy/cloudflare-negotiation.ts +40 -11
  180. package/src/deploy/robots.ts +2 -1
  181. package/src/deploy/rss.ts +2 -1
  182. package/src/deploy/sitemap.ts +89 -8
  183. package/src/deploy/vercel-negotiation.ts +11 -4
  184. package/src/eval/agents.ts +13 -10
  185. package/src/eval/report.ts +5 -18
  186. package/src/eval/run.ts +2 -2
  187. package/src/eval/schema.ts +1 -1
  188. package/src/markdown/base-links.ts +6 -6
  189. package/src/markdown/directives.ts +7 -1
  190. package/src/markdown/heading-anchors.ts +17 -6
  191. package/src/markdown/index.ts +73 -24
  192. package/src/markdown/inline-code.ts +14 -2
  193. package/src/markdown/language-icon.ts +6 -2
  194. package/src/markdown/mdast.ts +18 -4
  195. package/src/markdown/package-commands.ts +63 -58
  196. package/src/markdown/table-wrap.ts +4 -1
  197. package/src/markdown/twoslash.ts +2 -0
  198. package/src/og/card.ts +50 -33
  199. package/src/og/derive.ts +43 -27
  200. package/src/openapi/asyncapi.ts +366 -0
  201. package/src/openapi/model.ts +135 -66
  202. package/src/openapi/parse.ts +166 -33
  203. package/src/openapi/references.ts +47 -22
  204. package/src/openapi/render-mdx.ts +137 -59
  205. package/src/openapi/scalar.ts +8 -10
  206. package/src/openapi/source.ts +126 -29
  207. package/src/registry/eject.ts +7 -2
  208. package/src/search/documents.ts +103 -39
  209. package/src/search/facets.ts +7 -5
  210. package/src/search/orama-index.ts +117 -32
  211. package/src/search/popular.ts +10 -5
  212. package/src/search/providers.ts +2 -2
  213. package/src/search/sync/index.ts +2 -0
  214. package/src/search/sync/typesense.ts +4 -2
  215. package/src/seo/jsonld.ts +24 -6
  216. package/src/seo/x-handle.ts +8 -3
  217. package/src/theme/chrome-icons.ts +7 -2
  218. package/src/theme/fonts.ts +8 -4
  219. package/src/theme/icons.ts +4 -2
  220. package/src/theme/palette.ts +27 -15
  221. package/src/translate/ledger.ts +4 -2
  222. package/src/translate/meta.ts +15 -6
  223. package/src/translate/report.ts +10 -19
  224. package/src/translate/run.ts +29 -38
  225. package/src/translate/validate.ts +52 -17
  226. package/src/translate/work-list.ts +0 -0
  227. package/src/cli/coalesce.ts +0 -43
@@ -0,0 +1,348 @@
1
+ import type {
2
+ AsyncApiChannelObject,
3
+ AsyncApiDocument,
4
+ AsyncApiOperationObject,
5
+ AsyncApiRefLike,
6
+ AsyncApiServerObject,
7
+ AsyncApiSpecValue,
8
+ } from "../../openapi/asyncapi.ts";
9
+ import type { ParameterLike, SchemaLike } from "./helpers.ts";
10
+ import { resolveComponentRef } from "./helpers.ts";
11
+
12
+ /**
13
+ * Runtime helpers for the AsyncAPI components — the async counterpart of
14
+ * `helpers.ts`. These operate on the normalized 3.x document behind the
15
+ * `blume:openapi` alias, resolving the ref shapes AsyncAPI adds on top of
16
+ * `#/components/*`: operations point at channels, channel messages may `$ref`
17
+ * `#/components/messages`, and operation messages point *into* a channel
18
+ * (`#/channels/<id>/messages/<name>`). Browser-safe like the rest of the set.
19
+ */
20
+
21
+ /** A permissive view of an AsyncAPI message — only the fields we render. */
22
+ export interface AsyncApiMessageLike extends AsyncApiRefLike {
23
+ name?: string;
24
+ title?: string;
25
+ summary?: string;
26
+ description?: string;
27
+ contentType?: string;
28
+ payload?: AsyncApiSpecValue;
29
+ headers?: AsyncApiSpecValue;
30
+ examples?: {
31
+ name?: string;
32
+ summary?: string;
33
+ payload?: AsyncApiSpecValue;
34
+ headers?: AsyncApiSpecValue;
35
+ }[];
36
+ bindings?: AsyncApiChannelObject["bindings"];
37
+ }
38
+
39
+ /** A message paired with its channel-map key (the fallback display name). */
40
+ export interface NamedMessage {
41
+ key: string;
42
+ message: AsyncApiMessageLike;
43
+ }
44
+
45
+ const isObject = (
46
+ value: AsyncApiSpecValue
47
+ ): value is Record<string, AsyncApiSpecValue> =>
48
+ typeof value === "object" && value !== null && !Array.isArray(value);
49
+
50
+ /** Permissive spec nodes may lie about declared string fields; verify first. */
51
+ const isString = (value: AsyncApiSpecValue): value is string =>
52
+ typeof value === "string";
53
+
54
+ /** Decode a JSON-pointer token: `user~1signedup` -> `user/signedup`. */
55
+ const unescapePointer = (token: string): string =>
56
+ token.replaceAll("~1", "/").replaceAll("~0", "~");
57
+
58
+ const CHANNEL_MESSAGE_REF =
59
+ /^#\/channels\/(?<channel>.+)\/messages\/(?<name>[^/]+)$/u;
60
+
61
+ type Components = AsyncApiDocument["components"];
62
+
63
+ /** Resolve a channel-map message (possibly a components `$ref`) to its object. */
64
+ const channelMessage = (
65
+ raw: AsyncApiRefLike | undefined,
66
+ components: Components
67
+ ): AsyncApiMessageLike | undefined => {
68
+ if (!isObject(raw)) {
69
+ return undefined;
70
+ }
71
+ const resolved = resolveComponentRef(raw, components, "messages");
72
+ // Still a bare `$ref` after resolution means it pointed nowhere useful.
73
+ if (isString(resolved.$ref)) {
74
+ return undefined;
75
+ }
76
+ // SAFETY: `resolved` is a plain message object; the permissive message view
77
+ // only narrows the fields the components render, all of them optional.
78
+ return resolved as AsyncApiMessageLike;
79
+ };
80
+
81
+ /**
82
+ * The messages one operation carries: its own `messages` refs when declared
83
+ * (each pointing into the channel's message map or at a components message),
84
+ * else every message the channel declares. Unresolvable refs are dropped —
85
+ * the schema tables can only render an actual message object.
86
+ */
87
+ export const operationMessages = (
88
+ operation: AsyncApiOperationObject | undefined,
89
+ channel: AsyncApiChannelObject | undefined,
90
+ document: AsyncApiDocument
91
+ ): NamedMessage[] => {
92
+ const { components } = document;
93
+ const messageMap: Record<string, AsyncApiRefLike> = isObject(
94
+ channel?.messages
95
+ )
96
+ ? channel.messages
97
+ : {};
98
+ const refs = operation?.messages;
99
+ if (!Array.isArray(refs) || refs.length === 0) {
100
+ const all: NamedMessage[] = [];
101
+ for (const [key, raw] of Object.entries(messageMap)) {
102
+ const message = channelMessage(raw, components);
103
+ if (message) {
104
+ all.push({ key, message });
105
+ }
106
+ }
107
+ return all;
108
+ }
109
+ const named: NamedMessage[] = [];
110
+ for (const ref of refs) {
111
+ if (!isObject(ref)) {
112
+ continue;
113
+ }
114
+ const pointer = isString(ref.$ref)
115
+ ? CHANNEL_MESSAGE_REF.exec(ref.$ref)?.groups?.name
116
+ : undefined;
117
+ const key = pointer === undefined ? undefined : unescapePointer(pointer);
118
+ // A channel-message pointer resolves through the channel map; anything
119
+ // else (a components ref, an inline message) resolves directly.
120
+ const message =
121
+ key === undefined
122
+ ? channelMessage(ref, components)
123
+ : channelMessage(messageMap[key], components);
124
+ if (message) {
125
+ named.push({ key: key ?? message.name ?? "message", message });
126
+ }
127
+ }
128
+ return named;
129
+ };
130
+
131
+ /** A message's display name: its `name`/`title`, else its channel-map key. */
132
+ export const messageLabel = (named: NamedMessage): string =>
133
+ named.message.title ?? named.message.name ?? named.key;
134
+
135
+ /**
136
+ * The `schemaFormat` media types whose schemas render as JSON Schema: the
137
+ * AsyncAPI Schema Object (a JSON Schema superset), OpenAPI Schema Objects,
138
+ * and JSON Schema itself, in their bare and `+json`/`+yaml` spellings.
139
+ */
140
+ const JSON_SCHEMA_FORMAT =
141
+ /^application\/(?:vnd\.aai\.asyncapi|vnd\.oai\.openapi|schema)(?:\+(?:json|yaml))?$/u;
142
+
143
+ /**
144
+ * The JSON-schema view of a possibly multi-format schema value
145
+ * (`{ schemaFormat, schema }`, allowed on both message payloads and headers).
146
+ * Unwraps when the format is JSON-schema compatible and yields nothing
147
+ * otherwise (an Avro or Protobuf schema can't render as a schema table —
148
+ * callers fall back to a note).
149
+ */
150
+ export const schemaOf = (value: AsyncApiSpecValue): SchemaLike | undefined => {
151
+ if (!isObject(value)) {
152
+ return undefined;
153
+ }
154
+ if (isString(value.schemaFormat) && "schema" in value) {
155
+ // Match the media type proper (parameters like `;version=…` stripped)
156
+ // against the JSON-Schema-compatible formats AsyncAPI registers. A
157
+ // substring test would misclassify e.g. Avro's `+json` encoding, whose
158
+ // schema is JSON but not JSON Schema.
159
+ const format = (value.schemaFormat.split(";")[0] ?? "")
160
+ .trim()
161
+ .toLowerCase();
162
+ const jsonish = JSON_SCHEMA_FORMAT.test(format);
163
+ if (jsonish && isObject(value.schema)) {
164
+ // SAFETY: a JSON-Schema-format schema object; the permissive SchemaLike
165
+ // view only narrows the fields the schema tables render, all optional.
166
+ return value.schema as SchemaLike;
167
+ }
168
+ return undefined;
169
+ }
170
+ // SAFETY: a bare schema object; the permissive SchemaLike view only narrows
171
+ // the fields the schema tables render, all of them optional.
172
+ return value as SchemaLike;
173
+ };
174
+
175
+ /** The JSON-schema view of a message payload; see {@link schemaOf}. */
176
+ export const payloadSchema = (
177
+ message: AsyncApiMessageLike
178
+ ): SchemaLike | undefined => schemaOf(message.payload);
179
+
180
+ /** The channel-parameter fields {@link channelParameters} lowers. */
181
+ interface AsyncApiParameterLike extends AsyncApiRefLike {
182
+ description?: string;
183
+ default?: AsyncApiSpecValue;
184
+ enum?: AsyncApiSpecValue[];
185
+ }
186
+
187
+ /**
188
+ * Channel parameters lowered into the shared parameter-table shape. AsyncAPI
189
+ * 3.x parameters are always strings (`enum`/`default`/`examples`, no schema),
190
+ * and every one is required — an address template can't resolve without it.
191
+ */
192
+ export const channelParameters = (
193
+ channel: AsyncApiChannelObject | undefined,
194
+ document: AsyncApiDocument
195
+ ): ParameterLike[] => {
196
+ const parameters: ParameterLike[] = [];
197
+ const declared: Record<string, AsyncApiRefLike> = channel?.parameters ?? {};
198
+ for (const [name, raw] of Object.entries(declared)) {
199
+ if (!isObject(raw)) {
200
+ continue;
201
+ }
202
+ // SAFETY: the permissive parameter view only narrows the fields lowered
203
+ // below; each one is runtime-checked before use.
204
+ const parameter = resolveComponentRef(
205
+ raw as AsyncApiParameterLike,
206
+ document.components,
207
+ "parameters"
208
+ );
209
+ const schema: SchemaLike = { type: "string" };
210
+ if (Array.isArray(parameter.enum)) {
211
+ schema.enum = parameter.enum;
212
+ }
213
+ if (parameter.default !== undefined) {
214
+ schema.default = parameter.default;
215
+ }
216
+ parameters.push({
217
+ description: isString(parameter.description)
218
+ ? parameter.description
219
+ : undefined,
220
+ in: "channel",
221
+ name,
222
+ required: true,
223
+ schema,
224
+ });
225
+ }
226
+ return parameters;
227
+ };
228
+
229
+ const SERVER_REF = /^#\/servers\/(?<name>[^/]+)$/u;
230
+
231
+ /**
232
+ * The servers a channel is available on: its `servers` refs when declared,
233
+ * else every server the document declares (the spec's default).
234
+ */
235
+ export const channelServers = (
236
+ channel: AsyncApiChannelObject | undefined,
237
+ document: AsyncApiDocument
238
+ ): AsyncApiServerObject[] => {
239
+ const all = document.servers ?? {};
240
+ const refs = channel?.servers;
241
+ if (!Array.isArray(refs) || refs.length === 0) {
242
+ return Object.values(all).filter(isObject);
243
+ }
244
+ const servers: AsyncApiServerObject[] = [];
245
+ for (const ref of refs) {
246
+ const name = SERVER_REF.exec(
247
+ isObject(ref) && isString(ref.$ref) ? ref.$ref : ""
248
+ )?.groups?.name;
249
+ const server = name === undefined ? undefined : all[unescapePointer(name)];
250
+ if (isObject(server)) {
251
+ servers.push(server);
252
+ }
253
+ }
254
+ return servers;
255
+ };
256
+
257
+ /** Normalize protocol spellings onto the binding key they document. */
258
+ const PROTOCOL_ALIASES = {
259
+ "kafka-secure": "kafka",
260
+ mqtt5: "mqtt",
261
+ mqtts: "mqtt",
262
+ "secure-mqtt": "mqtt",
263
+ wss: "ws",
264
+ };
265
+
266
+ /**
267
+ * The protocol an operation speaks, for binding-aware code samples: the first
268
+ * operation/channel binding key, else the first relevant server's `protocol`.
269
+ */
270
+ export const protocolOf = (
271
+ operation: AsyncApiOperationObject | undefined,
272
+ channel: AsyncApiChannelObject | undefined,
273
+ servers: AsyncApiServerObject[]
274
+ ): string | undefined => {
275
+ const declared =
276
+ Object.keys(operation?.bindings ?? {})[0] ??
277
+ Object.keys(channel?.bindings ?? {})[0] ??
278
+ servers.find((server) => isString(server.protocol))?.protocol;
279
+ if (!isString(declared) || declared === "") {
280
+ return undefined;
281
+ }
282
+ const lower = declared.toLowerCase();
283
+ // SAFETY: guarded by the `in` check, `lower` is one of the alias keys.
284
+ return lower in PROTOCOL_ALIASES
285
+ ? PROTOCOL_ALIASES[lower as keyof typeof PROTOCOL_ALIASES]
286
+ : lower;
287
+ };
288
+
289
+ /**
290
+ * The security list an operation actually enforces: its own `security` when
291
+ * declared, else the union of its servers' — connecting already requires the
292
+ * server's schemes. Server entries dedupe by `$ref`, so two servers sharing a
293
+ * scheme render it once.
294
+ */
295
+ export const asyncApiSecurityEntries = (
296
+ operation: AsyncApiOperationObject | undefined,
297
+ servers: AsyncApiServerObject[]
298
+ ): AsyncApiRefLike[] => {
299
+ if (Array.isArray(operation?.security)) {
300
+ return operation.security;
301
+ }
302
+ const entries: AsyncApiRefLike[] = [];
303
+ const seen = new Set<string>();
304
+ for (const server of servers) {
305
+ for (const entry of server.security ?? []) {
306
+ if (!isObject(entry)) {
307
+ continue;
308
+ }
309
+ if (isString(entry.$ref)) {
310
+ if (seen.has(entry.$ref)) {
311
+ continue;
312
+ }
313
+ seen.add(entry.$ref);
314
+ }
315
+ entries.push(entry);
316
+ }
317
+ }
318
+ return entries;
319
+ };
320
+
321
+ /** One protocol's binding fields, ready for a key/value table. */
322
+ export interface BindingGroup {
323
+ protocol: string;
324
+ rows: { name: string; value: unknown }[];
325
+ }
326
+
327
+ /**
328
+ * Binding maps flattened for display, `bindingVersion` (metadata, not
329
+ * behavior) dropped. Values stay unformatted — the component renders schema-ish
330
+ * objects as nested schema tables and everything else as code.
331
+ */
332
+ export const bindingGroups = (
333
+ bindings?: AsyncApiChannelObject["bindings"]
334
+ ): BindingGroup[] => {
335
+ const groups: BindingGroup[] = [];
336
+ for (const [protocol, fields] of Object.entries(bindings ?? {})) {
337
+ if (!isObject(fields)) {
338
+ continue;
339
+ }
340
+ const rows = Object.entries(fields)
341
+ .filter(([name]) => name !== "bindingVersion")
342
+ .map(([name, value]) => ({ name, value }));
343
+ if (rows.length > 0) {
344
+ groups.push({ protocol, rows });
345
+ }
346
+ }
347
+ return groups;
348
+ };
@@ -8,6 +8,22 @@ import { sample } from "openapi-sampler";
8
8
  * values come from openapi-sampler, which is likewise browser-safe.
9
9
  */
10
10
 
11
+ /** Any value a parsed OpenAPI document can hold: JSON, nested schemas included. */
12
+ export type SpecValue =
13
+ | string
14
+ | number
15
+ | boolean
16
+ | null
17
+ | undefined
18
+ | SpecValue[]
19
+ | { [key: string]: SpecValue };
20
+
21
+ const isString = (value: SpecValue): value is string =>
22
+ typeof value === "string";
23
+
24
+ const isNumber = (value: SpecValue): value is number =>
25
+ typeof value === "number";
26
+
11
27
  /** A permissive view of an OpenAPI 3.1 schema — only the fields we render. */
12
28
  export interface SchemaLike {
13
29
  $ref?: string;
@@ -18,11 +34,11 @@ export interface SchemaLike {
18
34
  properties?: Record<string, SchemaLike>;
19
35
  required?: string[];
20
36
  items?: SchemaLike;
21
- enum?: unknown[];
22
- const?: unknown;
23
- default?: unknown;
24
- example?: unknown;
25
- examples?: unknown[];
37
+ enum?: SpecValue[];
38
+ const?: SpecValue;
39
+ default?: SpecValue;
40
+ example?: SpecValue;
41
+ examples?: SpecValue[];
26
42
  allOf?: SchemaLike[];
27
43
  oneOf?: SchemaLike[];
28
44
  anyOf?: SchemaLike[];
@@ -38,7 +54,7 @@ export interface SchemaLike {
38
54
  minItems?: number;
39
55
  maxItems?: number;
40
56
  pattern?: string;
41
- [key: string]: unknown;
57
+ [key: string]: SpecValue;
42
58
  }
43
59
 
44
60
  /** A permissive view of an operation parameter — only the fields we render. */
@@ -50,9 +66,16 @@ export interface ParameterLike {
50
66
  required?: boolean;
51
67
  deprecated?: boolean;
52
68
  schema?: SchemaLike;
53
- example?: unknown;
69
+ example?: SpecValue;
70
+ [key: string]: SpecValue;
54
71
  }
55
72
 
73
+ /** The `components` object of a parsed spec: section name → named-node table. */
74
+ export type ComponentsLike = Record<
75
+ string,
76
+ Record<string, SpecValue> | undefined
77
+ >;
78
+
56
79
  const REF_PATTERN = /#\/components\/schemas\/(?<name>[^/]+)$/u;
57
80
 
58
81
  const COMPONENT_REF = /#\/components\/(?<section>[^/]+)\/(?<name>[^/]+)$/u;
@@ -65,16 +88,18 @@ const COMPONENT_REF = /#\/components\/(?<section>[^/]+)\/(?<name>[^/]+)$/u;
65
88
  */
66
89
  export const resolveComponentRef = <T extends { $ref?: string }>(
67
90
  node: T,
68
- components: Record<string, unknown> | undefined,
91
+ components: ComponentsLike | undefined,
69
92
  section: string
70
93
  ): T => {
71
- if (typeof node.$ref !== "string") {
94
+ if (!isString(node.$ref)) {
72
95
  return node;
73
96
  }
74
97
  const groups = COMPONENT_REF.exec(node.$ref)?.groups;
75
98
  if (groups?.section !== section) {
76
99
  return node;
77
100
  }
101
+ // SAFETY: a components section table stores nodes of that section's type,
102
+ // and callers always pair `section` with the matching `T`.
78
103
  const table = components?.[section] as Record<string, T> | undefined;
79
104
  return table?.[groups.name ?? ""] ?? node;
80
105
  };
@@ -88,7 +113,7 @@ export const resolveComponentRef = <T extends { $ref?: string }>(
88
113
  export const mergeParameters = (
89
114
  pathParameters: ParameterLike[] | undefined,
90
115
  operationParameters: ParameterLike[] | undefined,
91
- components?: Record<string, unknown>
116
+ components?: ComponentsLike
92
117
  ): ParameterLike[] => {
93
118
  const merged = new Map<string, ParameterLike>();
94
119
  let position = 0;
@@ -118,7 +143,7 @@ export const resolveSchema = (
118
143
  if (!schema) {
119
144
  return {};
120
145
  }
121
- if (typeof schema.$ref === "string") {
146
+ if (isString(schema.$ref)) {
122
147
  const name = REF_PATTERN.exec(schema.$ref)?.groups?.name;
123
148
  if (name && schemas[name]) {
124
149
  return schemas[name];
@@ -140,7 +165,7 @@ const nonNullTypes = (type: string | string[] | undefined): string[] => {
140
165
  * array items can't recurse forever.
141
166
  */
142
167
  export const typeLabel = (schema: SchemaLike): string => {
143
- if (typeof schema.$ref === "string") {
168
+ if (isString(schema.$ref)) {
144
169
  return refName(schema.$ref);
145
170
  }
146
171
  if (schema.oneOf || schema.anyOf) {
@@ -177,11 +202,11 @@ export const constraints = (schema: SchemaLike): string[] => {
177
202
  ];
178
203
  for (const [key, label] of numeric) {
179
204
  const value = schema[key];
180
- if (typeof value === "number") {
205
+ if (isNumber(value)) {
181
206
  out.push(`${label} ${value}`);
182
207
  }
183
208
  }
184
- if (typeof schema.pattern === "string") {
209
+ if (isString(schema.pattern)) {
185
210
  out.push(`matches ${schema.pattern}`);
186
211
  }
187
212
  if (schema.default !== undefined) {
@@ -190,6 +215,12 @@ export const constraints = (schema: SchemaLike): string[] => {
190
215
  return out;
191
216
  };
192
217
 
218
+ /** The merged property list and required set a schema exposes. */
219
+ export interface ObjectPropertySet {
220
+ properties: [string, SchemaLike][];
221
+ required: Set<string>;
222
+ }
223
+
193
224
  /**
194
225
  * The object properties a schema exposes, merging `allOf` branches so an
195
226
  * `allOf`-composed model still lists every field. Returns the properties plus
@@ -198,7 +229,7 @@ export const constraints = (schema: SchemaLike): string[] => {
198
229
  export const objectProperties = (
199
230
  schema: SchemaLike,
200
231
  schemas: Record<string, SchemaLike>
201
- ): { properties: [string, SchemaLike][]; required: Set<string> } => {
232
+ ): ObjectPropertySet => {
202
233
  const properties = new Map<string, SchemaLike>();
203
234
  const required = new Set<string>();
204
235
  // Cycles can only enter through `$ref`s (inline JSON can't self-nest), so
@@ -206,7 +237,7 @@ export const objectProperties = (
206
237
  const seen = new Set<string>();
207
238
 
208
239
  const collect = (node: SchemaLike): void => {
209
- if (typeof node.$ref === "string") {
240
+ if (isString(node.$ref)) {
210
241
  if (seen.has(node.$ref)) {
211
242
  return;
212
243
  }
@@ -239,16 +270,18 @@ export const objectProperties = (
239
270
  export const exampleValue = (
240
271
  schema: SchemaLike | undefined,
241
272
  schemas: Record<string, SchemaLike>
242
- ): unknown => {
273
+ ): SpecValue => {
243
274
  if (!schema) {
244
275
  return null;
245
276
  }
246
277
  try {
278
+ // SAFETY: SchemaLike structurally covers the JSONSchema7 fields the
279
+ // sampler reads, and the sampler only ever assembles JSON values.
247
280
  return sample(
248
281
  schema as Parameters<typeof sample>[0],
249
282
  { quiet: true, skipReadOnly: true },
250
283
  { components: { schemas } }
251
- );
284
+ ) as SpecValue;
252
285
  } catch {
253
286
  // An unresolvable $ref or malformed schema is a spec problem the schema
254
287
  // tables already surface; a sample is best-effort.
@@ -257,5 +290,4 @@ export const exampleValue = (
257
290
  };
258
291
 
259
292
  /** Pretty-print a JSON value for an example/code block. */
260
- export const toJson = (value: unknown): string =>
261
- JSON.stringify(value, null, 2);
293
+ export const toJson = <T>(value: T): string => JSON.stringify(value, null, 2);
@@ -6,6 +6,8 @@
6
6
  * theme.
7
7
  */
8
8
 
9
+ import { copyText, createCopyFlash } from "../copy-feedback.ts";
10
+
9
11
  class BlumePanelTabs extends HTMLElement {
10
12
  connectedCallback() {
11
13
  const tabs = [
@@ -36,18 +38,19 @@ class BlumePanelTabs extends HTMLElement {
36
38
  }
37
39
 
38
40
  if (copy) {
41
+ const flash = createCopyFlash((copied) => {
42
+ if (copied) {
43
+ copy.dataset.copied = "true";
44
+ } else {
45
+ delete copy.dataset.copied;
46
+ }
47
+ }, "Copied");
39
48
  copy.addEventListener("click", async () => {
40
49
  const active = panels.find(
41
50
  (panel) => !panel.classList.contains("hidden")
42
51
  );
43
- try {
44
- await navigator.clipboard.writeText(active?.textContent ?? "");
45
- copy.dataset.copied = "true";
46
- setTimeout(() => {
47
- delete copy.dataset.copied;
48
- }, 1500);
49
- } catch {
50
- // Clipboard unavailable (insecure context); silently ignore.
52
+ if (await copyText(active?.textContent ?? "")) {
53
+ flash();
51
54
  }
52
55
  });
53
56
  }