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.
- package/CHANGELOG.md +17 -0
- package/dist/cli/index.js +1621 -576
- package/dist/cli/index.js.map +109 -104
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/config-input.d.ts +79 -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 +112 -15
- package/dist/types/core/sources/types.d.ts +3 -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 +6 -5
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- 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/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +3 -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 +37 -10
- package/src/ai/ask-context.ts +5 -1
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +40 -16
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +28 -11
- package/src/ai/mcp/server.ts +183 -38
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +6 -2
- package/src/astro/generate.ts +54 -29
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +16 -9
- package/src/astro/templates.ts +152 -33
- package/src/audit/agent.ts +2 -2
- 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 +37 -9
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +5 -2
- package/src/audit/snapshot.ts +2 -4
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/build.ts +15 -9
- package/src/cli/commands/dev.ts +2 -0
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eval.ts +7 -3
- 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/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +22 -15
- package/src/cli/internal-error.ts +1 -0
- 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/islands/ask-ai.tsx +33 -25
- 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/RootLayout.astro +18 -0
- package/src/components/layout/Search.astro +77 -13
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- 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 +2 -0
- package/src/components/layout/search/types.ts +13 -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/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +88 -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 +33 -7
- 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/project-graph.ts +15 -3
- package/src/core/schema.ts +213 -67
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/github-releases.ts +39 -31
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +67 -20
- package/src/core/sources/notion.ts +49 -17
- package/src/core/sources/portable-text.ts +32 -11
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +4 -0
- 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 +25 -10
- package/src/deploy/sitemap.ts +33 -1
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/report.ts +4 -4
- 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 +6 -8
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +30 -11
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +126 -57
- package/src/openapi/parse.ts +97 -5
- package/src/openapi/references.ts +12 -10
- package/src/openapi/render-mdx.ts +73 -34
- package/src/openapi/scalar.ts +6 -8
- package/src/openapi/source.ts +98 -28
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +25 -5
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +66 -20
- 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 +22 -14
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +9 -5
- package/src/translate/run.ts +10 -4
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import specs from "blume:openapi";
|
|
3
3
|
|
|
4
|
+
import type { AsyncApiDocument } from "../../openapi/asyncapi.ts";
|
|
5
|
+
|
|
4
6
|
// The spec-level metadata block (version + base URLs) at the top of an API
|
|
5
7
|
// overview page. The tag sections that follow are emitted by `overviewMdx` as
|
|
6
8
|
// markdown headings plus `<ApiTagOperations>` lists, so they land in the
|
|
@@ -11,8 +13,32 @@ interface Props {
|
|
|
11
13
|
|
|
12
14
|
const { source } = Astro.props;
|
|
13
15
|
const spec = specs[source];
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
// OpenAPI declares `servers` as an array of URLs; AsyncAPI as a named map of
|
|
18
|
+
// host/protocol/pathname. Both flatten into one list of address chips.
|
|
19
|
+
const addresses: string[] = [];
|
|
20
|
+
if (spec?.kind === "asyncapi") {
|
|
21
|
+
const servers = (spec.document as AsyncApiDocument).servers ?? {};
|
|
22
|
+
for (const server of Object.values(servers)) {
|
|
23
|
+
if (server?.host) {
|
|
24
|
+
addresses.push(
|
|
25
|
+
`${server.protocol ? `${server.protocol}://` : ""}${server.host}${server.pathname ?? ""}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
// Hand-written specs sometimes declare `servers` as a bare object; degrade
|
|
31
|
+
// to no address chips instead of throwing mid-build.
|
|
32
|
+
const declared = ((spec?.document ?? {}) as { servers?: { url?: string }[] })
|
|
33
|
+
.servers;
|
|
34
|
+
const servers = Array.isArray(declared) ? declared : [];
|
|
35
|
+
for (const server of servers) {
|
|
36
|
+
if (server.url) {
|
|
37
|
+
addresses.push(server.url);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const addressLabel = spec?.kind === "asyncapi" ? "Servers" : "Base URL";
|
|
16
42
|
---
|
|
17
43
|
|
|
18
44
|
{
|
|
@@ -23,12 +49,12 @@ const servers =
|
|
|
23
49
|
Version {spec.version}
|
|
24
50
|
</div>
|
|
25
51
|
)}
|
|
26
|
-
{
|
|
52
|
+
{addresses.length > 0 && (
|
|
27
53
|
<div class="not-prose mb-8 flex flex-wrap items-center gap-2">
|
|
28
|
-
<span class="text-muted-foreground text-xs">
|
|
29
|
-
{
|
|
54
|
+
<span class="text-muted-foreground text-xs">{addressLabel}</span>
|
|
55
|
+
{addresses.map((address) => (
|
|
30
56
|
<code class="rounded bg-muted px-2 py-0.5 text-foreground text-xs">
|
|
31
|
-
{
|
|
57
|
+
{address}
|
|
32
58
|
</code>
|
|
33
59
|
))}
|
|
34
60
|
</div>
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
import data from "blume:data";
|
|
3
|
+
import specs from "blume:openapi";
|
|
4
|
+
|
|
5
|
+
import type { AsyncApiDocument } from "../../openapi/asyncapi.ts";
|
|
6
|
+
import { asyncApiOperationObject } from "../../openapi/asyncapi.ts";
|
|
7
|
+
import { highlightCode } from "../../markdown/index.ts";
|
|
8
|
+
import {
|
|
9
|
+
asyncApiSecurityEntries,
|
|
10
|
+
bindingGroups,
|
|
11
|
+
channelParameters,
|
|
12
|
+
channelServers,
|
|
13
|
+
messageLabel,
|
|
14
|
+
operationMessages,
|
|
15
|
+
payloadSchema,
|
|
16
|
+
protocolOf,
|
|
17
|
+
schemaOf,
|
|
18
|
+
} from "./async.ts";
|
|
19
|
+
import type { MessageSample } from "./async-snippets.ts";
|
|
20
|
+
import { asyncSampleLanguages } from "./async-snippets.ts";
|
|
21
|
+
import Authorization from "./Authorization.astro";
|
|
22
|
+
import Bindings from "./Bindings.astro";
|
|
23
|
+
import { exampleValue, type SchemaLike, toJson } from "./helpers.ts";
|
|
24
|
+
import MethodBadge from "./MethodBadge.astro";
|
|
25
|
+
import PanelTabs from "./PanelTabs.astro";
|
|
26
|
+
import ParametersTable from "./ParametersTable.astro";
|
|
27
|
+
import SchemaTable from "./SchemaTable.astro";
|
|
28
|
+
import type { SecuritySchemeLike } from "./security.ts";
|
|
29
|
+
import { resolveAsyncApiSecurity } from "./security.ts";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The AsyncAPI front-end of the operation page: message payloads instead of
|
|
33
|
+
* request/response, channel parameters instead of path/query, protocol
|
|
34
|
+
* bindings, and binding-aware samples in the right rail. Everything below the
|
|
35
|
+
* dispatch — schema tables, parameter rows, authorization, the tabbed panels —
|
|
36
|
+
* is the same component set the OpenAPI operation renders.
|
|
37
|
+
*/
|
|
38
|
+
interface Props {
|
|
39
|
+
source: string;
|
|
40
|
+
id: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { source, id } = Astro.props;
|
|
44
|
+
const spec = specs[source];
|
|
45
|
+
const ref = spec?.operations[id];
|
|
46
|
+
const document = (spec?.document ?? {}) as AsyncApiDocument;
|
|
47
|
+
const operation = ref ? asyncApiOperationObject(document, ref) : undefined;
|
|
48
|
+
const channel = ref?.channelId
|
|
49
|
+
? document.channels?.[ref.channelId]
|
|
50
|
+
: undefined;
|
|
51
|
+
|
|
52
|
+
const schemas = (document.components?.schemas ?? {}) as Record<
|
|
53
|
+
string,
|
|
54
|
+
SchemaLike
|
|
55
|
+
>;
|
|
56
|
+
const servers = channelServers(channel, document);
|
|
57
|
+
const security = resolveAsyncApiSecurity(
|
|
58
|
+
asyncApiSecurityEntries(operation, servers),
|
|
59
|
+
document.components?.securitySchemes as
|
|
60
|
+
| Record<string, SecuritySchemeLike>
|
|
61
|
+
| undefined
|
|
62
|
+
);
|
|
63
|
+
const parameters = channelParameters(channel, document);
|
|
64
|
+
const messages = operation ? operationMessages(operation, channel, document) : [];
|
|
65
|
+
const protocol = protocolOf(operation, channel, servers);
|
|
66
|
+
|
|
67
|
+
/** Declared example first, else a sampled value from the payload schema. */
|
|
68
|
+
const exampleOf = (message: (typeof messages)[number]["message"]): unknown =>
|
|
69
|
+
message.examples?.[0]?.payload ?? exampleValue(payloadSchema(message), schemas);
|
|
70
|
+
|
|
71
|
+
const messagePanels = await Promise.all(
|
|
72
|
+
messages.map(async (named, index) => {
|
|
73
|
+
const example = exampleOf(named.message);
|
|
74
|
+
return {
|
|
75
|
+
html:
|
|
76
|
+
example === undefined || example === null
|
|
77
|
+
? null
|
|
78
|
+
: await highlightCode(toJson(example), "json", {
|
|
79
|
+
icons: false,
|
|
80
|
+
themes: data.config.codeThemes,
|
|
81
|
+
}),
|
|
82
|
+
key: `message-${index}`,
|
|
83
|
+
label: messageLabel(named),
|
|
84
|
+
text: named.message.summary || "No example payload.",
|
|
85
|
+
};
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const sample: MessageSample | null = ref
|
|
90
|
+
? {
|
|
91
|
+
action: ref.method === "send" ? "send" : "receive",
|
|
92
|
+
address: ref.path,
|
|
93
|
+
payload: messages.length > 0 ? exampleOf(messages[0].message) : undefined,
|
|
94
|
+
server: servers[0],
|
|
95
|
+
}
|
|
96
|
+
: null;
|
|
97
|
+
const languages = asyncSampleLanguages(spec?.codeSamples ?? [], protocol);
|
|
98
|
+
const samplePanels = sample
|
|
99
|
+
? await Promise.all(
|
|
100
|
+
languages.map(async (language) => ({
|
|
101
|
+
html: await highlightCode(language.build(sample), language.lang, {
|
|
102
|
+
icons: false,
|
|
103
|
+
themes: data.config.codeThemes,
|
|
104
|
+
}),
|
|
105
|
+
key: language.id,
|
|
106
|
+
label: language.label,
|
|
107
|
+
}))
|
|
108
|
+
)
|
|
109
|
+
: [];
|
|
110
|
+
|
|
111
|
+
const operationBindings = bindingGroups(operation?.bindings);
|
|
112
|
+
const channelBindings = bindingGroups(channel?.bindings);
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
!(spec && ref && operation) ? (
|
|
117
|
+
<div class="text-muted-foreground">This API operation could not be found.</div>
|
|
118
|
+
) : (
|
|
119
|
+
<div class="not-prose">
|
|
120
|
+
<div class="mb-6 flex flex-wrap items-center gap-3">
|
|
121
|
+
<MethodBadge method={ref.method} />
|
|
122
|
+
<code class="break-all font-mono text-foreground text-sm">
|
|
123
|
+
{ref.path}
|
|
124
|
+
</code>
|
|
125
|
+
{ref.deprecated && (
|
|
126
|
+
<span class="font-medium text-[0.625rem] text-orange-600 uppercase tracking-wide dark:text-orange-400">
|
|
127
|
+
deprecated
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
<div class="grid grid-cols-1 items-start gap-x-10 gap-y-8 xl:grid-cols-[minmax(0,1fr)_minmax(0,28rem)]">
|
|
132
|
+
<div>
|
|
133
|
+
<Authorization security={security} />
|
|
134
|
+
<ParametersTable parameters={parameters} schemas={schemas} />
|
|
135
|
+
{messages.map((named) => {
|
|
136
|
+
const payload = payloadSchema(named.message);
|
|
137
|
+
// Headers may be multi-format too; undefined with `headers`
|
|
138
|
+
// present means an inline-unrenderable schema format.
|
|
139
|
+
const headers = schemaOf(named.message.headers);
|
|
140
|
+
const messageBindings = bindingGroups(named.message.bindings);
|
|
141
|
+
return (
|
|
142
|
+
<section class="mt-6">
|
|
143
|
+
<div
|
|
144
|
+
aria-level="2"
|
|
145
|
+
class="mb-2 font-semibold text-foreground text-sm"
|
|
146
|
+
role="heading"
|
|
147
|
+
>
|
|
148
|
+
{messages.length > 1
|
|
149
|
+
? `Message: ${messageLabel(named)}`
|
|
150
|
+
: "Message"}
|
|
151
|
+
</div>
|
|
152
|
+
{named.message.description && (
|
|
153
|
+
<p class="mb-2 text-muted-foreground text-sm">
|
|
154
|
+
{named.message.description}
|
|
155
|
+
</p>
|
|
156
|
+
)}
|
|
157
|
+
{named.message.contentType && (
|
|
158
|
+
<div class="mb-2 text-muted-foreground text-xs">
|
|
159
|
+
<code class="rounded bg-muted px-1 py-0.5">
|
|
160
|
+
{named.message.contentType}
|
|
161
|
+
</code>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
{payload && (
|
|
165
|
+
<div class="not-prose rounded-blume border border-border px-4 py-3">
|
|
166
|
+
<SchemaTable
|
|
167
|
+
expandAll={spec.expandSchemas}
|
|
168
|
+
schema={payload}
|
|
169
|
+
schemas={schemas}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
{/* Payload is optional on a message — only a payload that
|
|
174
|
+
failed to unwrap earns the can't-render note. */}
|
|
175
|
+
{named.message.payload !== undefined && !payload && (
|
|
176
|
+
<p class="text-muted-foreground text-sm">
|
|
177
|
+
The payload uses a schema format this reference can't
|
|
178
|
+
render inline.
|
|
179
|
+
</p>
|
|
180
|
+
)}
|
|
181
|
+
{named.message.headers && (
|
|
182
|
+
<div class="mt-3">
|
|
183
|
+
<div class="mb-1 font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
|
184
|
+
Headers
|
|
185
|
+
</div>
|
|
186
|
+
{headers ? (
|
|
187
|
+
<div class="not-prose rounded-blume border border-border px-4 py-3">
|
|
188
|
+
<SchemaTable
|
|
189
|
+
expandAll={spec.expandSchemas}
|
|
190
|
+
schema={headers}
|
|
191
|
+
schemas={schemas}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
) : (
|
|
195
|
+
<p class="text-muted-foreground text-sm">
|
|
196
|
+
The headers use a schema format this reference can't
|
|
197
|
+
render inline.
|
|
198
|
+
</p>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
{messageBindings.length > 0 && (
|
|
203
|
+
<Bindings
|
|
204
|
+
expandAll={spec.expandSchemas}
|
|
205
|
+
groups={messageBindings}
|
|
206
|
+
schemas={schemas}
|
|
207
|
+
title="Message bindings"
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
</section>
|
|
211
|
+
);
|
|
212
|
+
})}
|
|
213
|
+
<Bindings
|
|
214
|
+
expandAll={spec.expandSchemas}
|
|
215
|
+
groups={operationBindings}
|
|
216
|
+
schemas={schemas}
|
|
217
|
+
title="Operation bindings"
|
|
218
|
+
/>
|
|
219
|
+
<Bindings
|
|
220
|
+
expandAll={spec.expandSchemas}
|
|
221
|
+
groups={channelBindings}
|
|
222
|
+
schemas={schemas}
|
|
223
|
+
title="Channel bindings"
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
{(samplePanels.length > 0 || messagePanels.length > 0) && (
|
|
227
|
+
<div class="xl:sticky xl:top-24 xl:self-start">
|
|
228
|
+
<div class="not-prose flex flex-col gap-6">
|
|
229
|
+
<PanelTabs copy heading="Example" panels={samplePanels} />
|
|
230
|
+
<PanelTabs heading="Message" panels={messagePanels} />
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
@@ -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) => ({
|