blume 1.4.3 → 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 (194) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli/index.js +1621 -576
  3. package/dist/cli/index.js.map +109 -104
  4. package/dist/types/ai/component-markdown.d.ts +14 -4
  5. package/dist/types/core/config-input.d.ts +79 -27
  6. package/dist/types/core/config.d.ts +2 -1
  7. package/dist/types/core/data.d.ts +16 -1
  8. package/dist/types/core/diagnostics.d.ts +5 -1
  9. package/dist/types/core/i18n-ui.d.ts +12 -0
  10. package/dist/types/core/schema.d.ts +112 -15
  11. package/dist/types/core/sources/types.d.ts +3 -1
  12. package/dist/types/core/standard-schema.d.ts +7 -3
  13. package/dist/types/core/types.d.ts +43 -2
  14. package/dist/types/core/ui-packs/index.d.ts +9 -1
  15. package/dist/types/openapi/references.d.ts +6 -5
  16. package/dist/types/seo/x-handle.d.ts +3 -2
  17. package/docs/advanced/api-reference.mdx +8 -6
  18. package/docs/configuration/search.mdx +2 -0
  19. package/docs/configuration/seo.mdx +1 -1
  20. package/docs/content/i18n.mdx +1 -1
  21. package/docs/content/meta.mdx +2 -1
  22. package/docs/content/meta.ts +1 -0
  23. package/docs/content/navigation.mdx +35 -1
  24. package/docs/content/versioning.mdx +106 -0
  25. package/docs/reference/cli.mdx +1 -0
  26. package/docs/reference/frontmatter.mdx +3 -0
  27. package/package.json +3 -1
  28. package/skills/blume-migrate/SKILL.md +2 -2
  29. package/skills/blume-migrate/references/docusaurus.md +1 -1
  30. package/skills/blume-migrate/references/fumadocs.md +1 -1
  31. package/skills/blume-migrate/references/mintlify.md +1 -1
  32. package/src/ai/agent-readability.ts +37 -10
  33. package/src/ai/ask-context.ts +5 -1
  34. package/src/ai/ask.ts +10 -1
  35. package/src/ai/component-markdown.ts +80 -43
  36. package/src/ai/llms.ts +40 -16
  37. package/src/ai/mcp/data.ts +48 -12
  38. package/src/ai/mcp/discovery.ts +28 -11
  39. package/src/ai/mcp/server.ts +183 -38
  40. package/src/ai/mcp/tools.ts +3 -3
  41. package/src/ai/skills.ts +32 -9
  42. package/src/ai/visibility.ts +2 -2
  43. package/src/astro/component-slots.ts +2 -0
  44. package/src/astro/examples.ts +6 -2
  45. package/src/astro/generate.ts +54 -29
  46. package/src/astro/integration.ts +13 -2
  47. package/src/astro/islands.ts +16 -9
  48. package/src/astro/templates.ts +152 -33
  49. package/src/audit/agent.ts +2 -2
  50. package/src/audit/checks/content.ts +26 -11
  51. package/src/audit/checks/dns-aid.ts +3 -0
  52. package/src/audit/checks/indexability.ts +24 -6
  53. package/src/audit/checks/llms.ts +9 -4
  54. package/src/audit/checks/network.ts +2 -0
  55. package/src/audit/checks/social.ts +18 -10
  56. package/src/audit/crawl.ts +37 -9
  57. package/src/audit/report.ts +20 -19
  58. package/src/audit/run.ts +5 -2
  59. package/src/audit/snapshot.ts +2 -4
  60. package/src/audit/types.ts +25 -3
  61. package/src/blume-modules.d.ts +5 -1
  62. package/src/cli/commands/audit.ts +9 -4
  63. package/src/cli/commands/build.ts +15 -9
  64. package/src/cli/commands/dev.ts +2 -0
  65. package/src/cli/commands/doctor.ts +2 -0
  66. package/src/cli/commands/eval.ts +7 -3
  67. package/src/cli/commands/init.ts +9 -9
  68. package/src/cli/commands/mcp-stdio.ts +3 -0
  69. package/src/cli/commands/translate.ts +14 -3
  70. package/src/cli/commands/version.ts +85 -0
  71. package/src/cli/dev-lock.ts +31 -10
  72. package/src/cli/eject-scripts.ts +17 -2
  73. package/src/cli/index.ts +2 -0
  74. package/src/cli/init/questions.ts +1 -1
  75. package/src/cli/init/scaffold.ts +22 -15
  76. package/src/cli/internal-error.ts +1 -0
  77. package/src/components/content/auto-type-table.ts +3 -0
  78. package/src/components/content/diff.ts +9 -5
  79. package/src/components/content/github-info.ts +2 -0
  80. package/src/components/islands/ask-ai.tsx +33 -25
  81. package/src/components/islands/hooks.ts +5 -1
  82. package/src/components/islands/webmcp.ts +49 -12
  83. package/src/components/layout/Header.astro +25 -1
  84. package/src/components/layout/NavSelector.astro +11 -2
  85. package/src/components/layout/NavTree.astro +4 -2
  86. package/src/components/layout/RootLayout.astro +18 -0
  87. package/src/components/layout/Search.astro +77 -13
  88. package/src/components/layout/VersionBanner.astro +39 -0
  89. package/src/components/layout/analytics-client.ts +8 -5
  90. package/src/components/layout/hydration-hint.ts +1 -1
  91. package/src/components/layout/nav-utils.ts +1 -4
  92. package/src/components/layout/overrides.ts +25 -12
  93. package/src/components/layout/search/algolia.ts +18 -5
  94. package/src/components/layout/search/endpoint.ts +3 -0
  95. package/src/components/layout/search/flexsearch.ts +23 -7
  96. package/src/components/layout/search/orama-cloud.ts +1 -1
  97. package/src/components/layout/search/orama.ts +4 -1
  98. package/src/components/layout/search/pagefind.ts +2 -0
  99. package/src/components/layout/search/types.ts +13 -1
  100. package/src/components/layout/search/typesense.ts +19 -3
  101. package/src/components/openapi/ApiOverview.astro +32 -6
  102. package/src/components/openapi/AsyncApiOperation.astro +237 -0
  103. package/src/components/openapi/Bindings.astro +89 -0
  104. package/src/components/openapi/MethodBadge.astro +3 -0
  105. package/src/components/openapi/Operation.astro +7 -2
  106. package/src/components/openapi/PanelTabs.astro +131 -0
  107. package/src/components/openapi/ParametersTable.astro +2 -0
  108. package/src/components/openapi/RequestPanel.astro +12 -119
  109. package/src/components/openapi/async-snippets.ts +174 -0
  110. package/src/components/openapi/async.ts +348 -0
  111. package/src/components/openapi/helpers.ts +52 -20
  112. package/src/components/openapi/security.ts +102 -29
  113. package/src/components/openapi/snippets.ts +11 -11
  114. package/src/core/component-overrides.ts +28 -23
  115. package/src/core/config-input.ts +88 -27
  116. package/src/core/config.ts +20 -7
  117. package/src/core/content.ts +3 -1
  118. package/src/core/data.ts +16 -1
  119. package/src/core/define-components.ts +5 -0
  120. package/src/core/diagnostics.ts +46 -38
  121. package/src/core/frontmatter.ts +33 -7
  122. package/src/core/graph.ts +137 -53
  123. package/src/core/i18n-ui.ts +15 -0
  124. package/src/core/i18n.ts +16 -8
  125. package/src/core/load-module.ts +1 -0
  126. package/src/core/manifest.ts +92 -3
  127. package/src/core/meta.ts +44 -14
  128. package/src/core/nav-diagnostics.ts +3 -3
  129. package/src/core/navigation.ts +247 -67
  130. package/src/core/project-graph.ts +15 -3
  131. package/src/core/schema.ts +213 -67
  132. package/src/core/sources/assets.ts +2 -0
  133. package/src/core/sources/cache.ts +6 -0
  134. package/src/core/sources/github-releases.ts +39 -31
  135. package/src/core/sources/mdx-remote.ts +4 -0
  136. package/src/core/sources/normalize.ts +67 -20
  137. package/src/core/sources/notion.ts +49 -17
  138. package/src/core/sources/portable-text.ts +32 -11
  139. package/src/core/sources/sanity.ts +68 -14
  140. package/src/core/sources/types.ts +4 -0
  141. package/src/core/sources/watch.ts +1 -1
  142. package/src/core/standard-schema.ts +9 -3
  143. package/src/core/text-width.ts +26 -0
  144. package/src/core/tsconfig-aliases.ts +9 -5
  145. package/src/core/types.ts +45 -2
  146. package/src/core/ui-packs/index.ts +9 -1
  147. package/src/core/version-cut.ts +301 -0
  148. package/src/core/version.ts +2 -0
  149. package/src/core/versions.ts +170 -0
  150. package/src/deploy/adapter-output.ts +5 -2
  151. package/src/deploy/cloudflare-negotiation.ts +25 -10
  152. package/src/deploy/sitemap.ts +33 -1
  153. package/src/deploy/vercel-negotiation.ts +11 -4
  154. package/src/eval/report.ts +4 -4
  155. package/src/eval/run.ts +2 -2
  156. package/src/eval/schema.ts +1 -1
  157. package/src/markdown/base-links.ts +6 -6
  158. package/src/markdown/directives.ts +7 -1
  159. package/src/markdown/heading-anchors.ts +17 -6
  160. package/src/markdown/index.ts +73 -24
  161. package/src/markdown/inline-code.ts +14 -2
  162. package/src/markdown/language-icon.ts +6 -2
  163. package/src/markdown/mdast.ts +18 -4
  164. package/src/markdown/package-commands.ts +6 -8
  165. package/src/markdown/table-wrap.ts +4 -1
  166. package/src/markdown/twoslash.ts +2 -0
  167. package/src/og/card.ts +30 -11
  168. package/src/og/derive.ts +43 -27
  169. package/src/openapi/asyncapi.ts +366 -0
  170. package/src/openapi/model.ts +126 -57
  171. package/src/openapi/parse.ts +97 -5
  172. package/src/openapi/references.ts +12 -10
  173. package/src/openapi/render-mdx.ts +73 -34
  174. package/src/openapi/scalar.ts +6 -8
  175. package/src/openapi/source.ts +98 -28
  176. package/src/registry/eject.ts +7 -2
  177. package/src/search/documents.ts +25 -5
  178. package/src/search/facets.ts +7 -5
  179. package/src/search/orama-index.ts +66 -20
  180. package/src/search/popular.ts +10 -5
  181. package/src/search/providers.ts +2 -2
  182. package/src/search/sync/index.ts +2 -0
  183. package/src/search/sync/typesense.ts +4 -2
  184. package/src/seo/jsonld.ts +24 -6
  185. package/src/seo/x-handle.ts +8 -3
  186. package/src/theme/chrome-icons.ts +7 -2
  187. package/src/theme/fonts.ts +8 -4
  188. package/src/theme/icons.ts +4 -2
  189. package/src/theme/palette.ts +22 -14
  190. package/src/translate/meta.ts +15 -6
  191. package/src/translate/report.ts +9 -5
  192. package/src/translate/run.ts +10 -4
  193. package/src/translate/validate.ts +52 -17
  194. package/src/translate/work-list.ts +0 -0
@@ -3,6 +3,7 @@ import data from "blume:data";
3
3
 
4
4
  import { highlightCode } from "../../markdown/index.ts";
5
5
  import { exampleValue, type SchemaLike, toJson } from "./helpers.ts";
6
+ import PanelTabs from "./PanelTabs.astro";
6
7
  import type { RequestSample, SampleLanguage } from "./snippets.ts";
7
8
 
8
9
  interface MediaTypeLike {
@@ -24,27 +25,18 @@ interface Props {
24
25
 
25
26
  const { sample, languages, responses, schemas } = Astro.props;
26
27
 
27
- // A `.prose` wrapper gives Shiki its scoped token colors; the global style at
28
- // the foot of this file strips the standalone code block's own box (border,
29
- // injected copy button, language label) so the code sits flush inside the one
30
- // panel border.
31
- const CODE_WRAP = "prose max-w-none text-xs";
32
- const TAB_CLASS =
33
- "-mb-px cursor-pointer border-transparent border-b-2 bg-transparent py-2 font-medium text-muted-foreground text-xs transition-colors hover:text-foreground aria-[selected=true]:border-accent aria-[selected=true]:text-foreground";
34
- const HEADING = "mb-2 font-semibold text-foreground text-sm";
35
-
36
- const requestSamples = await Promise.all(
28
+ const requestPanels = await Promise.all(
37
29
  languages.map(async (language) => ({
38
30
  html: await highlightCode(language.build(sample), language.lang, {
39
31
  icons: false,
40
32
  themes: data.config.codeThemes,
41
33
  }),
42
- id: language.id,
34
+ key: language.id,
43
35
  label: language.label,
44
36
  }))
45
37
  );
46
38
 
47
- const responseEntries = await Promise.all(
39
+ const responsePanels = await Promise.all(
48
40
  Object.entries(responses).map(async ([status, response]) => {
49
41
  const media =
50
42
  Object.entries(response.content ?? {}).find(([type]) =>
@@ -60,116 +52,17 @@ const responseEntries = await Promise.all(
60
52
  icons: false,
61
53
  themes: data.config.codeThemes,
62
54
  });
63
- return { description: response.description ?? "", html, status };
55
+ return {
56
+ html,
57
+ key: status,
58
+ label: status,
59
+ text: response.description || "No example response.",
60
+ };
64
61
  })
65
62
  );
66
63
  ---
67
64
 
68
65
  <div class="not-prose flex flex-col gap-6">
69
- {
70
- requestSamples.length > 0 && (
71
- <div>
72
- <div aria-level="3" class={HEADING} role="heading">
73
- Request
74
- </div>
75
- <blume-panel-tabs class="block overflow-hidden rounded-blume border border-border bg-background">
76
- <div class="flex items-center justify-between gap-2 border-border border-b px-3">
77
- <div class="flex gap-4" role="tablist">
78
- {requestSamples.map((entry, index) => (
79
- <button
80
- aria-selected={index === 0 ? "true" : "false"}
81
- class={TAB_CLASS}
82
- data-panel-tab={entry.id}
83
- role="tab"
84
- type="button"
85
- >
86
- {entry.label}
87
- </button>
88
- ))}
89
- </div>
90
- <button
91
- aria-label="Copy request"
92
- class="group shrink-0 cursor-pointer rounded px-1.5 py-1 text-muted-foreground text-xs hover:text-foreground"
93
- data-panel-copy
94
- type="button"
95
- >
96
- <span class="group-data-[copied]:hidden">Copy</span>
97
- <span class="hidden group-data-[copied]:inline">Copied</span>
98
- </button>
99
- </div>
100
- {requestSamples.map((entry, index) => (
101
- <div
102
- class:list={[index === 0 ? "" : "hidden", CODE_WRAP]}
103
- data-panel={entry.id}
104
- >
105
- <Fragment set:html={entry.html} />
106
- </div>
107
- ))}
108
- </blume-panel-tabs>
109
- </div>
110
- )
111
- }
112
- {
113
- responseEntries.length > 0 && (
114
- <div>
115
- <div aria-level="3" class={HEADING} role="heading">
116
- Response
117
- </div>
118
- <blume-panel-tabs class="block overflow-hidden rounded-blume border border-border bg-background">
119
- <div class="flex flex-wrap gap-4 border-border border-b px-3" role="tablist">
120
- {responseEntries.map((entry, index) => (
121
- <button
122
- aria-selected={index === 0 ? "true" : "false"}
123
- class={`${TAB_CLASS} font-mono`}
124
- data-panel-tab={entry.status}
125
- role="tab"
126
- type="button"
127
- >
128
- {entry.status}
129
- </button>
130
- ))}
131
- </div>
132
- {responseEntries.map((entry, index) => (
133
- <div
134
- class:list={[index === 0 ? "" : "hidden"]}
135
- data-panel={entry.status}
136
- >
137
- {entry.html ? (
138
- <div class={CODE_WRAP}>
139
- <Fragment set:html={entry.html} />
140
- </div>
141
- ) : (
142
- <div class="px-3 py-4 text-muted-foreground text-xs">
143
- {entry.description || "No example response."}
144
- </div>
145
- )}
146
- </div>
147
- ))}
148
- </blume-panel-tabs>
149
- </div>
150
- )
151
- }
66
+ <PanelTabs copy heading="Request" panels={requestPanels} />
67
+ <PanelTabs heading="Response" mono panels={responsePanels} />
152
68
  </div>
153
-
154
- <script>
155
- import "./panel.ts";
156
- </script>
157
-
158
- <style is:global>
159
- /* Strip the standalone code block's own chrome inside a panel: the border,
160
- margin, radius, and the copy button + language label the prose code theme
161
- adds — the panel supplies a single border and its own copy button. */
162
- blume-panel-tabs pre.astro-code {
163
- margin: 0 !important;
164
- border: 0 !important;
165
- border-radius: 0 !important;
166
- background: transparent !important;
167
- padding: 0.75rem 1rem !important;
168
- }
169
- blume-panel-tabs pre.astro-code::before {
170
- content: none !important;
171
- }
172
- blume-panel-tabs [data-blume-copy] {
173
- display: none !important;
174
- }
175
- </style>
@@ -0,0 +1,174 @@
1
+ import type {
2
+ AsyncApiAction,
3
+ AsyncApiServerObject,
4
+ } from "../../openapi/asyncapi.ts";
5
+ import { toJson } from "./helpers.ts";
6
+
7
+ /**
8
+ * Protocol-aware code samples for AsyncAPI operations — the async counterpart
9
+ * of `snippets.ts`. Samples are written from the reader's side of the wire:
10
+ * a `receive` operation means the application receives, so the sample shows
11
+ * how to *produce* a message; a `send` operation shows how to consume one.
12
+ * Protocols without a supported tool yield no samples at all — the message
13
+ * example panel already shows the payload, and fabricating a client for an
14
+ * unknown binding would be worse than nothing.
15
+ */
16
+
17
+ /** Everything a snippet builder needs about one operation. */
18
+ export interface MessageSample {
19
+ action: AsyncApiAction;
20
+ /** Channel address, `{param}` templates left intact. */
21
+ address: string;
22
+ /** Example payload value (undefined when none could be derived). */
23
+ payload?: unknown;
24
+ /** First server the channel is available on, if any. */
25
+ server?: AsyncApiServerObject;
26
+ }
27
+
28
+ /** `host[:port]` split apart; MQTT tooling wants them as separate flags. */
29
+ const hostParts = (server?: AsyncApiServerObject) => {
30
+ const raw = server?.host ?? "localhost";
31
+ const colon = raw.lastIndexOf(":");
32
+ if (colon > 0 && /^\d+$/u.test(raw.slice(colon + 1))) {
33
+ return { host: raw.slice(0, colon), port: raw.slice(colon + 1) };
34
+ }
35
+ return { host: raw, port: undefined };
36
+ };
37
+
38
+ /** POSIX single-quote escaping, matching `snippets.ts`. */
39
+ const shellQuote = (text: string): string =>
40
+ `'${text.replaceAll("'", String.raw`'\''`)}'`;
41
+
42
+ const payloadJson = (sample: MessageSample): string =>
43
+ toJson(sample.payload ?? {});
44
+
45
+ /** Compact single-line payload for shell `-m`/`echo` arguments. */
46
+ const payloadInline = (sample: MessageSample): string =>
47
+ JSON.stringify(sample.payload ?? {});
48
+
49
+ /** `wss://host/path` for a WebSocket channel; the address is the path. */
50
+ const wsUrl = (sample: MessageSample): string => {
51
+ const { server } = sample;
52
+ const scheme = server?.protocol === "ws" ? "ws" : "wss";
53
+ const host = server?.host ?? "localhost";
54
+ const base = `${server?.pathname ?? ""}/${sample.address}`.replaceAll(
55
+ /\/+/gu,
56
+ "/"
57
+ );
58
+ return `${scheme}://${host}${base === "/" ? "" : base}`;
59
+ };
60
+
61
+ const wscatSnippet = (sample: MessageSample): string => {
62
+ const connect = `wscat -c ${shellQuote(wsUrl(sample))}`;
63
+ return sample.action === "receive"
64
+ ? `${connect}\n> ${payloadInline(sample)}`
65
+ : `# Prints each message as it arrives\n${connect}`;
66
+ };
67
+
68
+ const webSocketSnippet = (sample: MessageSample): string => {
69
+ const open = `const socket = new WebSocket(${JSON.stringify(wsUrl(sample))});`;
70
+ if (sample.action === "receive") {
71
+ return [
72
+ open,
73
+ "",
74
+ 'socket.addEventListener("open", () => {',
75
+ ` socket.send(JSON.stringify(${payloadJson(sample).replaceAll("\n", "\n ")}));`,
76
+ "});",
77
+ ].join("\n");
78
+ }
79
+ return [
80
+ open,
81
+ "",
82
+ 'socket.addEventListener("message", (event) => {',
83
+ " console.log(JSON.parse(event.data));",
84
+ "});",
85
+ ].join("\n");
86
+ };
87
+
88
+ const kcatSnippet = (sample: MessageSample): string => {
89
+ const broker = sample.server?.host ?? "localhost:9092";
90
+ const base = `kcat -b ${shellQuote(broker)} -t ${shellQuote(sample.address)}`;
91
+ return sample.action === "receive"
92
+ ? `echo ${shellQuote(payloadInline(sample))} | ${base} -P`
93
+ : `${base} -C`;
94
+ };
95
+
96
+ const mosquittoSnippet = (sample: MessageSample): string => {
97
+ const { host, port } = hostParts(sample.server);
98
+ const target = `-h ${shellQuote(host)}${port ? ` -p ${port}` : ""} -t ${shellQuote(sample.address)}`;
99
+ return sample.action === "receive"
100
+ ? `mosquitto_pub ${target} -m ${shellQuote(payloadInline(sample))}`
101
+ : `mosquitto_sub ${target} -v`;
102
+ };
103
+
104
+ /** One renderable sample tool: tab id/label, Shiki language, builder. */
105
+ export interface AsyncSampleLanguage {
106
+ id: string;
107
+ label: string;
108
+ lang: string;
109
+ build: (sample: MessageSample) => string;
110
+ }
111
+
112
+ const TOOLS = {
113
+ js: { build: webSocketSnippet, id: "js", label: "JavaScript", lang: "js" },
114
+ kcat: { build: kcatSnippet, id: "kcat", label: "kcat", lang: "bash" },
115
+ mosquitto: {
116
+ build: mosquittoSnippet,
117
+ id: "mosquitto",
118
+ label: "mosquitto",
119
+ lang: "bash",
120
+ },
121
+ wscat: { build: wscatSnippet, id: "wscat", label: "wscat", lang: "bash" },
122
+ } satisfies Record<string, AsyncSampleLanguage>;
123
+
124
+ type ToolId = keyof typeof TOOLS;
125
+
126
+ /** The tools appropriate to each protocol binding, in display order. */
127
+ const PROTOCOL_TOOLS = new Map<string, readonly ToolId[]>([
128
+ ["kafka", ["kcat"]],
129
+ ["mqtt", ["mosquitto"]],
130
+ ["ws", ["wscat", "js"]],
131
+ ]);
132
+
133
+ /** Accepted spellings for configured `codeSamples` ids. */
134
+ const ALIASES = new Map<string, ToolId>([
135
+ ["javascript", "js"],
136
+ ["kafkacat", "kcat"],
137
+ ["mosquitto_pub", "mosquitto"],
138
+ ["mosquitto_sub", "mosquitto"],
139
+ ["node", "js"],
140
+ ["typescript", "js"],
141
+ ["websocket", "js"],
142
+ ]);
143
+
144
+ /**
145
+ * The sample tools to render for an operation. The protocol picks the
146
+ * candidate set; a non-empty `codeSamples` config filters and orders it
147
+ * (unknown ids are dropped, aliases accepted). No protocol, an unsupported
148
+ * one, or a filter that matches nothing yields no samples.
149
+ */
150
+ export const asyncSampleLanguages = (
151
+ ids: string[],
152
+ protocol?: string
153
+ ): AsyncSampleLanguage[] => {
154
+ const candidates = PROTOCOL_TOOLS.get(protocol ?? "") ?? [];
155
+ if (candidates.length === 0) {
156
+ return [];
157
+ }
158
+ if (ids.length === 0) {
159
+ return candidates.map((id) => TOOLS[id]);
160
+ }
161
+ const chosen: AsyncSampleLanguage[] = [];
162
+ const seen = new Set<string>();
163
+ for (const raw of ids) {
164
+ // Case-insensitive like `sampleLanguages` — the docs spell the tools
165
+ // `WebSocket`/`mosquitto_pub`, so configured ids arrive in any casing.
166
+ const id = ALIASES.get(raw.toLowerCase()) ?? raw.toLowerCase();
167
+ const match = candidates.find((candidate) => candidate === id);
168
+ if (match && !seen.has(match)) {
169
+ seen.add(match);
170
+ chosen.push(TOOLS[match]);
171
+ }
172
+ }
173
+ return chosen;
174
+ };
@@ -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
+ };