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,366 @@
|
|
|
1
|
+
import type { ApiOperationRef, ApiTagRef } from "./model.ts";
|
|
2
|
+
import { operationCollector, operationKey } from "./model.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Blume's own AsyncAPI model — the second front-end of the API reference
|
|
6
|
+
* pipeline. Specs are normalized to AsyncAPI 3.x at parse time (see
|
|
7
|
+
* `parseAsyncApiSpec` in `parse.ts`), so this module only handles one shape:
|
|
8
|
+
* top-level `operations` with `action: send | receive` pointing at `channels`.
|
|
9
|
+
* Internal `$ref`s are deliberately left in place, mirroring `model.ts` — the
|
|
10
|
+
* components resolve them lazily, which keeps circular schemas serializable
|
|
11
|
+
* and lets type labels keep their `#/components/schemas/<name>` names.
|
|
12
|
+
* Browser-safe: no Node imports (the components import from here).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The two AsyncAPI 3.x operation actions, from the application's perspective. */
|
|
16
|
+
export const ASYNCAPI_ACTIONS = ["send", "receive"] as const;
|
|
17
|
+
|
|
18
|
+
export type AsyncApiAction = (typeof ASYNCAPI_ACTIONS)[number];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A value carried by a parsed spec document: JSON-compatible data whose exact
|
|
22
|
+
* position in the document is not modeled (extension fields, bindings,
|
|
23
|
+
* protocol-specific extras). `undefined` covers absent optional members.
|
|
24
|
+
*/
|
|
25
|
+
export type AsyncApiSpecValue =
|
|
26
|
+
| string
|
|
27
|
+
| number
|
|
28
|
+
| boolean
|
|
29
|
+
| null
|
|
30
|
+
| undefined
|
|
31
|
+
| AsyncApiSpecValue[]
|
|
32
|
+
| { [key: string]: AsyncApiSpecValue };
|
|
33
|
+
|
|
34
|
+
/** A permissive view of an AsyncAPI reference object. */
|
|
35
|
+
export interface AsyncApiRefLike {
|
|
36
|
+
$ref?: string;
|
|
37
|
+
[key: string]: AsyncApiSpecValue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A permissive view of an AsyncAPI 3.x channel — only the fields we render. */
|
|
41
|
+
export interface AsyncApiChannelObject {
|
|
42
|
+
address?: string | null;
|
|
43
|
+
title?: string;
|
|
44
|
+
summary?: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
messages?: Record<string, AsyncApiRefLike>;
|
|
47
|
+
parameters?: Record<string, AsyncApiRefLike>;
|
|
48
|
+
servers?: AsyncApiRefLike[];
|
|
49
|
+
bindings?: Record<string, Record<string, AsyncApiSpecValue>>;
|
|
50
|
+
[key: string]: AsyncApiSpecValue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A permissive view of an AsyncAPI 3.x operation — only the fields we render. */
|
|
54
|
+
export interface AsyncApiOperationObject {
|
|
55
|
+
action?: string;
|
|
56
|
+
channel?: AsyncApiRefLike;
|
|
57
|
+
title?: string;
|
|
58
|
+
summary?: string;
|
|
59
|
+
description?: string;
|
|
60
|
+
deprecated?: boolean;
|
|
61
|
+
tags?: { name?: string; description?: string }[];
|
|
62
|
+
security?: AsyncApiRefLike[];
|
|
63
|
+
messages?: AsyncApiRefLike[];
|
|
64
|
+
bindings?: Record<string, Record<string, AsyncApiSpecValue>>;
|
|
65
|
+
[key: string]: AsyncApiSpecValue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A permissive view of an AsyncAPI 3.x server object. */
|
|
69
|
+
export interface AsyncApiServerObject {
|
|
70
|
+
host?: string;
|
|
71
|
+
protocol?: string;
|
|
72
|
+
pathname?: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
security?: AsyncApiRefLike[];
|
|
75
|
+
[key: string]: AsyncApiSpecValue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A normalized AsyncAPI 3.x document, internal `$ref`s intact. */
|
|
79
|
+
export interface AsyncApiDocument {
|
|
80
|
+
asyncapi?: string;
|
|
81
|
+
info?: {
|
|
82
|
+
title?: string;
|
|
83
|
+
version?: string;
|
|
84
|
+
description?: string;
|
|
85
|
+
tags?: { name?: string; description?: string }[];
|
|
86
|
+
[key: string]: AsyncApiSpecValue;
|
|
87
|
+
};
|
|
88
|
+
defaultContentType?: string;
|
|
89
|
+
servers?: Record<string, AsyncApiServerObject>;
|
|
90
|
+
channels?: Record<string, AsyncApiChannelObject>;
|
|
91
|
+
operations?: Record<string, AsyncApiOperationObject>;
|
|
92
|
+
components?: Record<string, Record<string, AsyncApiSpecValue>>;
|
|
93
|
+
[key: string]: AsyncApiSpecValue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Decode a JSON-pointer token: `user~1signedup` -> `user/signedup`. */
|
|
97
|
+
const unescapePointer = (token: string): string =>
|
|
98
|
+
token.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
99
|
+
|
|
100
|
+
const CHANNEL_REF = /^#\/channels\/(?<id>.+)$/u;
|
|
101
|
+
|
|
102
|
+
const isString = (value: AsyncApiSpecValue): value is string =>
|
|
103
|
+
typeof value === "string";
|
|
104
|
+
|
|
105
|
+
/** The channel id an operation's `channel.$ref` points at, if resolvable. */
|
|
106
|
+
export const channelIdOf = (channel?: AsyncApiRefLike): string | undefined => {
|
|
107
|
+
const id = CHANNEL_REF.exec(channel?.$ref ?? "")?.groups?.id;
|
|
108
|
+
return id === undefined ? undefined : unescapePointer(id);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A channel's display address. AsyncAPI 3.x allows `address: null` (unknown at
|
|
113
|
+
* design time) — fall back to the channel id so the operation still shows
|
|
114
|
+
* where it lives.
|
|
115
|
+
*/
|
|
116
|
+
export const channelAddress = (
|
|
117
|
+
channelId: string,
|
|
118
|
+
channel?: AsyncApiChannelObject
|
|
119
|
+
): string => {
|
|
120
|
+
const address = channel?.address;
|
|
121
|
+
return isString(address) && address !== "" ? address : channelId;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const isObject = (
|
|
125
|
+
value: AsyncApiSpecValue
|
|
126
|
+
): value is Record<string, AsyncApiSpecValue> =>
|
|
127
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
128
|
+
|
|
129
|
+
const COMPONENT_SECTION_REF =
|
|
130
|
+
/^#\/components\/(?<section>channels|operations)\/(?<name>[^/]+)$/u;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Inline top-level `channels`/`operations` entries declared as Reference
|
|
134
|
+
* Objects (`{ $ref: "#/components/channels/…" }` — the spec's reuse pattern)
|
|
135
|
+
* by replacing them with their components target. Runs before trait merging
|
|
136
|
+
* so an inlined operation's traits merge exactly like an inline one's.
|
|
137
|
+
* Unresolvable refs stay in place; the extractor reports them.
|
|
138
|
+
*/
|
|
139
|
+
const inlineComponentRefs = (document: AsyncApiDocument): void => {
|
|
140
|
+
const maps: [Record<string, AsyncApiSpecValue> | undefined, string][] = [
|
|
141
|
+
[document.channels, "channels"],
|
|
142
|
+
[document.operations, "operations"],
|
|
143
|
+
];
|
|
144
|
+
for (const [map, section] of maps) {
|
|
145
|
+
if (!isObject(map)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const table = document.components?.[section];
|
|
149
|
+
for (const [id, entry] of Object.entries(map)) {
|
|
150
|
+
if (!isObject(entry) || !isString(entry.$ref)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const groups = COMPONENT_SECTION_REF.exec(entry.$ref)?.groups;
|
|
154
|
+
if (groups?.section !== section) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const resolved = isObject(table)
|
|
158
|
+
? table[unescapePointer(groups.name ?? "")]
|
|
159
|
+
: undefined;
|
|
160
|
+
if (isObject(resolved) && !isString(resolved.$ref)) {
|
|
161
|
+
map[id] = resolved;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const TRAIT_REF =
|
|
168
|
+
/^#\/components\/(?<section>operationTraits|messageTraits)\/(?<name>[^/]+)$/u;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Merge `traits` into their operation/message objects, in declaration order,
|
|
172
|
+
* with the object's own properties taking precedence (the spec's merge rule;
|
|
173
|
+
* applied shallowly, which covers the fields traits carry in practice —
|
|
174
|
+
* bindings, security, tags, headers). Runs once at parse time so the
|
|
175
|
+
* serialized document and every downstream consumer are trait-free; the
|
|
176
|
+
* `traits` key itself is dropped. Unresolvable trait `$ref`s are skipped.
|
|
177
|
+
*/
|
|
178
|
+
export const applyAsyncApiTraits = (
|
|
179
|
+
document: AsyncApiDocument
|
|
180
|
+
): AsyncApiDocument => {
|
|
181
|
+
const resolveTrait = (
|
|
182
|
+
trait: AsyncApiSpecValue
|
|
183
|
+
): Record<string, AsyncApiSpecValue> | undefined => {
|
|
184
|
+
if (!isObject(trait)) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
if (!isString(trait.$ref)) {
|
|
188
|
+
return trait;
|
|
189
|
+
}
|
|
190
|
+
const groups = TRAIT_REF.exec(trait.$ref)?.groups;
|
|
191
|
+
const section = groups?.section
|
|
192
|
+
? document.components?.[groups.section]
|
|
193
|
+
: undefined;
|
|
194
|
+
const resolved = section?.[groups?.name ?? ""];
|
|
195
|
+
return isObject(resolved) ? resolved : undefined;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const mergeTraits = (node: Record<string, AsyncApiSpecValue>): void => {
|
|
199
|
+
const { traits } = node;
|
|
200
|
+
if (!Array.isArray(traits)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
delete node.traits;
|
|
204
|
+
const merged: Record<string, AsyncApiSpecValue> = {};
|
|
205
|
+
for (const trait of traits) {
|
|
206
|
+
Object.assign(merged, resolveTrait(trait));
|
|
207
|
+
}
|
|
208
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
209
|
+
if (!(key in node)) {
|
|
210
|
+
node[key] = value;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
for (const operation of Object.values(document.operations ?? {})) {
|
|
216
|
+
if (isObject(operation)) {
|
|
217
|
+
mergeTraits(operation);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const channelMaps = [
|
|
221
|
+
...Object.values(document.channels ?? {}),
|
|
222
|
+
// Reusable channels under components carry messages too; their traits
|
|
223
|
+
// must merge the same way so a resolved component channel renders alike.
|
|
224
|
+
...Object.values(document.components?.channels ?? {}),
|
|
225
|
+
];
|
|
226
|
+
const messageMaps = [
|
|
227
|
+
...channelMaps.map((channel) =>
|
|
228
|
+
isObject(channel) ? channel.messages : undefined
|
|
229
|
+
),
|
|
230
|
+
document.components?.messages,
|
|
231
|
+
];
|
|
232
|
+
for (const messages of messageMaps) {
|
|
233
|
+
if (!isObject(messages)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
for (const message of Object.values(messages)) {
|
|
237
|
+
// A channel message that is itself a `$ref` resolves to a components
|
|
238
|
+
// message, which this loop also visits — don't merge through the ref.
|
|
239
|
+
if (isObject(message) && !isString(message.$ref)) {
|
|
240
|
+
mergeTraits(message);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return document;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The parse-time normalization pass: component channel/operation refs inlined,
|
|
249
|
+
* then traits merged. The serialized document and every downstream consumer
|
|
250
|
+
* see plain channel/operation objects, trait-free.
|
|
251
|
+
*/
|
|
252
|
+
export const normalizeAsyncApiDocument = (
|
|
253
|
+
document: AsyncApiDocument
|
|
254
|
+
): AsyncApiDocument => {
|
|
255
|
+
inlineComponentRefs(document);
|
|
256
|
+
return applyAsyncApiTraits(document);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Resolve where one operation renders from: its action and its declared
|
|
261
|
+
* channel. A string return is the warning explaining why the operation can't
|
|
262
|
+
* appear in the reference.
|
|
263
|
+
*/
|
|
264
|
+
const operationSite = (
|
|
265
|
+
id: string,
|
|
266
|
+
operation: AsyncApiOperationObject,
|
|
267
|
+
channels: Record<string, AsyncApiChannelObject>
|
|
268
|
+
): { action: AsyncApiAction; channelId: string; address: string } | string => {
|
|
269
|
+
const action = ASYNCAPI_ACTIONS.find((a) => a === operation.action);
|
|
270
|
+
if (!action) {
|
|
271
|
+
return `Operation "${id}" declares no send/receive action; it is missing from the reference.`;
|
|
272
|
+
}
|
|
273
|
+
const channelId = channelIdOf(operation.channel);
|
|
274
|
+
const channel = channelId === undefined ? undefined : channels[channelId];
|
|
275
|
+
// A channel entry that is still a bare `$ref` survived normalization — its
|
|
276
|
+
// components target doesn't exist — so it renders nothing useful either.
|
|
277
|
+
if (channelId === undefined || !isObject(channel) || isString(channel.$ref)) {
|
|
278
|
+
return `Operation "${id}" references a channel that isn't declared under "channels"; it is missing from the reference.`;
|
|
279
|
+
}
|
|
280
|
+
return { action, address: channelAddress(channelId, channel), channelId };
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/** The route-mapped operations, ordered tags, and skip warnings of one document. */
|
|
284
|
+
export interface AsyncApiOperationCatalog {
|
|
285
|
+
operations: ApiOperationRef[];
|
|
286
|
+
tags: ApiTagRef[];
|
|
287
|
+
warnings: string[];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Flatten a normalized AsyncAPI 3.x document into a route-mapped operation
|
|
292
|
+
* list and its ordered tags — the AsyncAPI counterpart of `extractOperations`
|
|
293
|
+
* in `model.ts`, built on the same `operationCollector`. Operations group by
|
|
294
|
+
* their first tag; untagged operations fall back to their channel address, so
|
|
295
|
+
* a spec with no tags still gets one sidebar group per channel. Keys come from
|
|
296
|
+
* the operation id (the `operations` map key), which the official 2.x
|
|
297
|
+
* converter synthesizes deterministically (`<channel>.publish` /
|
|
298
|
+
* `<channel>.subscribe`) — so a 2.x spec and its converter-upgraded 3.x form
|
|
299
|
+
* yield identical URLs. An id that slugifies to nothing falls back to
|
|
300
|
+
* action + address, the same scheme `operationKey` uses for method + path.
|
|
301
|
+
*/
|
|
302
|
+
export const extractAsyncApiOperations = (
|
|
303
|
+
document: AsyncApiDocument,
|
|
304
|
+
baseRoute: string
|
|
305
|
+
): AsyncApiOperationCatalog => {
|
|
306
|
+
const warnings: string[] = [];
|
|
307
|
+
const tagMeta = new Map(
|
|
308
|
+
(document.info?.tags ?? [])
|
|
309
|
+
.filter(
|
|
310
|
+
(tag): tag is { name: string; description?: string } =>
|
|
311
|
+
typeof tag?.name === "string"
|
|
312
|
+
)
|
|
313
|
+
.map((tag): [string, string] => [tag.name, tag.description ?? ""])
|
|
314
|
+
);
|
|
315
|
+
const collector = operationCollector(baseRoute, tagMeta);
|
|
316
|
+
const channels = document.channels ?? {};
|
|
317
|
+
|
|
318
|
+
for (const [id, operation] of Object.entries(document.operations ?? {})) {
|
|
319
|
+
if (!isObject(operation)) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
// An operation entry still carrying a `$ref` is one normalization could
|
|
323
|
+
// not inline; name the real problem instead of the missing-action one.
|
|
324
|
+
if (isString(operation.$ref)) {
|
|
325
|
+
warnings.push(
|
|
326
|
+
`Operation "${id}" is a reference that doesn't resolve to a components operation; it is missing from the reference.`
|
|
327
|
+
);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const site = operationSite(id, operation, channels);
|
|
331
|
+
if (isString(site)) {
|
|
332
|
+
warnings.push(site);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const { action, address, channelId } = site;
|
|
336
|
+
const tag = operation.tags?.find(
|
|
337
|
+
(candidate): candidate is { name: string; description?: string } =>
|
|
338
|
+
typeof candidate?.name === "string"
|
|
339
|
+
)?.name;
|
|
340
|
+
collector.add({
|
|
341
|
+
channelId,
|
|
342
|
+
deprecated: operation.deprecated === true,
|
|
343
|
+
description: operation.description ?? "",
|
|
344
|
+
key: operationKey(action, address, id),
|
|
345
|
+
method: action,
|
|
346
|
+
operationId: id,
|
|
347
|
+
path: address,
|
|
348
|
+
summary: operation.title ?? operation.summary ?? "",
|
|
349
|
+
tag: tag ?? address,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { ...collector.finish(), warnings };
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/** Resolve the operation object for a ref out of its (AsyncAPI) document. */
|
|
357
|
+
export const asyncApiOperationObject = (
|
|
358
|
+
document: AsyncApiDocument,
|
|
359
|
+
ref: ApiOperationRef
|
|
360
|
+
): AsyncApiOperationObject | undefined => {
|
|
361
|
+
const operation =
|
|
362
|
+
ref.operationId === undefined
|
|
363
|
+
? undefined
|
|
364
|
+
: document.operations?.[ref.operationId];
|
|
365
|
+
return isObject(operation) ? operation : undefined;
|
|
366
|
+
};
|
package/src/openapi/model.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "
|
|
1
|
+
import type { Document, OperationObject } from "@scalar/openapi-types/3.1";
|
|
2
|
+
|
|
3
|
+
import type { AsyncApiAction, AsyncApiDocument } from "./asyncapi.ts";
|
|
4
|
+
import type { ReferenceKind } from "./references.ts";
|
|
5
|
+
import { slugify } from "./references.ts";
|
|
6
|
+
|
|
7
|
+
// The slug rules live with the reference resolver so operation routes and
|
|
8
|
+
// reference-source tokens can never drift apart; re-exported for existing
|
|
9
|
+
// importers.
|
|
10
|
+
export { slugify } from "./references.ts";
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
|
-
* Blume's own
|
|
9
|
-
*
|
|
13
|
+
* Blume's own API reference model, shared by both spec kinds. OpenAPI specs
|
|
14
|
+
* are parsed and upgraded to 3.1, AsyncAPI specs normalized to 3.x (see
|
|
15
|
+
* `parse.ts`), with internal `$ref`s left intact — the document stays
|
|
10
16
|
* JSON-serializable (a fully dereferenced graph can be circular), and the schema
|
|
11
17
|
* components resolve refs against `document.components.schemas` at render time.
|
|
12
18
|
* Each operation is flattened into an {@link ApiOperationRef} with a real,
|
|
@@ -16,13 +22,6 @@ import type {
|
|
|
16
22
|
/** A normalized OpenAPI 3.1 document, internal `$ref`s intact. */
|
|
17
23
|
export type ApiDocument = Document;
|
|
18
24
|
|
|
19
|
-
const NON_SLUG = /[^a-z0-9]+/gu;
|
|
20
|
-
const SLUG_EDGES = /^-+|-+$/gu;
|
|
21
|
-
|
|
22
|
-
/** Lowercase, URL-safe slug: `Add a Pet!` -> `add-a-pet`. */
|
|
23
|
-
export const slugify = (text: string): string =>
|
|
24
|
-
text.toLowerCase().replace(NON_SLUG, "-").replace(SLUG_EDGES, "");
|
|
25
|
-
|
|
26
25
|
/** The HTTP methods an OpenAPI path item may declare, in spec order. */
|
|
27
26
|
export const HTTP_METHODS = [
|
|
28
27
|
"get",
|
|
@@ -50,22 +49,26 @@ export const operationKey = (
|
|
|
50
49
|
return fromId || slugify(`${method}-${path}`);
|
|
51
50
|
};
|
|
52
51
|
|
|
53
|
-
/** One operation, flattened out of
|
|
52
|
+
/** One operation, flattened out of its document and mapped to a route. */
|
|
54
53
|
export interface ApiOperationRef {
|
|
55
54
|
/** Stable key, unique within a spec; matches the MDX `<Operation id>`. */
|
|
56
55
|
key: string;
|
|
57
|
-
method
|
|
58
|
-
|
|
56
|
+
/** HTTP method (OpenAPI) or `send`/`receive` action (AsyncAPI). */
|
|
57
|
+
method: HttpMethod | AsyncApiAction;
|
|
58
|
+
/** Templated path, e.g. `/pets/{id}` — or the channel address (AsyncAPI). */
|
|
59
59
|
path: string;
|
|
60
60
|
/** Full site route for this operation's page, e.g. `/reference/pet/add-pet`. */
|
|
61
61
|
route: string;
|
|
62
|
-
/** Display tag name (first tag
|
|
62
|
+
/** Display tag name (first tag; `Operations` or the channel address when untagged). */
|
|
63
63
|
tag: string;
|
|
64
64
|
tagSlug: string;
|
|
65
65
|
summary: string;
|
|
66
66
|
description: string;
|
|
67
|
+
/** The `operationId` (OpenAPI) or the `operations` map key (AsyncAPI). */
|
|
67
68
|
operationId?: string;
|
|
68
69
|
deprecated: boolean;
|
|
70
|
+
/** The channel the operation acts on (AsyncAPI only). */
|
|
71
|
+
channelId?: string;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
/** A tag/section, in first-seen order. */
|
|
@@ -77,6 +80,8 @@ export interface ApiTagRef {
|
|
|
77
80
|
|
|
78
81
|
/** Everything the runtime needs for one spec, serialized into `blume:openapi`. */
|
|
79
82
|
export interface ApiSpecData {
|
|
83
|
+
/** Which front-end parsed the spec (and which components render it). */
|
|
84
|
+
kind: ReferenceKind;
|
|
80
85
|
/** Unique token used as the `<Operation source>` and the data-module key. */
|
|
81
86
|
slug: string;
|
|
82
87
|
/** Base route the spec's operations hang off, e.g. `/reference`. */
|
|
@@ -85,7 +90,7 @@ export interface ApiSpecData {
|
|
|
85
90
|
title: string;
|
|
86
91
|
version: string;
|
|
87
92
|
description: string;
|
|
88
|
-
document: ApiDocument;
|
|
93
|
+
document: ApiDocument | AsyncApiDocument;
|
|
89
94
|
/** Operations keyed by {@link ApiOperationRef.key}. */
|
|
90
95
|
operations: Record<string, ApiOperationRef>;
|
|
91
96
|
tags: ApiTagRef[];
|
|
@@ -98,17 +103,27 @@ export interface ApiSpecData {
|
|
|
98
103
|
/** The generated `blume:openapi` module: specs keyed by {@link ApiSpecData.slug}. */
|
|
99
104
|
export type OpenApiData = Record<string, ApiSpecData>;
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
// The runtime object check stands guard because the document was parsed from
|
|
107
|
+
// arbitrary YAML/JSON: a spec can put a scalar where the type promises an
|
|
108
|
+
// operation object.
|
|
109
|
+
const isOperation = (
|
|
110
|
+
value: OperationObject | undefined
|
|
111
|
+
): value is OperationObject => typeof value === "object" && value !== null;
|
|
112
|
+
|
|
113
|
+
/** A declared tag whose `name` really is a string at runtime, type aside. */
|
|
114
|
+
const hasTagName = (tag: SpecTag): tag is SpecTag =>
|
|
115
|
+
typeof tag.name === "string";
|
|
103
116
|
|
|
104
117
|
/**
|
|
105
118
|
* Assign each distinct tag name a unique slug. `slugify` can collapse
|
|
106
|
-
* different names onto one value — any two
|
|
107
|
-
*
|
|
119
|
+
* different names onto one value — any two punctuation-only tags (`!!!`,
|
|
120
|
+
* `???`) both fall through to the `operations` fallback — and a shared slug
|
|
108
121
|
* silently merges the tags' routes, sidebar groups, and overview sections.
|
|
109
|
-
* Collisions gain `-2`, `-3`, … in first-seen order.
|
|
122
|
+
* Collisions gain `-2`, `-3`, … in first-seen order. Shared with the AsyncAPI
|
|
123
|
+
* extractor (`asyncapi.ts`), whose untagged fallback groups are channel
|
|
124
|
+
* addresses.
|
|
110
125
|
*/
|
|
111
|
-
const tagSlugger = (): ((name: string) => string) => {
|
|
126
|
+
export const tagSlugger = (): ((name: string) => string) => {
|
|
112
127
|
const assigned = new Map<string, string>();
|
|
113
128
|
const taken = new Set<string>();
|
|
114
129
|
return (name) => {
|
|
@@ -127,6 +142,79 @@ const tagSlugger = (): ((name: string) => string) => {
|
|
|
127
142
|
};
|
|
128
143
|
};
|
|
129
144
|
|
|
145
|
+
/** An operation before the collector assigns its unique key and route. */
|
|
146
|
+
type CollectedOperation = Omit<ApiOperationRef, "route" | "tagSlug">;
|
|
147
|
+
|
|
148
|
+
/** A document's declared tag entry (`tags[n]`). */
|
|
149
|
+
type SpecTag = NonNullable<ApiDocument["tags"]>[number];
|
|
150
|
+
|
|
151
|
+
/** The flattened output both extractors produce. */
|
|
152
|
+
export interface CollectedOperations {
|
|
153
|
+
operations: ApiOperationRef[];
|
|
154
|
+
tags: ApiTagRef[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The collector handle: feed operations in, read the flattened output out. */
|
|
158
|
+
export interface OperationCollector {
|
|
159
|
+
add: (entry: CollectedOperation) => void;
|
|
160
|
+
finish: () => CollectedOperations;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** {@link CollectedOperations} plus anything the extractor had to skip. */
|
|
164
|
+
export interface ExtractedOperations extends CollectedOperations {
|
|
165
|
+
warnings: string[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The collector behind both extractors (OpenAPI here, AsyncAPI in
|
|
170
|
+
* `asyncapi.ts`): first-seen tag ordering, key de-duplication (a repeated key
|
|
171
|
+
* gains its method/action as a suffix), and the shared route template — so
|
|
172
|
+
* URL shape and slug rules can never drift between the two spec kinds.
|
|
173
|
+
*/
|
|
174
|
+
export const operationCollector = (
|
|
175
|
+
baseRoute: string,
|
|
176
|
+
tagMeta: ReadonlyMap<string, string>
|
|
177
|
+
): OperationCollector => {
|
|
178
|
+
const operations: ApiOperationRef[] = [];
|
|
179
|
+
const tagOrder: string[] = [];
|
|
180
|
+
const tagsSeen = new Set<string>();
|
|
181
|
+
const seen = new Set<string>();
|
|
182
|
+
const slugForTag = tagSlugger();
|
|
183
|
+
|
|
184
|
+
const add = (entry: CollectedOperation): void => {
|
|
185
|
+
const tagSlug = slugForTag(entry.tag);
|
|
186
|
+
if (!tagsSeen.has(entry.tag)) {
|
|
187
|
+
tagsSeen.add(entry.tag);
|
|
188
|
+
tagOrder.push(entry.tag);
|
|
189
|
+
}
|
|
190
|
+
let { key } = entry;
|
|
191
|
+
while (seen.has(key)) {
|
|
192
|
+
key = `${key}-${entry.method}`;
|
|
193
|
+
}
|
|
194
|
+
seen.add(key);
|
|
195
|
+
operations.push({
|
|
196
|
+
...entry,
|
|
197
|
+
key,
|
|
198
|
+
// A root-mounted reference (`route: "/"`) must not emit `//tag/key`.
|
|
199
|
+
route: `${baseRoute === "/" ? "" : baseRoute}/${tagSlug}/${key}`,
|
|
200
|
+
tagSlug,
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const finish = (): CollectedOperations => ({
|
|
205
|
+
operations,
|
|
206
|
+
tags: tagOrder.map((name) => ({
|
|
207
|
+
description: tagMeta.get(name) ?? "",
|
|
208
|
+
name,
|
|
209
|
+
// The same slugger instance, so every tag resolves to the slug its
|
|
210
|
+
// operations were routed under.
|
|
211
|
+
slug: slugForTag(name),
|
|
212
|
+
})),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return { add, finish };
|
|
216
|
+
};
|
|
217
|
+
|
|
130
218
|
/**
|
|
131
219
|
* Flatten a 3.1 document into a route-mapped operation list and its ordered
|
|
132
220
|
* tags. Operations inherit the first tag they declare; keys are de-duplicated so
|
|
@@ -136,19 +224,17 @@ const tagSlugger = (): ((name: string) => string) => {
|
|
|
136
224
|
export const extractOperations = (
|
|
137
225
|
document: ApiDocument,
|
|
138
226
|
baseRoute: string
|
|
139
|
-
):
|
|
140
|
-
const
|
|
141
|
-
const tagOrder: string[] = [];
|
|
142
|
-
const tagsSeen = new Set<string>();
|
|
227
|
+
): ExtractedOperations => {
|
|
228
|
+
const warnings: string[] = [];
|
|
143
229
|
const tagMeta = new Map(
|
|
144
|
-
(document.tags ?? [])
|
|
230
|
+
(document.tags ?? [])
|
|
231
|
+
.filter(hasTagName)
|
|
232
|
+
.map((tag) => [tag.name, tag.description ?? ""])
|
|
145
233
|
);
|
|
146
|
-
const
|
|
147
|
-
const warnings: string[] = [];
|
|
148
|
-
const slugForTag = tagSlugger();
|
|
234
|
+
const collector = operationCollector(baseRoute, tagMeta);
|
|
149
235
|
|
|
150
|
-
for (const [path,
|
|
151
|
-
|
|
236
|
+
for (const [path, item] of Object.entries(document.paths ?? {})) {
|
|
237
|
+
// A parsed spec can carry a null path item despite the type; skip it.
|
|
152
238
|
if (!item) {
|
|
153
239
|
continue;
|
|
154
240
|
}
|
|
@@ -163,52 +249,35 @@ export const extractOperations = (
|
|
|
163
249
|
if (!isOperation(operation)) {
|
|
164
250
|
continue;
|
|
165
251
|
}
|
|
166
|
-
|
|
167
|
-
const tagSlug = slugForTag(tag);
|
|
168
|
-
if (!tagsSeen.has(tag)) {
|
|
169
|
-
tagsSeen.add(tag);
|
|
170
|
-
tagOrder.push(tag);
|
|
171
|
-
}
|
|
172
|
-
let key = operationKey(method, path, operation.operationId);
|
|
173
|
-
while (seen.has(key)) {
|
|
174
|
-
key = `${key}-${method}`;
|
|
175
|
-
}
|
|
176
|
-
seen.add(key);
|
|
177
|
-
operations.push({
|
|
252
|
+
collector.add({
|
|
178
253
|
deprecated: operation.deprecated ?? false,
|
|
179
254
|
description: operation.description ?? "",
|
|
180
|
-
key,
|
|
255
|
+
key: operationKey(method, path, operation.operationId),
|
|
181
256
|
method,
|
|
182
257
|
operationId: operation.operationId,
|
|
183
258
|
path,
|
|
184
|
-
// A root-mounted reference (`route: "/"`) must not emit `//tag/key`.
|
|
185
|
-
route: `${baseRoute === "/" ? "" : baseRoute}/${tagSlug}/${key}`,
|
|
186
259
|
summary: operation.summary ?? "",
|
|
187
|
-
tag,
|
|
188
|
-
tagSlug,
|
|
260
|
+
tag: operation.tags?.[0] ?? UNTAGGED,
|
|
189
261
|
});
|
|
190
262
|
}
|
|
191
263
|
}
|
|
192
264
|
|
|
193
|
-
|
|
194
|
-
description: tagMeta.get(name) ?? "",
|
|
195
|
-
name,
|
|
196
|
-
// The same slugger instance, so every tag resolves to the slug its
|
|
197
|
-
// operations were routed under.
|
|
198
|
-
slug: slugForTag(name),
|
|
199
|
-
}));
|
|
200
|
-
|
|
201
|
-
return { operations, tags, warnings };
|
|
265
|
+
return { ...collector.finish(), warnings };
|
|
202
266
|
};
|
|
203
267
|
|
|
204
|
-
/** Resolve the operation object for a ref out of its document. */
|
|
268
|
+
/** Resolve the operation object for a ref out of its (OpenAPI) document. */
|
|
205
269
|
export const operationObject = (
|
|
206
270
|
spec: ApiSpecData,
|
|
207
271
|
ref: ApiOperationRef
|
|
208
272
|
): OperationObject | undefined => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
273
|
+
// Only OpenAPI refs carry HTTP methods; the AsyncAPI counterpart is
|
|
274
|
+
// `asyncApiOperationObject` in `asyncapi.ts`.
|
|
275
|
+
const method = HTTP_METHODS.find((candidate) => candidate === ref.method);
|
|
276
|
+
// SAFETY: only OpenAPI specs route their refs through this resolver
|
|
277
|
+
// (AsyncAPI documents go to `asyncApiOperationObject`), so the spec's
|
|
278
|
+
// document is the OpenAPI shape.
|
|
279
|
+
const document = spec.document as ApiDocument;
|
|
280
|
+
const item = document.paths?.[ref.path];
|
|
281
|
+
const operation = method === undefined ? undefined : item?.[method];
|
|
213
282
|
return isOperation(operation) ? operation : undefined;
|
|
214
283
|
};
|