@bielu/scalar-signalr 0.1.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/README.md +85 -0
- package/dist/plugin.js +22 -0
- package/dist/types/auth.d.ts +21 -0
- package/dist/types/components/SignalRConsole.vue.d.ts +7 -0
- package/dist/types/discovery.d.ts +13 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/plugin.d.ts +32 -0
- package/dist/types/signalr-bindings.d.ts +11 -0
- package/dist/types/types.d.ts +69 -0
- package/package.json +56 -0
- package/src/auth.ts +94 -0
- package/src/components/SignalRConsole.vue +818 -0
- package/src/discovery.ts +21 -0
- package/src/index.ts +26 -0
- package/src/plugin.ts +39 -0
- package/src/signalr-bindings.ts +114 -0
- package/src/types.ts +75 -0
package/src/discovery.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { resolveDocuments as resolveDocumentsCore } from '@bielu/scalar-core'
|
|
2
|
+
import type { SignalRDocumentRef } from './types'
|
|
3
|
+
|
|
4
|
+
/** How the SignalR bundle discovers its AsyncAPI documents (see `resolveDocuments` in @bielu/scalar-core). */
|
|
5
|
+
export const SIGNALR_DISCOVERY = {
|
|
6
|
+
configKey: 'signalr',
|
|
7
|
+
globalName: '__BIELU_SCALAR_SIGNALR__',
|
|
8
|
+
} as const
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the documents to scan, in decreasing priority: an explicit override (`documents` prop);
|
|
12
|
+
* a `SignalRPluginConfig` embedded inline on the Scalar config as `config.signalr`; a
|
|
13
|
+
* `window.__BIELU_SCALAR_SIGNALR__` global; otherwise auto-discovery from the Scalar configuration
|
|
14
|
+
* passed as `options`.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveDocuments(
|
|
17
|
+
options: Record<string, any> | undefined,
|
|
18
|
+
override?: SignalRDocumentRef[],
|
|
19
|
+
): SignalRDocumentRef[] {
|
|
20
|
+
return resolveDocumentsCore(options, SIGNALR_DISCOVERY, override)
|
|
21
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { bootstrapConsole } from '@bielu/scalar-core'
|
|
2
|
+
import SignalRConsole from './components/SignalRConsole.vue'
|
|
3
|
+
import { SIGNALR_DISCOVERY } from './discovery'
|
|
4
|
+
import { SIGNALR_PLUGIN_SPEC } from './plugin'
|
|
5
|
+
|
|
6
|
+
export { createSignalRPlugin } from './plugin'
|
|
7
|
+
export { loadSignalRHubs, parseSignalRHubs } from './signalr-bindings'
|
|
8
|
+
export type {
|
|
9
|
+
SignalRDirection,
|
|
10
|
+
SignalRDocumentRef,
|
|
11
|
+
SignalRHubModel,
|
|
12
|
+
SignalROperationModel,
|
|
13
|
+
SignalRPluginConfig,
|
|
14
|
+
} from './types'
|
|
15
|
+
|
|
16
|
+
// Register the console Web Component and hook `window.Scalar.createApiReference` so every Scalar
|
|
17
|
+
// API Reference on the page picks up the SignalR plugin (see @bielu/scalar-core's bootstrap).
|
|
18
|
+
bootstrapConsole(
|
|
19
|
+
{
|
|
20
|
+
...SIGNALR_PLUGIN_SPEC,
|
|
21
|
+
discovery: SIGNALR_DISCOVERY,
|
|
22
|
+
stylesId: 'bielu-signalr-styles',
|
|
23
|
+
wrappedFlag: '__bieluSignalRWrapped',
|
|
24
|
+
},
|
|
25
|
+
SignalRConsole,
|
|
26
|
+
)
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createConsolePlugin } from '@bielu/scalar-core'
|
|
2
|
+
import type { SignalRDocumentRef } from './types'
|
|
3
|
+
|
|
4
|
+
/** The custom element tag the console is rendered as (registered in `index.ts`). */
|
|
5
|
+
export const SIGNALR_CONSOLE_TAG = 'bielu-signalr-console'
|
|
6
|
+
|
|
7
|
+
/** The Scalar plugin's identifier, used to detect (and de-duplicate) an existing registration. */
|
|
8
|
+
export const SIGNALR_PLUGIN_NAME = 'bielu-signalr'
|
|
9
|
+
|
|
10
|
+
/** How the SignalR console registers with Scalar (plugin name, element tag, sidebar label). */
|
|
11
|
+
export const SIGNALR_PLUGIN_SPEC = {
|
|
12
|
+
pluginName: SIGNALR_PLUGIN_NAME,
|
|
13
|
+
elementTag: SIGNALR_CONSOLE_TAG,
|
|
14
|
+
sidebarLabel: 'SignalR',
|
|
15
|
+
} as const
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The structural shape of a Scalar API Reference plugin. Declared locally (a structural copy of
|
|
19
|
+
* @bielu/scalar-core's type) so the published declaration files never reference the private core
|
|
20
|
+
* package; Scalar validates it at runtime.
|
|
21
|
+
*/
|
|
22
|
+
export type ApiReferencePlugin = () => {
|
|
23
|
+
name: string
|
|
24
|
+
extensions: unknown[]
|
|
25
|
+
views?: Record<string, unknown[]>
|
|
26
|
+
hooks?: Record<string, unknown>
|
|
27
|
+
apiClientPlugins?: unknown[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates the SignalR console plugin for the Scalar API Reference.
|
|
32
|
+
*
|
|
33
|
+
* The view's `component` is the custom element tag name (a string), so Scalar's own Vue renders a
|
|
34
|
+
* plain element — our Web Component (with its own Vue + styles) takes over from there. The AsyncAPI
|
|
35
|
+
* documents (discovered from the Scalar configuration at registration time) are passed to the
|
|
36
|
+
* element as the `documents` prop, so the console does not depend on Scalar binding its config.
|
|
37
|
+
*/
|
|
38
|
+
export const createSignalRPlugin = (documents?: SignalRDocumentRef[]): ApiReferencePlugin =>
|
|
39
|
+
createConsolePlugin(SIGNALR_PLUGIN_SPEC, documents)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deref,
|
|
3
|
+
exampleFromSchema,
|
|
4
|
+
extractSecuritySchemes,
|
|
5
|
+
firstServerHost,
|
|
6
|
+
loadAsyncApiDocuments,
|
|
7
|
+
refName,
|
|
8
|
+
resolveServerBaseUrl,
|
|
9
|
+
} from '@bielu/scalar-core'
|
|
10
|
+
import type {
|
|
11
|
+
SignalRDirection,
|
|
12
|
+
SignalRDocumentRef,
|
|
13
|
+
SignalRHubModel,
|
|
14
|
+
SignalRMessageModel,
|
|
15
|
+
SignalROperationModel,
|
|
16
|
+
} from './types'
|
|
17
|
+
|
|
18
|
+
const SIGNALR = 'signalr'
|
|
19
|
+
|
|
20
|
+
type AnyRecord = Record<string, any>
|
|
21
|
+
|
|
22
|
+
/** Resolve the message(s) an operation carries and generate an example payload for each. */
|
|
23
|
+
function buildMessages(doc: AnyRecord, op: AnyRecord): SignalRMessageModel[] {
|
|
24
|
+
const refs = (Array.isArray(op.messages) ? op.messages : [])
|
|
25
|
+
.map((m: AnyRecord) => m?.$ref)
|
|
26
|
+
.filter((ref: unknown): ref is string => typeof ref === 'string')
|
|
27
|
+
|
|
28
|
+
const messages: SignalRMessageModel[] = []
|
|
29
|
+
for (const ref of refs) {
|
|
30
|
+
const message = deref(doc, { $ref: ref })
|
|
31
|
+
const name = message?.name ?? refName(ref) ?? 'message'
|
|
32
|
+
const payloadExample = exampleFromSchema(doc, message?.payload)
|
|
33
|
+
// SignalR hub methods take positional arguments; the payload is the (first) argument.
|
|
34
|
+
const args = payloadExample === null ? [] : [payloadExample]
|
|
35
|
+
messages.push({ name, title: message?.title, example: JSON.stringify(args, null, 2) })
|
|
36
|
+
}
|
|
37
|
+
return messages
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function directionFor(op: AnyRecord, binding: AnyRecord | undefined): SignalRDirection {
|
|
41
|
+
const fromBinding = binding?.direction
|
|
42
|
+
if (fromBinding === 'clientToServer' || fromBinding === 'serverToClient') {
|
|
43
|
+
return fromBinding
|
|
44
|
+
}
|
|
45
|
+
// AsyncAPI 3 `action`: `send` = application -> channel, `receive` = channel -> application.
|
|
46
|
+
return op.action === 'receive' ? 'serverToClient' : 'clientToServer'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Extract every SignalR hub (channel + its operations) from a parsed AsyncAPI document. */
|
|
50
|
+
export function parseSignalRHubs(documentName: string, doc: AnyRecord): SignalRHubModel[] {
|
|
51
|
+
if (!doc || typeof doc !== 'object') {
|
|
52
|
+
return []
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const baseUrl = resolveServerBaseUrl(firstServerHost(doc, SIGNALR))
|
|
56
|
+
|
|
57
|
+
// Extract security schemes from the document so the console can read them at connect time
|
|
58
|
+
// without re-fetching the document. Only a minimal subset is needed for auth resolution.
|
|
59
|
+
const securitySchemes = extractSecuritySchemes(doc)
|
|
60
|
+
const docSecuritySchemes = Object.keys(securitySchemes).length > 0 ? securitySchemes : undefined
|
|
61
|
+
|
|
62
|
+
const channels: AnyRecord = doc.channels ?? {}
|
|
63
|
+
const hubs = new Map<string, SignalRHubModel>()
|
|
64
|
+
|
|
65
|
+
for (const channelName of Object.keys(channels)) {
|
|
66
|
+
const channel: AnyRecord = channels[channelName] ?? {}
|
|
67
|
+
const binding: AnyRecord | undefined = channel.bindings?.[SIGNALR]
|
|
68
|
+
if (!binding) {
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
hubs.set(channelName, {
|
|
72
|
+
documentName,
|
|
73
|
+
channelName,
|
|
74
|
+
hubPath: binding.hub ?? channel.address ?? `/${channelName}`,
|
|
75
|
+
baseUrl,
|
|
76
|
+
transports: binding.transports ?? [],
|
|
77
|
+
protocols: binding.protocols ?? [],
|
|
78
|
+
operations: [],
|
|
79
|
+
securitySchemes: docSecuritySchemes,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const operations: AnyRecord = doc.operations ?? {}
|
|
84
|
+
for (const opName of Object.keys(operations)) {
|
|
85
|
+
const op: AnyRecord = operations[opName] ?? {}
|
|
86
|
+
const channelName = refName(op.channel?.$ref)
|
|
87
|
+
const hub = channelName ? hubs.get(channelName) : undefined
|
|
88
|
+
if (!hub) {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
const binding: AnyRecord | undefined = op.bindings?.[SIGNALR]
|
|
92
|
+
const model: SignalROperationModel = {
|
|
93
|
+
id: opName,
|
|
94
|
+
target: binding?.target ?? opName,
|
|
95
|
+
direction: directionFor(op, binding),
|
|
96
|
+
callType: binding?.callType,
|
|
97
|
+
summary: op.summary ?? op.description,
|
|
98
|
+
messages: buildMessages(doc, op),
|
|
99
|
+
}
|
|
100
|
+
hub.operations.push(model)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [...hubs.values()]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the configured documents (inline object or URL) and aggregate the SignalR hubs they
|
|
108
|
+
* declare. Only documents that are actually AsyncAPI are considered, so OpenAPI sources in the
|
|
109
|
+
* same Scalar configuration are ignored.
|
|
110
|
+
*/
|
|
111
|
+
export async function loadSignalRHubs(documents: SignalRDocumentRef[]): Promise<SignalRHubModel[]> {
|
|
112
|
+
const loaded = await loadAsyncApiDocuments(documents)
|
|
113
|
+
return loaded.flatMap(({ name, doc }) => parseSignalRHubs(name, doc))
|
|
114
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Direction of a SignalR hub operation relative to the server. */
|
|
2
|
+
export type SignalRDirection = 'clientToServer' | 'serverToClient'
|
|
3
|
+
|
|
4
|
+
/** A reference to an AsyncAPI document — either by URL or as an already-parsed object. */
|
|
5
|
+
export type SignalRDocumentRef = {
|
|
6
|
+
/** Logical document name (matches the AsyncAPI/source title). */
|
|
7
|
+
name: string
|
|
8
|
+
/** URL the AsyncAPI JSON document is served from. */
|
|
9
|
+
url?: string
|
|
10
|
+
/** An inline, already-parsed document object (from a Scalar source's `content`). */
|
|
11
|
+
doc?: Record<string, any>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Configuration consumed by the SignalR console plugin. */
|
|
15
|
+
export type SignalRPluginConfig = {
|
|
16
|
+
/**
|
|
17
|
+
* AsyncAPI documents to scan for SignalR bindings. Normally derived automatically from the
|
|
18
|
+
* Scalar configuration's `sources`; only set explicitly to override that discovery.
|
|
19
|
+
*/
|
|
20
|
+
documents: SignalRDocumentRef[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A message an operation can carry, with a generated example payload. */
|
|
24
|
+
export type SignalRMessageModel = {
|
|
25
|
+
/** Message name/key (e.g. `chatMessage`). */
|
|
26
|
+
name: string
|
|
27
|
+
/** Human-friendly title, when the document provides one. */
|
|
28
|
+
title?: string
|
|
29
|
+
/**
|
|
30
|
+
* A ready-to-send example of the hub method arguments, as a pretty-printed JSON array string
|
|
31
|
+
* (e.g. `[{ "user": "string", … }]`), generated from the message payload schema.
|
|
32
|
+
*/
|
|
33
|
+
example: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A single hub operation (client-to-server method or server-to-client event). */
|
|
37
|
+
export type SignalROperationModel = {
|
|
38
|
+
/** AsyncAPI operation id. */
|
|
39
|
+
id: string
|
|
40
|
+
/** The hub method / event name actually used on the wire. */
|
|
41
|
+
target: string
|
|
42
|
+
direction: SignalRDirection
|
|
43
|
+
/** `invocation`, `streamInvocation` or `send`. */
|
|
44
|
+
callType?: string
|
|
45
|
+
summary?: string
|
|
46
|
+
/** Messages this operation carries, each with an example payload to prefill the editor. */
|
|
47
|
+
messages: SignalRMessageModel[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A minimal representation of an AsyncAPI security scheme, extracted at document-parse time. */
|
|
51
|
+
export type SecuritySchemeModel = {
|
|
52
|
+
/** e.g. `'apiKey'`, `'http'`, `'oauth2'`, `'openIdConnect'`. */
|
|
53
|
+
type: string
|
|
54
|
+
/** For `apiKey`: `'query'`, `'header'`, or `'cookie'`. */
|
|
55
|
+
in?: string
|
|
56
|
+
/** For `apiKey`: the query-param or header field name. */
|
|
57
|
+
name?: string
|
|
58
|
+
/** For `http`: `'bearer'`, `'basic'`, etc. */
|
|
59
|
+
scheme?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A SignalR hub discovered from an AsyncAPI channel with a `signalr` binding. */
|
|
63
|
+
export type SignalRHubModel = {
|
|
64
|
+
documentName: string
|
|
65
|
+
channelName: string
|
|
66
|
+
/** Hub path, e.g. `/chatHub`. */
|
|
67
|
+
hubPath: string
|
|
68
|
+
/** Best-effort absolute base URL (scheme://host) resolved from the AsyncAPI servers. */
|
|
69
|
+
baseUrl: string
|
|
70
|
+
transports: string[]
|
|
71
|
+
protocols: string[]
|
|
72
|
+
operations: SignalROperationModel[]
|
|
73
|
+
/** Security schemes declared in this hub's AsyncAPI document, keyed by scheme name. */
|
|
74
|
+
securitySchemes?: Record<string, SecuritySchemeModel>
|
|
75
|
+
}
|