@scalar/mock-server 0.12.11 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/create-asyncapi-mock-server.d.ts +8 -2
  3. package/dist/create-asyncapi-mock-server.d.ts.map +1 -1
  4. package/dist/create-asyncapi-mock-server.js +5 -2
  5. package/dist/create-mock-server.d.ts.map +1 -1
  6. package/dist/create-mock-server.js +112 -7
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/routes/mock-any-response.d.ts +1 -4
  10. package/dist/routes/mock-any-response.d.ts.map +1 -1
  11. package/dist/routes/mock-any-response.js +43 -22
  12. package/dist/routes/mock-handler-response.d.ts.map +1 -1
  13. package/dist/routes/mock-handler-response.js +2 -1
  14. package/dist/types.d.ts +15 -0
  15. package/dist/types.d.ts.map +1 -1
  16. package/dist/utils/build-handler-context.d.ts.map +1 -1
  17. package/dist/utils/build-handler-context.js +3 -2
  18. package/dist/utils/collect-sse-events.d.ts +30 -0
  19. package/dist/utils/collect-sse-events.d.ts.map +1 -0
  20. package/dist/utils/collect-sse-events.js +137 -0
  21. package/dist/utils/hono-route-from-path.d.ts +16 -3
  22. package/dist/utils/hono-route-from-path.d.ts.map +1 -1
  23. package/dist/utils/hono-route-from-path.js +98 -5
  24. package/dist/utils/log-authentication-instructions.d.ts +7 -2
  25. package/dist/utils/log-authentication-instructions.d.ts.map +1 -1
  26. package/dist/utils/log-authentication-instructions.js +64 -60
  27. package/dist/utils/path-parameters.d.ts +13 -0
  28. package/dist/utils/path-parameters.d.ts.map +1 -0
  29. package/dist/utils/path-parameters.js +17 -0
  30. package/dist/utils/replace-circular-markers.d.ts +22 -0
  31. package/dist/utils/replace-circular-markers.d.ts.map +1 -0
  32. package/dist/utils/replace-circular-markers.js +226 -0
  33. package/dist/utils/request-matches-pinned-query.d.ts +10 -0
  34. package/dist/utils/request-matches-pinned-query.d.ts.map +1 -0
  35. package/dist/utils/request-matches-pinned-query.js +13 -0
  36. package/dist/utils/resolve-logger.d.ts +12 -0
  37. package/dist/utils/resolve-logger.d.ts.map +1 -0
  38. package/dist/utils/resolve-logger.js +16 -0
  39. package/dist/utils/serialize-response-body.d.ts +11 -0
  40. package/dist/utils/serialize-response-body.d.ts.map +1 -0
  41. package/dist/utils/serialize-response-body.js +88 -0
  42. package/dist/utils/split-path-key.d.ts +30 -0
  43. package/dist/utils/split-path-key.d.ts.map +1 -0
  44. package/dist/utils/split-path-key.js +77 -0
  45. package/dist/utils/validate-request.d.ts.map +1 -1
  46. package/dist/utils/validate-request.js +13 -2
  47. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # @scalar/mock-server
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#10051](https://github.com/scalar/scalar/pull/10051): feat: add a `logger` option to control startup logging
8
+
9
+ `createMockServer()` prints authentication instructions for the security schemes of the document when it starts. That is helpful in a terminal, but it is noise when the mock server runs inside a test harness or another program, which so far left callers replacing the global console.
10
+
11
+ Pass `logger: false` to silence those instructions, or a `(line: string) => void` sink to route them elsewhere:
12
+
13
+ ```ts
14
+ const app = await createMockServer({ document, logger: false })
15
+ ```
16
+
17
+ Diagnostics are not affected: warnings and errors about security schemes the mock server cannot handle, request validator compilation errors, and `x-seed` errors are printed either way.
18
+
19
+ `createAsyncApiMockServer()` already accepted a `logger` sink; it now takes the same `boolean | ((line: string) => void)` shape, so both factories are silenced and redirected the same way. It stays silent by default — pass `logger: true` to print its transport lifecycle lines.
20
+
21
+ The `MockServerLogger` type is now exported as well, so a custom sink can be typed outside of the package.
22
+
23
+ - [#10037](https://github.com/scalar/scalar/pull/10037): Frame `text/event-stream` responses as Server-Sent Events instead of returning a single JSON body with an SSE content type. Each event is written as a `data:` line terminated by a blank line, then the stream closes.
24
+ - Named `examples` are read as the sequence of events the endpoint emits, in declaration order (`Prefer: example=<name>` still pins the stream to that one example).
25
+ - An array example is read as the event sequence too, one event per item.
26
+ - An example that already spells out SSE framing (`data:` and `event:` lines, or a `:` comment heartbeat) is written as its own framing, with only its terminating blank line normalized, instead of being wrapped in a second `data:` line. Such examples describe a whole stream, so a map of them is read as alternatives and the first one is served.
27
+ - With no example, the schema-generated payload is emitted three times so the stream has more than one event to iterate — unless the schema already generates a sequence (a multi-item array, or a string that spells the wire format out), which is sent once, not repeated.
28
+
29
+ - [#10038](https://github.com/scalar/scalar/pull/10038): Answer unhandled errors with a structured JSON `500` naming the operation that failed, instead of the previous plain-text `Internal Server Error`. The body reports the error `message` along with the matched operation's `method`, OpenAPI `path`, and `operationId` (when the document declares one), and the error is still logged to the console. Errors that already carry their own response keep the status and body they chose.
30
+
31
+ ### Patch Changes
32
+
33
+ - [#9967](https://github.com/scalar/scalar/pull/9967): Align the `ajv` and `ajv-formats` dependencies with the shared workspace catalog (`ajv@^8.20.0`).
34
+ - [#10039](https://github.com/scalar/scalar/pull/10039): Fix request validation falling open for recursive schemas. Resolving a schema that references itself leaves a `'[circular]'` marker where the cycle was cut, which Ajv refused to compile, so the mock server logged an error and skipped validating the request body — or every parameter in that location. The recursion point now compiles as an always-valid schema, and a constraint that would flip that into a stricter one (`not`, `if`, `oneOf`, `contains`, and the keywords that only qualify them) is dropped, so the rest of the schema is enforced again without rejecting requests the document allows.
35
+ - [#10035](https://github.com/scalar/scalar/pull/10035): Fix JSON responses for primitive string bodies. A response declaring `application/json` with a `type: string` schema (or a plain string example) was written to the wire verbatim, so clients received the bare characters `string` instead of `"string"` and could not parse the payload.
36
+
37
+ A string body is now JSON-encoded whenever the negotiated media type carries a single JSON document, which includes suffixed types such as `application/problem+json` and parameterized ones such as `application/json; charset=utf-8`. Two things keep their raw string: every other media type, where the characters are already the payload (`text/plain`, `text/event-stream`, XML, and the line-delimited JSON types), and a body that is already the value the schema describes — anything that parses when the schema declares a non-string type, or a JSON object or array when the schema says nothing, both of which are documents the author serialized by hand.
38
+
39
+ XML is now matched on the parsed media type subtype rather than by substring, so only a genuine XML type is serialized as an XML document. A media type that merely contains `xml` somewhere, such as one carrying it in a parameter, no longer is.
40
+
41
+ - [#10044](https://github.com/scalar/scalar/pull/10044): fix: escape spec-derived path keys and route path keys that carry a query string
42
+
43
+ Path keys such as `/v1/messages?beta=true` used to be registered verbatim as routes, where the query string was read as routing syntax. On a parameterized path that made the router compile an invalid regular expression and every single request failed with an empty `500`. The query is now peeled off and matched against the incoming request, so `/v1/messages?beta=true` answers only requests that send `beta=true` and `/v1/messages` keeps answering the rest.
44
+
45
+ Characters that would otherwise be read as routing syntax (`:`, `*`, `|`, and braces outside a path parameter) are escaped, so a path key can no longer act as a pattern. Note that this also applies to `*`: a path key ending in `*` is now served as a literal path instead of matching everything below it.
46
+
47
+ One limitation is worth knowing: Hono allows a single parameter per path segment, so a segment that mixes a path parameter with escaped literal text (`/v1/jobs/{jobId}:cancel`) routes to the right operation but does not bind `jobId` by name. Request validation reads it as missing, so a document describing such a path needs the server-wide `validateRequest: false` for now.
48
+
49
+ ## 0.12.13
50
+
51
+ ### Patch Changes
52
+
53
+ - [#9941](https://github.com/scalar/scalar/pull/9941): Republish every package through npm trusted publishing. No functional changes.
54
+
55
+ ## 0.12.12
56
+
3
57
  ## 0.12.11
4
58
 
5
59
  ## 0.12.10
@@ -1,6 +1,7 @@
1
1
  import { createNodeWebSocket } from '@hono/node-ws';
2
2
  import { Hono } from 'hono';
3
3
  import type { MessageDirection, MockTransport } from './transports/types.js';
4
+ import type { MockServerLogger } from './types.js';
4
5
  /** Options for {@link createAsyncApiMockServer}. */
5
6
  export type AsyncApiMockServerOptions = {
6
7
  /**
@@ -20,8 +21,13 @@ export type AsyncApiMockServerOptions = {
20
21
  direction: MessageDirection;
21
22
  payload: unknown;
22
23
  }) => void;
23
- /** Optional sink for transport lifecycle log lines. Defaults to no-op. */
24
- logger?: (line: string) => void;
24
+ /**
25
+ * Control the transport lifecycle log lines the server prints. Pass `true` to log them to the
26
+ * console, a `(line) => void` sink to route them elsewhere, or `false` (the default) to stay silent.
27
+ *
28
+ * Diagnostics such as a channel with no matching transport are printed either way.
29
+ */
30
+ logger?: boolean | MockServerLogger;
25
31
  };
26
32
  /** The result of {@link createAsyncApiMockServer}. */
27
33
  export type AsyncApiMockServer = {
@@ -1 +1 @@
1
- {"version":3,"file":"create-asyncapi-mock-server.d.ts","sourceRoot":"","sources":["../src/create-asyncapi-mock-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAoB,MAAM,oBAAoB,CAAA;AAK3F,oDAAoD;AACpD,MAAM,MAAM,yBAAyB,GAAG;IACtC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEvC;;;;OAIG;IACH,UAAU,CAAC,EAAE,aAAa,EAAE,CAAA;IAE5B,yFAAyF;IACzF,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IAE/F,0EAA0E;IAC1E,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC,CAAA;AAED,sDAAsD;AACtD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,sEAAsE;IACtE,GAAG,EAAE,IAAI,CAAA;IACT;;;OAGG;IACH,eAAe,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,iBAAiB,CAAC,CAAA;CAC3E,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAsC9G"}
1
+ {"version":3,"file":"create-asyncapi-mock-server.d.ts","sourceRoot":"","sources":["../src/create-asyncapi-mock-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAoB,MAAM,oBAAoB,CAAA;AAC3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAM/C,oDAAoD;AACpD,MAAM,MAAM,yBAAyB,GAAG;IACtC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEvC;;;;OAIG;IACH,UAAU,CAAC,EAAE,aAAa,EAAE,CAAA;IAE5B,yFAAyF;IACzF,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IAE/F;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAA;CACpC,CAAA;AAED,sDAAsD;AACtD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,sEAAsE;IACtE,GAAG,EAAE,IAAI,CAAA;IACT;;;OAGG;IACH,eAAe,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,iBAAiB,CAAC,CAAA;CAC3E,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA0C9G"}
@@ -5,6 +5,7 @@ import { defaultTransports } from './transports/index.js';
5
5
  import { generateMessage } from './utils/generate-message.js';
6
6
  import { processAsyncApiDocument } from './utils/process-asyncapi-document.js';
7
7
  import { resolveChannels } from './utils/resolve-channels.js';
8
+ import { resolveLogger } from './utils/resolve-logger.js';
8
9
  /**
9
10
  * Create a mock server for an AsyncAPI 3.1 document — the event-driven counterpart of
10
11
  * {@link createMockServer}. Each channel is registered on a transport (WebSocket or SSE by
@@ -28,7 +29,7 @@ export async function createAsyncApiMockServer(options) {
28
29
  const document = await processAsyncApiDocument(options.document);
29
30
  const channels = resolveChannels(document);
30
31
  const transports = [...defaultTransports, ...(options.transports ?? [])];
31
- const log = options.logger ?? (() => undefined);
32
+ const log = resolveLogger(options.logger, false);
32
33
  // CORS for the SSE/HTTP routes (WebSocket upgrades are not subject to CORS).
33
34
  app.use(cors());
34
35
  const context = {
@@ -41,7 +42,9 @@ export async function createAsyncApiMockServer(options) {
41
42
  for (const channel of channels) {
42
43
  const transport = transports.find((candidate) => candidate.supports(channel));
43
44
  if (!transport) {
44
- log(`[asyncapi] no transport for channel "${channel.id}" (protocols: ${channel.protocols.join(', ') || 'none'})`);
45
+ // A channel with no matching transport is a configuration problem, so surface it through the
46
+ // console unconditionally rather than the (silenceable) logger.
47
+ console.warn(`[asyncapi] no transport for channel "${channel.id}" (protocols: ${channel.protocols.join(', ') || 'none'})`);
45
48
  continue;
46
49
  }
47
50
  transport.register(channel, context);
@@ -1 +1 @@
1
- {"version":3,"file":"create-mock-server.d.ts","sourceRoot":"","sources":["../src/create-mock-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAG3B,OAAO,KAAK,EAAc,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAiB5D;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,aAAa,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2GtF"}
1
+ {"version":3,"file":"create-mock-server.d.ts","sourceRoot":"","sources":["../src/create-mock-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,IAAI,EAA0B,MAAM,MAAM,CAAA;AAIjE,OAAO,KAAK,EAAc,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAwD5D;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,aAAa,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8NtF"}
@@ -1,5 +1,6 @@
1
1
  import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
2
  import { Hono } from 'hono';
3
+ import { every } from 'hono/combine';
3
4
  import { cors } from 'hono/cors';
4
5
  import { buildSeedContext } from './utils/build-seed-context.js';
5
6
  import { executeSeed } from './utils/execute-seed.js';
@@ -9,17 +10,62 @@ import { honoRouteFromPath } from './utils/hono-route-from-path.js';
9
10
  import { isAuthenticationRequired } from './utils/is-authentication-required.js';
10
11
  import { logAuthenticationInstructions } from './utils/log-authentication-instructions.js';
11
12
  import { processOpenApiDocument } from './utils/process-openapi-document.js';
13
+ import { requestMatchesPinnedQuery } from './utils/request-matches-pinned-query.js';
14
+ import { resolveLogger } from './utils/resolve-logger.js';
12
15
  import { setUpAuthenticationRoutes } from './utils/set-up-authentication-routes.js';
16
+ import { splitPathKey } from './utils/split-path-key.js';
13
17
  import { validateRequest } from './utils/validate-request.js';
14
18
  import { store } from './libs/store.js';
15
19
  import { mockAnyResponse } from './routes/mock-any-response.js';
16
20
  import { mockHandlerResponse } from './routes/mock-handler-response.js';
17
21
  import { respondWithOpenApiDocument } from './routes/respond-with-openapi-document.js';
22
+ /** Record which operation a request matched, so an error escaping it can be named */
23
+ const setMockedOperation = (c, operation) => {
24
+ ;
25
+ c.set('mockedOperation', operation);
26
+ };
27
+ /** Read back the operation a request matched, or `undefined` when it reached no mocked route */
28
+ const getMockedOperation = (c) => c.get('mockedOperation');
29
+ /**
30
+ * Whether an error carries its own response, the way Hono's `HTTPException` does.
31
+ *
32
+ * Checked structurally rather than with `instanceof`, exactly like Hono's own default handler, so an
33
+ * `HTTPException` thrown from a second copy of Hono is still recognized.
34
+ */
35
+ const carriesResponse = (error) => 'getResponse' in error && typeof error.getResponse === 'function';
18
36
  /**
19
37
  * Create a mock server instance
20
38
  */
21
39
  export async function createMockServer(configuration) {
22
40
  const app = new Hono();
41
+ // Unhandled errors would otherwise reach Hono's default handler, which answers with a plain-text
42
+ // `Internal Server Error` that says nothing about what broke. A mock server fails for mundane,
43
+ // document-shaped reasons — a response header name the runtime rejects, an example that cannot be
44
+ // serialized — so answer with JSON that names the failing operation and repeats the message,
45
+ // which makes the failure readable in the response instead of something to go reproduce.
46
+ app.onError((error, c) => {
47
+ // The status and body such an error chose are deliberate, not an internal failure, so answer
48
+ // with them. Unlike Hono's default handler, which returns the response directly, it is rebuilt
49
+ // through the context so headers already staged on the response — those from the CORS
50
+ // middleware, most of all — still apply.
51
+ if (carriesResponse(error)) {
52
+ const response = error.getResponse();
53
+ return c.newResponse(response.body, response);
54
+ }
55
+ const operation = getMockedOperation(c);
56
+ // Keep logging the error, as Hono's default handler does, so the stack trace is not lost. Routes
57
+ // added to the returned app are not mocked operations, so name the concrete request instead —
58
+ // the log always points somewhere.
59
+ console.error(operation
60
+ ? `Error while mocking ${operation.method} ${operation.path}:`
61
+ : `Error handling ${c.req.method} ${c.req.path}:`, error);
62
+ return c.json({
63
+ error: 'Internal Server Error',
64
+ message: error.message,
65
+ // Left out entirely rather than sent as `null`, so a client can test for the key.
66
+ ...(operation ? { operation } : {}),
67
+ }, 500);
68
+ });
23
69
  /** Dereferenced OpenAPI document */
24
70
  const schema = await processOpenApiDocument(configuration?.document ?? configuration?.specification);
25
71
  // Seed data from schemas with x-seed extension
@@ -52,10 +98,34 @@ export async function createMockServer(configuration) {
52
98
  app.use(cors());
53
99
  /** Authentication methods defined in the OpenAPI document */
54
100
  setUpAuthenticationRoutes(app, schema);
55
- logAuthenticationInstructions(schema?.components?.securitySchemes || {});
101
+ // Only the instructions honor `logger` (on by default); the util still prints warnings and errors
102
+ // about security schemes the mock server cannot handle, which a silenced startup should surface.
103
+ logAuthenticationInstructions(schema?.components?.securitySchemes || {}, resolveLogger(configuration.logger, true));
56
104
  /** Paths specified in the OpenAPI document */
57
105
  const paths = schema?.paths ?? {};
58
- Object.keys(paths).forEach((path) => {
106
+ // A path key may pin query parameters to describe a variant of an operation, for example
107
+ // `/v1/messages?beta=true` next to `/v1/messages`. Hono runs every matching route in registration
108
+ // order, so a variant has to come before the sibling it shares a path with — otherwise the sibling
109
+ // answers its requests too, and the more pinned parameters a key has the more specific it is.
110
+ const pathKeys = Object.keys(paths).map((path) => {
111
+ const { path: pathname, query } = splitPathKey(path);
112
+ return { path, pathname, query };
113
+ });
114
+ /** Where each path first shows up in the document, so its variants stay with it */
115
+ const documentOrder = new Map();
116
+ pathKeys.forEach(({ pathname }, index) => {
117
+ if (!documentOrder.has(pathname)) {
118
+ documentOrder.set(pathname, index);
119
+ }
120
+ });
121
+ // Keys that share a path are grouped where the first of them appears and the most specific one
122
+ // leads the group; a key with a path of its own never moves. So the only keys that change places
123
+ // with anything unrelated are the variants of a path that is described more than once. When two
124
+ // distinct paths overlap — a literal and a parameterized one that pins a query — neither is moved,
125
+ // so whichever the document declares first is matched first, the same as any pair of overlapping
126
+ // Hono routes.
127
+ const orderedPathKeys = [...pathKeys].sort((a, b) => (documentOrder.get(a.pathname) ?? 0) - (documentOrder.get(b.pathname) ?? 0) || b.query.length - a.query.length);
128
+ orderedPathKeys.forEach(({ path, query }) => {
59
129
  // A path item may itself be a `$ref`, so resolve it before reading its operations.
60
130
  const pathItem = getResolvedRef(paths[path]);
61
131
  const methods = Object.keys(getOperations(pathItem));
@@ -63,17 +133,37 @@ export async function createMockServer(configuration) {
63
133
  methods.forEach((method) => {
64
134
  const route = honoRouteFromPath(path);
65
135
  const operation = pathItem?.[method];
136
+ // Remember which operation this route mocks, so the error handler can name it when something
137
+ // fails downstream. Recorded on the context rather than mapped back from the request path,
138
+ // which would not survive the app being mounted under a base path. Registered before the rest
139
+ // of the route so a failure in request validation is named too. The OpenAPI path key is kept
140
+ // (rather than the Hono route) because that is what the document author reads.
141
+ const mockedOperation = {
142
+ // `toUpperCase` widens to `string`, so restate the narrower type the method union guarantees.
143
+ method: method.toUpperCase(),
144
+ path,
145
+ ...(operation?.operationId ? { operationId: operation.operationId } : {}),
146
+ };
147
+ /** Middleware chain answering this operation, in the order it runs */
148
+ const handlers = [];
149
+ // Runs first, so the error handler can name the operation even when validation fails. For a
150
+ // path key that pins a query it is part of the guarded chain below, so it only fires once the
151
+ // request actually matches the variant rather than for one that is handed on to the sibling.
152
+ handlers.push(async (c, next) => {
153
+ setMockedOperation(c, mockedOperation);
154
+ await next();
155
+ });
66
156
  // Operation-level security overrides the global requirement, so fall back to the
67
157
  // document-wide `security` when the operation does not define its own.
68
158
  const effectiveSecurity = operation.security ?? schema?.security;
69
159
  // Check if authentication is required for this operation
70
160
  if (isAuthenticationRequired(effectiveSecurity)) {
71
- app[method](route, handleAuthentication(schema, operation));
161
+ handlers.push(handleAuthentication(schema, operation));
72
162
  }
73
163
  // Notify the `onRequest` callback before validation runs, so it fires for every request —
74
164
  // including ones the validation middleware rejects with a `422`.
75
165
  if (configuration.onRequest) {
76
- app[method](route, async (c, next) => {
166
+ handlers.push(async (c, next) => {
77
167
  configuration.onRequest?.({ context: c, operation });
78
168
  await next();
79
169
  });
@@ -82,7 +172,7 @@ export async function createMockServer(configuration) {
82
172
  // opt out with `validateRequest: false`). Runs after authentication but before the
83
173
  // mock handler. Validators are compiled once here, so there is no per-request recompilation.
84
174
  if (configuration.validateRequest !== false) {
85
- app[method](route, validateRequest(operation, pathItem?.parameters));
175
+ handlers.push(validateRequest(operation, pathItem?.parameters));
86
176
  }
87
177
  // Check if operation has x-handler extension
88
178
  // Validate that it's a non-empty string (consistent with x-seed validation)
@@ -90,11 +180,26 @@ export async function createMockServer(configuration) {
90
180
  const hasHandler = handlerCode && typeof handlerCode === 'string' && handlerCode.trim().length > 0;
91
181
  // Route to appropriate handler
92
182
  if (hasHandler) {
93
- app[method](route, (c) => mockHandlerResponse(c, operation));
183
+ handlers.push(async (c) => await mockHandlerResponse(c, operation));
94
184
  }
95
185
  else {
96
- app[method](route, (c) => mockAnyResponse(c, operation));
186
+ handlers.push(async (c) => await mockAnyResponse(c, operation));
187
+ }
188
+ if (query.length === 0) {
189
+ handlers.forEach((handler) => app[method](route, handler));
190
+ return;
97
191
  }
192
+ // The pinned query parameters are not part of the route, so they are checked here. A request
193
+ // that does not carry them is handed on to the next matching route — usually the sibling path
194
+ // key without the query string.
195
+ const operationChain = every(...handlers);
196
+ app[method](route, async (c, next) => {
197
+ if (!requestMatchesPinnedQuery(c, query)) {
198
+ await next();
199
+ return;
200
+ }
201
+ await operationChain(c, next);
202
+ });
98
203
  });
99
204
  });
100
205
  // OpenAPI JSON file
package/dist/index.d.ts CHANGED
@@ -2,5 +2,6 @@ export { type AsyncApiMockServer, type AsyncApiMockServerOptions, createAsyncApi
2
2
  export { createMockServer } from './create-mock-server.js';
3
3
  export { defaultTransports, sseTransport, websocketTransport } from './transports/index.js';
4
4
  export type { MessageDirection, MockMessage, MockTransport, ResolvedChannel, ResolvedMessage, ResolvedOperation, TransportContext, } from './transports/types.js';
5
+ export type { MockServerLogger, MockServerOptions } from './types.js';
5
6
  export { isAsyncApiDocument } from './utils/process-asyncapi-document.js';
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,wBAAwB,GACzB,MAAM,+BAA+B,CAAA;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAClF,YAAY,EACV,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,wBAAwB,GACzB,MAAM,+BAA+B,CAAA;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAClF,YAAY,EACV,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAA"}
@@ -1,10 +1,7 @@
1
1
  import type { OpenAPIV3_1 } from '@scalar/openapi-types';
2
2
  import type { Context } from 'hono';
3
- import type { StatusCode } from 'hono/utils/http-status';
4
3
  /**
5
4
  * Mock any response
6
5
  */
7
- export declare function mockAnyResponse(c: Context, operation: OpenAPIV3_1.OperationObject): (Response & import("hono").TypedResponse<{
8
- error: string;
9
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<null, StatusCode, "body">) | (Response & import("hono").TypedResponse<string, import("hono/utils/http-status").ContentfulStatusCode, "body">);
6
+ export declare function mockAnyResponse(c: Context, operation: OpenAPIV3_1.OperationObject): Response;
10
7
  //# sourceMappingURL=mock-any-response.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mock-any-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-any-response.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAOxD;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe;;4PAwGjF"}
1
+ {"version":3,"file":"mock-any-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-any-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAanC;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe,YA6HjF"}
@@ -1,12 +1,15 @@
1
- import { json2xml } from '@scalar/helpers/file/json2xml';
2
1
  import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
3
2
  import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved-ref-deep';
4
3
  import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
5
4
  import { accepts } from 'hono/accepts';
5
+ import { streamSSE } from 'hono/streaming';
6
+ import { collectSseEvents, isEventStreamContentType } from '../utils/collect-sse-events.js';
6
7
  import { findPreferredResponseKey } from '../utils/find-preferred-response-key.js';
7
8
  import { normalizeResponseBody } from '../utils/normalize-response-body.js';
8
9
  import { parsePreferHeader } from '../utils/parse-prefer-header.js';
10
+ import { pathParameters } from '../utils/path-parameters.js';
9
11
  import { selectResponseExample } from '../utils/select-response-example.js';
12
+ import { serializeResponseBody } from '../utils/serialize-response-body.js';
10
13
  /**
11
14
  * Mock any response
12
15
  */
@@ -61,33 +64,51 @@ export function mockAnyResponse(c, operation) {
61
64
  });
62
65
  c.header('Content-Type', acceptedContentType);
63
66
  const acceptedResponse = selectedResponse?.content?.[acceptedContentType];
67
+ const responseSchema = acceptedResponse?.schema ? getResolvedRefDeep(acceptedResponse.schema) : undefined;
68
+ /** Generates the response body from the schema, or returns `undefined` when there is no schema. */
69
+ const generateFromSchema = () => responseSchema
70
+ ? getExampleFromSchema(responseSchema, {
71
+ emptyString: 'string',
72
+ variables: pathParameters(c),
73
+ mode: 'read',
74
+ })
75
+ : undefined;
76
+ // Server-Sent Events are a framed, multi-event wire format, so they cannot go out as one buffered
77
+ // body: a client reading the stream expects `data:` lines terminated by a blank line. Everything
78
+ // else (JSON, XML, text) keeps taking the single-body path below.
79
+ if (isEventStreamContentType(acceptedContentType)) {
80
+ const events = collectSseEvents(acceptedResponse, {
81
+ exampleName: prefer.example,
82
+ generate: generateFromSchema,
83
+ });
84
+ c.status(statusCode);
85
+ // `streamSSE` sets the transport headers itself (`Content-Type`, `Cache-Control`, `Connection`,
86
+ // `Transfer-Encoding`), so those win over a value the document declared for the same header —
87
+ // they are what makes the stream readable. Every other declared header set above survives.
88
+ return streamSSE(c, async (stream) => {
89
+ for (const event of events) {
90
+ if (event.framed) {
91
+ await stream.write(event.text);
92
+ }
93
+ else {
94
+ await stream.writeSSE({ data: event.text });
95
+ }
96
+ }
97
+ });
98
+ }
64
99
  // Body: a named/singular/first example if one is defined, otherwise generate
65
100
  // a value from the schema. `Prefer: example=<name>` picks a named example.
66
101
  const selectedExample = selectResponseExample(acceptedResponse, prefer.example);
67
- const responseSchema = acceptedResponse?.schema ? getResolvedRefDeep(acceptedResponse.schema) : undefined;
68
102
  const body = selectedExample
69
103
  ? normalizeResponseBody(selectedExample.value, responseSchema)
70
104
  : responseSchema
71
- ? normalizeResponseBody(getExampleFromSchema(responseSchema, {
72
- emptyString: 'string',
73
- variables: c.req.param(),
74
- mode: 'read',
75
- }), responseSchema)
105
+ ? normalizeResponseBody(generateFromSchema(), responseSchema)
76
106
  : null;
77
107
  c.status(statusCode);
78
- return c.body(
79
- // `null` is `typeof 'object'` too, but it is not a valid XML/JSON object
80
- // root — serialize it (and any non-string primitive) with `JSON.stringify`
81
- // so a `null` example does not get fed into `json2xml`.
82
- body !== null && typeof body === 'object'
83
- ? // XML
84
- acceptedContentType?.includes('xml')
85
- ? json2xml(body)
86
- : // JSON
87
- JSON.stringify(body)
88
- : typeof body === 'string'
89
- ? // String
90
- body
91
- : // null / number / boolean
92
- JSON.stringify(body));
108
+ const serializedBody = serializeResponseBody(body, acceptedContentType, responseSchema);
109
+ // `JSON.stringify` returns `undefined` for an `undefined` body, which is an empty response.
110
+ if (serializedBody === undefined) {
111
+ return c.body(null);
112
+ }
113
+ return c.body(serializedBody);
93
114
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mock-handler-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-handler-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAoIxD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe,gMA8D3F"}
1
+ {"version":3,"file":"mock-handler-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-handler-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAqIxD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe,gMA8D3F"}
@@ -5,6 +5,7 @@ import { buildHandlerContext } from '../utils/build-handler-context.js';
5
5
  import { executeHandler } from '../utils/execute-handler.js';
6
6
  import { normalizeResponseBody } from '../utils/normalize-response-body.js';
7
7
  import { parsePreferHeader } from '../utils/parse-prefer-header.js';
8
+ import { pathParameters } from '../utils/path-parameters.js';
8
9
  import { selectResponseExample } from '../utils/select-response-example.js';
9
10
  /**
10
11
  * Get example response from OpenAPI spec for a given status code.
@@ -48,7 +49,7 @@ function getExampleFromResponse(c, statusCode, responses, exampleName) {
48
49
  : responseSchema
49
50
  ? normalizeResponseBody(getExampleFromSchema(responseSchema, {
50
51
  emptyString: 'string',
51
- variables: c.req.param(),
52
+ variables: pathParameters(c),
52
53
  mode: 'read',
53
54
  }), responseSchema)
54
55
  : null;
package/dist/types.d.ts CHANGED
@@ -10,6 +10,8 @@ export type HttpMethod = (typeof httpMethods)[number];
10
10
  type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> & {
11
11
  [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;
12
12
  }[Keys];
13
+ /** A sink for the informational log lines the mock servers print while starting up. */
14
+ export type MockServerLogger = (line: string) => void;
13
15
  type BaseMockServerOptions = {
14
16
  /**
15
17
  * The OpenAPI document to use for mocking.
@@ -43,6 +45,19 @@ type BaseMockServerOptions = {
43
45
  * always return a mock response regardless of whether the request matches the contract.
44
46
  */
45
47
  validateRequest?: boolean;
48
+ /**
49
+ * Control the informational output the server prints while starting up, which is currently the
50
+ * authentication instructions for the security schemes of the document. Defaults to logging to
51
+ * the console.
52
+ *
53
+ * Pass `false` to silence it — handy when the mock server runs inside a test harness or another
54
+ * program, where the instructions are noise — or a `(line) => void` sink to route the lines
55
+ * somewhere other than the console.
56
+ *
57
+ * Diagnostics are not affected: warnings and errors about security schemes the mock server cannot
58
+ * handle, request validator compilation errors, and `x-seed` errors are printed either way.
59
+ */
60
+ logger?: boolean | MockServerLogger;
46
61
  };
47
62
  export type MockServerOptions = RequireAtLeastOne<BaseMockServerOptions, 'specification' | 'document'>;
48
63
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,6CAA6C;AAC7C,eAAO,MAAM,WAAW,+DAAgE,CAAA;AAExF,wBAAwB;AACxB,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;AAErD;;GAEG;AACH,KAAK,iBAAiB,CAAC,CAAC,EAAE,IAAI,SAAS,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,GACzF;KACG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;CACzE,CAAC,IAAI,CAAC,CAAA;AAET,KAAK,qBAAqB,GAAG;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE5C;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEvC;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,WAAW,CAAC,eAAe,CAAA;KAAE,KAAK,IAAI,CAAA;IAExF;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,CAAC,qBAAqB,EAAE,eAAe,GAAG,UAAU,CAAC,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,6CAA6C;AAC7C,eAAO,MAAM,WAAW,+DAAgE,CAAA;AAExF,wBAAwB;AACxB,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;AAErD;;GAEG;AACH,KAAK,iBAAiB,CAAC,CAAC,EAAE,IAAI,SAAS,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,GACzF;KACG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;CACzE,CAAC,IAAI,CAAC,CAAA;AAET,uFAAuF;AACvF,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;AAErD,KAAK,qBAAqB,GAAG;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE5C;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEvC;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,WAAW,CAAC,eAAe,CAAA;KAAE,KAAK,IAAI,CAAA;IAExF;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IAEzB;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,CAAC,qBAAqB,EAAE,eAAe,GAAG,UAAU,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"build-handler-context.d.ts","sourceRoot":"","sources":["../../src/utils/build-handler-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAKnC,OAAO,EAAE,KAAK,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEjF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,cAAc,CAAC,CAAA;IAC5D,KAAK,EAAE,OAAO,KAAK,CAAA;IACnB,GAAG,EAAE;QACH,IAAI,EAAE,GAAG,CAAA;QACT,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAChC,CAAA;IACD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACzB,CAAA;AAED;;GAEG;AACH,KAAK,oBAAoB,GAAG;IAC1B,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,sBAAsB,CAAA;CACjC,CAAA;AA4DD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,CAAC,EAAE,OAAO,EACV,SAAS,CAAC,EAAE,WAAW,CAAC,eAAe,GACtC,OAAO,CAAC,oBAAoB,CAAC,CAiD/B"}
1
+ {"version":3,"file":"build-handler-context.d.ts","sourceRoot":"","sources":["../../src/utils/build-handler-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAMnC,OAAO,EAAE,KAAK,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEjF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,cAAc,CAAC,CAAA;IAC5D,KAAK,EAAE,OAAO,KAAK,CAAA;IACnB,GAAG,EAAE;QACH,IAAI,EAAE,GAAG,CAAA;QACT,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAChC,CAAA;IACD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACzB,CAAA;AAED;;GAEG;AACH,KAAK,oBAAoB,GAAG;IAC1B,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,sBAAsB,CAAA;CACjC,CAAA;AA4DD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,CAAC,EAAE,OAAO,EACV,SAAS,CAAC,EAAE,WAAW,CAAC,eAAe,GACtC,OAAO,CAAC,oBAAoB,CAAC,CAiD/B"}
@@ -5,6 +5,7 @@ import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
5
5
  import { accepts } from 'hono/accepts';
6
6
  import { store } from '../libs/store.js';
7
7
  import { normalizeResponseBody } from './normalize-response-body.js';
8
+ import { pathParameters } from './path-parameters.js';
8
9
  import { createStoreWrapper } from './store-wrapper.js';
9
10
  /**
10
11
  * Get example response from OpenAPI spec for a given status code.
@@ -42,7 +43,7 @@ function getExampleFromResponse(c, statusCode, responses) {
42
43
  : responseSchema
43
44
  ? normalizeResponseBody(getExampleFromSchema(responseSchema, {
44
45
  emptyString: 'string',
45
- variables: c.req.param(),
46
+ variables: pathParameters(c),
46
47
  mode: 'read',
47
48
  }), responseSchema)
48
49
  : null;
@@ -86,7 +87,7 @@ export async function buildHandlerContext(c, operation) {
86
87
  faker,
87
88
  req: {
88
89
  body,
89
- params: c.req.param(),
90
+ params: pathParameters(c),
90
91
  query: Object.fromEntries(new URL(c.req.url).searchParams.entries()),
91
92
  headers: Object.fromEntries(Object.entries(c.req.header()).map(([key, value]) => [key, value ?? ''])),
92
93
  },
@@ -0,0 +1,30 @@
1
+ import type { OpenAPIV3_1 } from '@scalar/openapi-types';
2
+ /** One event of a mocked Server-Sent Events response. */
3
+ type SseEvent = {
4
+ /** Already-framed event text when `framed` is true, otherwise the `data` payload of one event. */
5
+ text: string;
6
+ /** Whether `text` is SSE framing of its own, to be written as is rather than wrapped in a `data:` line. */
7
+ framed: boolean;
8
+ };
9
+ /**
10
+ * The events a `text/event-stream` response emits, in order.
11
+ *
12
+ * Examples win over the schema, mirroring `selectResponseExample`, but a stream reads them as a
13
+ * sequence rather than a single body:
14
+ * 1. A named example requested via `Prefer: example=<name>`.
15
+ * 2. The singular `example` keyword.
16
+ * 3. Every entry of the `examples` map, in declaration order — an event stream that documents a
17
+ * `summary` and a `row` example is documenting the two events it sends. A map of nothing but
18
+ * complete framing is the exception: those are alternative streams, so only the first is served.
19
+ * 4. Nothing declared: a schema-generated payload, repeated so the stream has more than one event.
20
+ *
21
+ * `generate` is a callback so the schema is only turned into an example when no example is declared.
22
+ */
23
+ export declare const collectSseEvents: (mediaType: OpenAPIV3_1.MediaTypeObject | undefined, { exampleName, generate }: {
24
+ exampleName?: string;
25
+ generate: () => unknown;
26
+ }) => SseEvent[];
27
+ /** Whether a media type is Server-Sent Events, ignoring parameters such as `; charset=utf-8`. */
28
+ export declare const isEventStreamContentType: (contentType: string | undefined) => boolean;
29
+ export {};
30
+ //# sourceMappingURL=collect-sse-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect-sse-events.d.ts","sourceRoot":"","sources":["../../src/utils/collect-sse-events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAcxD,yDAAyD;AACzD,KAAK,QAAQ,GAAG;IACd,kGAAkG;IAClG,IAAI,EAAE,MAAM,CAAA;IACZ,2GAA2G;IAC3G,MAAM,EAAE,OAAO,CAAA;CAChB,CAAA;AAgFD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAC3B,WAAW,WAAW,CAAC,eAAe,GAAG,SAAS,EAClD,2BAA2B;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,OAAO,CAAA;CAAE,KAC3E,QAAQ,EAoDV,CAAA;AAED,iGAAiG;AACjG,eAAO,MAAM,wBAAwB,GAAI,aAAa,MAAM,GAAG,SAAS,KAAG,OACf,CAAA"}