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.
- package/CHANGELOG.md +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +116 -15
- package/dist/types/core/sources/types.d.ts +11 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/sources.mdx +1 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +13 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +8 -5
- package/src/components/layout/search/types.ts +45 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +74 -4
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { BindingGroup } from "./async.ts";
|
|
3
|
+
import type { SchemaLike } from "./helpers.ts";
|
|
4
|
+
import SchemaTable from "./SchemaTable.astro";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Protocol binding fields as a key/value list, one group per protocol
|
|
8
|
+
* (`kafka`, `ws`, `mqtt`, …). Schema-shaped values — the ws binding's `query`
|
|
9
|
+
* and `headers` — render as nested schema tables; everything else as code.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
title: string;
|
|
13
|
+
groups: BindingGroup[];
|
|
14
|
+
schemas: Record<string, SchemaLike>;
|
|
15
|
+
expandAll?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { title, groups, schemas, expandAll = false } = Astro.props;
|
|
19
|
+
|
|
20
|
+
// A bare `type` key is not enough — binding sub-objects carry protocol enums
|
|
21
|
+
// there (the AMQP exchange's `type: "topic"`), so only a JSON-Schema type
|
|
22
|
+
// value counts.
|
|
23
|
+
const SCHEMA_TYPES = new Set([
|
|
24
|
+
"array",
|
|
25
|
+
"boolean",
|
|
26
|
+
"integer",
|
|
27
|
+
"null",
|
|
28
|
+
"number",
|
|
29
|
+
"object",
|
|
30
|
+
"string",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const isSchemaish = (value: unknown): value is SchemaLike => {
|
|
34
|
+
if (typeof value !== "object" || value === null) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if ("properties" in value || "$ref" in value) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
const { type } = value as { type?: unknown };
|
|
41
|
+
return typeof type === "string" && SCHEMA_TYPES.has(type);
|
|
42
|
+
};
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
groups.length > 0 && (
|
|
47
|
+
<section class="mt-6">
|
|
48
|
+
<div
|
|
49
|
+
aria-level="2"
|
|
50
|
+
class="mb-2 font-semibold text-foreground text-sm"
|
|
51
|
+
role="heading"
|
|
52
|
+
>
|
|
53
|
+
{title}
|
|
54
|
+
</div>
|
|
55
|
+
{groups.map((group) => (
|
|
56
|
+
<div class="not-prose mb-3 rounded-blume border border-border px-4 last:mb-0">
|
|
57
|
+
<div class="flex items-baseline gap-2 border-border py-3">
|
|
58
|
+
<code class="font-mono font-semibold text-foreground text-sm">
|
|
59
|
+
{group.protocol}
|
|
60
|
+
</code>
|
|
61
|
+
</div>
|
|
62
|
+
{group.rows.map((row) => (
|
|
63
|
+
<div class="border-border border-t py-3">
|
|
64
|
+
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
65
|
+
<code class="font-mono text-foreground text-sm">{row.name}</code>
|
|
66
|
+
{!isSchemaish(row.value) && (
|
|
67
|
+
<code class="rounded bg-muted px-1 py-0.5 text-foreground text-xs">
|
|
68
|
+
{typeof row.value === "string"
|
|
69
|
+
? row.value
|
|
70
|
+
: JSON.stringify(row.value)}
|
|
71
|
+
</code>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
{isSchemaish(row.value) && (
|
|
75
|
+
<div class="mt-2">
|
|
76
|
+
<SchemaTable
|
|
77
|
+
expandAll={expandAll}
|
|
78
|
+
schema={row.value}
|
|
79
|
+
schemas={schemas}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</section>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -15,6 +15,9 @@ const COLORS: Record<string, string> = {
|
|
|
15
15
|
PATCH: "bg-yellow-500/20 text-yellow-800 dark:text-yellow-300",
|
|
16
16
|
POST: "bg-blue-500/15 text-blue-700 dark:text-blue-300",
|
|
17
17
|
PUT: "bg-orange-500/15 text-orange-700 dark:text-orange-300",
|
|
18
|
+
// AsyncAPI actions.
|
|
19
|
+
RECEIVE: "bg-teal-500/15 text-teal-700 dark:text-teal-300",
|
|
20
|
+
SEND: "bg-violet-500/15 text-violet-700 dark:text-violet-300",
|
|
18
21
|
};
|
|
19
22
|
const color = COLORS[upper] ?? "bg-muted text-muted-foreground";
|
|
20
23
|
---
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type SecuritySchemeLike,
|
|
15
15
|
} from "./security.ts";
|
|
16
16
|
import { buildRequestSample, sampleLanguages } from "./snippets.ts";
|
|
17
|
+
import AsyncApiOperation from "./AsyncApiOperation.astro";
|
|
17
18
|
import Authorization from "./Authorization.astro";
|
|
18
19
|
import MethodBadge from "./MethodBadge.astro";
|
|
19
20
|
import ParametersTable from "./ParametersTable.astro";
|
|
@@ -57,6 +58,9 @@ interface FullOperation {
|
|
|
57
58
|
const { source, id } = Astro.props;
|
|
58
59
|
const spec = specs[source];
|
|
59
60
|
const ref = spec?.operations[id];
|
|
61
|
+
// The AsyncAPI front-end renders its own body; the lookups below are
|
|
62
|
+
// OpenAPI-shaped (paths, request/response) and resolve to nothing for it.
|
|
63
|
+
const isAsyncApi = spec?.kind === "asyncapi";
|
|
60
64
|
|
|
61
65
|
const doc = (spec?.document ?? {}) as {
|
|
62
66
|
paths?: Record<
|
|
@@ -113,9 +117,10 @@ const sample =
|
|
|
113
117
|
: null;
|
|
114
118
|
const languages = sampleLanguages(spec?.codeSamples ?? []);
|
|
115
119
|
---
|
|
116
|
-
|
|
117
120
|
{
|
|
118
|
-
|
|
121
|
+
isAsyncApi ? (
|
|
122
|
+
<AsyncApiOperation id={id} source={source} />
|
|
123
|
+
) : !(spec && ref && operation) ? (
|
|
119
124
|
<div class="text-muted-foreground">This API operation could not be found.</div>
|
|
120
125
|
) : (
|
|
121
126
|
<div class="not-prose">
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The tabbed code-panel chrome shared by the request/response rail (OpenAPI)
|
|
4
|
+
* and the sample/message rail (AsyncAPI): a heading, a tab per panel, an
|
|
5
|
+
* optional copy button, and one visible panel at a time. Panels carry either
|
|
6
|
+
* highlighted `html` or plain fallback `text`. Behavior comes from the
|
|
7
|
+
* `<blume-panel-tabs>` custom element in `panel.ts`.
|
|
8
|
+
*/
|
|
9
|
+
interface Panel {
|
|
10
|
+
key: string;
|
|
11
|
+
label: string;
|
|
12
|
+
/** Highlighted code HTML; null/undefined renders the `text` fallback. */
|
|
13
|
+
html?: string | null;
|
|
14
|
+
/** Plain-text fallback when there is nothing to highlight. */
|
|
15
|
+
text?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
heading: string;
|
|
20
|
+
panels: Panel[];
|
|
21
|
+
/** Show a copy button that copies the active panel's text. */
|
|
22
|
+
copy?: boolean;
|
|
23
|
+
/** Render tab labels in a monospace face (status codes). */
|
|
24
|
+
mono?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { heading, panels, copy = false, mono = false } = Astro.props;
|
|
28
|
+
|
|
29
|
+
// A stable per-instance id prefix so tab/tabpanel pairs can reference each
|
|
30
|
+
// other without colliding across the multiple PanelTabs on one page (the
|
|
31
|
+
// heading is unique per rail group; panel keys are unique within a group).
|
|
32
|
+
const NON_ID = /[^a-z0-9]+/gu;
|
|
33
|
+
const idFor = (kind: "tab" | "panel", key: string): string =>
|
|
34
|
+
["panel-tabs", heading, kind, key]
|
|
35
|
+
.map((part) => part.toLowerCase().replace(NON_ID, "-"))
|
|
36
|
+
.join("-");
|
|
37
|
+
|
|
38
|
+
// A `.prose` wrapper gives Shiki its scoped token colors; the global style at
|
|
39
|
+
// the foot of this file strips the standalone code block's own box (border,
|
|
40
|
+
// injected copy button, language label) so the code sits flush inside the one
|
|
41
|
+
// panel border.
|
|
42
|
+
const CODE_WRAP = "prose max-w-none text-xs";
|
|
43
|
+
const TAB_CLASS =
|
|
44
|
+
"-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";
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
panels.length > 0 && (
|
|
49
|
+
<div>
|
|
50
|
+
<div
|
|
51
|
+
aria-level="3"
|
|
52
|
+
class="mb-2 font-semibold text-foreground text-sm"
|
|
53
|
+
role="heading"
|
|
54
|
+
>
|
|
55
|
+
{heading}
|
|
56
|
+
</div>
|
|
57
|
+
<blume-panel-tabs class="block overflow-hidden rounded-blume border border-border bg-background">
|
|
58
|
+
<div class="flex items-center justify-between gap-2 border-border border-b px-3">
|
|
59
|
+
<div class="flex flex-wrap gap-4" role="tablist">
|
|
60
|
+
{panels.map((panel, index) => (
|
|
61
|
+
<button
|
|
62
|
+
aria-controls={idFor("panel", panel.key)}
|
|
63
|
+
aria-selected={index === 0 ? "true" : "false"}
|
|
64
|
+
class:list={[TAB_CLASS, mono && "font-mono"]}
|
|
65
|
+
data-panel-tab={panel.key}
|
|
66
|
+
id={idFor("tab", panel.key)}
|
|
67
|
+
role="tab"
|
|
68
|
+
type="button"
|
|
69
|
+
>
|
|
70
|
+
{panel.label}
|
|
71
|
+
</button>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
{copy && (
|
|
75
|
+
<button
|
|
76
|
+
aria-label={`Copy ${heading.toLowerCase()}`}
|
|
77
|
+
class="group shrink-0 cursor-pointer rounded px-1.5 py-1 text-muted-foreground text-xs hover:text-foreground"
|
|
78
|
+
data-panel-copy
|
|
79
|
+
type="button"
|
|
80
|
+
>
|
|
81
|
+
<span class="group-data-[copied]:hidden">Copy</span>
|
|
82
|
+
<span class="hidden group-data-[copied]:inline">Copied</span>
|
|
83
|
+
</button>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
{panels.map((panel, index) => (
|
|
87
|
+
<div
|
|
88
|
+
aria-labelledby={idFor("tab", panel.key)}
|
|
89
|
+
class:list={[index === 0 ? "" : "hidden"]}
|
|
90
|
+
data-panel={panel.key}
|
|
91
|
+
id={idFor("panel", panel.key)}
|
|
92
|
+
role="tabpanel"
|
|
93
|
+
>
|
|
94
|
+
{panel.html ? (
|
|
95
|
+
<div class={CODE_WRAP}>
|
|
96
|
+
<Fragment set:html={panel.html} />
|
|
97
|
+
</div>
|
|
98
|
+
) : (
|
|
99
|
+
<div class="px-3 py-4 text-muted-foreground text-xs">
|
|
100
|
+
{panel.text ?? ""}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
))}
|
|
105
|
+
</blume-panel-tabs>
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
<script>
|
|
111
|
+
import "./panel.ts";
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<style is:global>
|
|
115
|
+
/* Strip the standalone code block's own chrome inside a panel: the border,
|
|
116
|
+
margin, radius, and the copy button + language label the prose code theme
|
|
117
|
+
adds — the panel supplies a single border and its own copy button. */
|
|
118
|
+
blume-panel-tabs pre.astro-code {
|
|
119
|
+
margin: 0 !important;
|
|
120
|
+
border: 0 !important;
|
|
121
|
+
border-radius: 0 !important;
|
|
122
|
+
background: transparent !important;
|
|
123
|
+
padding: 0.75rem 1rem !important;
|
|
124
|
+
}
|
|
125
|
+
blume-panel-tabs pre.astro-code::before {
|
|
126
|
+
content: none !important;
|
|
127
|
+
}
|
|
128
|
+
blume-panel-tabs [data-blume-copy] {
|
|
129
|
+
display: none !important;
|
|
130
|
+
}
|
|
131
|
+
</style>
|
|
@@ -27,6 +27,8 @@ const SECTIONS: { in: string; title: string }[] = [
|
|
|
27
27
|
{ in: "query", title: "Query parameters" },
|
|
28
28
|
{ in: "header", title: "Header parameters" },
|
|
29
29
|
{ in: "cookie", title: "Cookie parameters" },
|
|
30
|
+
// AsyncAPI channel address parameters.
|
|
31
|
+
{ in: "channel", title: "Channel parameters" },
|
|
30
32
|
];
|
|
31
33
|
|
|
32
34
|
const groups = SECTIONS.map((section) => ({
|
|
@@ -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
|
-
|
|
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
|
-
|
|
34
|
+
key: language.id,
|
|
43
35
|
label: language.label,
|
|
44
36
|
}))
|
|
45
37
|
);
|
|
46
38
|
|
|
47
|
-
const
|
|
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 {
|
|
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
|
-
|
|
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
|
+
};
|