@scalar/mock-server 0.11.0 → 0.11.1

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @scalar/mock-server
2
2
 
3
+ ## 0.11.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#9518](https://github.com/scalar/scalar/pull/9518): fix: compact generated JSON mock responses so finite JSONL and NDJSON bodies stay valid as single-line events.
8
+ - [#9517](https://github.com/scalar/scalar/pull/9517): fix: improve default response selection when mocking operations with multiple response codes. The mock server now prefers the lowest 2xx success, then the lowest non-informational code, then the `default` catch-all, only falling back to a 1xx response when nothing else is defined. Status code range patterns like `2XX` are supported and treated as their lowest member, with explicit codes taking precedence over the range that covers them.
9
+ - [#9519](https://github.com/scalar/scalar/pull/9519): fix: wrap singular examples in arrays when the response schema is an array.
10
+
3
11
  ## 0.11.0
4
12
 
5
13
  ### Minor Changes
@@ -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;AAMxD;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe;;4PAiGjF"}
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"}
@@ -4,6 +4,7 @@ import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved
4
4
  import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
5
5
  import { accepts } from 'hono/accepts';
6
6
  import { findPreferredResponseKey } from '../utils/find-preferred-response-key.js';
7
+ import { normalizeResponseBody } from '../utils/normalize-response-body.js';
7
8
  import { parsePreferHeader } from '../utils/parse-prefer-header.js';
8
9
  import { selectResponseExample } from '../utils/select-response-example.js';
9
10
  /**
@@ -26,8 +27,8 @@ export function mockAnyResponse(c, operation) {
26
27
  c.status(500);
27
28
  return c.json({ error: 'No response defined for this operation.' });
28
29
  }
29
- // Status code
30
- const statusCode = Number.parseInt(responseKey === 'default' ? '200' : (responseKey ?? '200'), 10);
30
+ // Status code. `default` and range patterns like `2XX` map to their lowest concrete code (e.g. 200).
31
+ const statusCode = Number.parseInt(responseKey && responseKey !== 'default' ? responseKey.replace(/XX$/i, '00') : '200', 10);
31
32
  // Headers
32
33
  const headers = selectedResponse?.headers ?? {};
33
34
  Object.keys(headers).forEach((header) => {
@@ -63,14 +64,15 @@ export function mockAnyResponse(c, operation) {
63
64
  // Body: a named/singular/first example if one is defined, otherwise generate
64
65
  // a value from the schema. `Prefer: example=<name>` picks a named example.
65
66
  const selectedExample = selectResponseExample(acceptedResponse, prefer.example);
67
+ const responseSchema = acceptedResponse?.schema ? getResolvedRefDeep(acceptedResponse.schema) : undefined;
66
68
  const body = selectedExample
67
- ? selectedExample.value
68
- : acceptedResponse?.schema
69
- ? getExampleFromSchema(getResolvedRefDeep(acceptedResponse.schema), {
69
+ ? normalizeResponseBody(selectedExample.value, responseSchema)
70
+ : responseSchema
71
+ ? normalizeResponseBody(getExampleFromSchema(responseSchema, {
70
72
  emptyString: 'string',
71
73
  variables: c.req.param(),
72
74
  mode: 'read',
73
- })
75
+ }), responseSchema)
74
76
  : null;
75
77
  c.status(statusCode);
76
78
  return c.body(
@@ -82,7 +84,7 @@ export function mockAnyResponse(c, operation) {
82
84
  acceptedContentType?.includes('xml')
83
85
  ? json2xml(body)
84
86
  : // JSON
85
- JSON.stringify(body, null, 2)
87
+ JSON.stringify(body)
86
88
  : typeof body === 'string'
87
89
  ? // String
88
90
  body
@@ -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;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AA8HxD;;;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;AAoIxD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe,gMA8D3F"}
@@ -1,9 +1,9 @@
1
1
  import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
- import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved-ref-deep';
3
2
  import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
4
3
  import { accepts } from 'hono/accepts';
5
4
  import { buildHandlerContext } from '../utils/build-handler-context.js';
6
5
  import { executeHandler } from '../utils/execute-handler.js';
6
+ import { normalizeResponseBody } from '../utils/normalize-response-body.js';
7
7
  import { parsePreferHeader } from '../utils/parse-prefer-header.js';
8
8
  import { selectResponseExample } from '../utils/select-response-example.js';
9
9
  /**
@@ -40,16 +40,17 @@ function getExampleFromResponse(c, statusCode, responses, exampleName) {
40
40
  if (!acceptedResponse) {
41
41
  return null;
42
42
  }
43
+ const responseSchema = acceptedResponse.schema ? getResolvedRef(acceptedResponse.schema) : undefined;
43
44
  // Extract example (named, singular, or first) or generate from schema
44
45
  const selectedExample = selectResponseExample(acceptedResponse, exampleName);
45
46
  return selectedExample
46
- ? selectedExample.value
47
- : acceptedResponse.schema
48
- ? getExampleFromSchema(getResolvedRefDeep(acceptedResponse.schema), {
47
+ ? normalizeResponseBody(selectedExample.value, responseSchema)
48
+ : responseSchema
49
+ ? normalizeResponseBody(getExampleFromSchema(responseSchema, {
49
50
  emptyString: 'string',
50
51
  variables: c.req.param(),
51
52
  mode: 'read',
52
- })
53
+ }), responseSchema)
53
54
  : null;
54
55
  }
55
56
  /**
@@ -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;AAInC,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;AAuDD;;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;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"}
@@ -4,6 +4,7 @@ import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved
4
4
  import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
5
5
  import { accepts } from 'hono/accepts';
6
6
  import { store } from '../libs/store.js';
7
+ import { normalizeResponseBody } from './normalize-response-body.js';
7
8
  import { createStoreWrapper } from './store-wrapper.js';
8
9
  /**
9
10
  * Get example response from OpenAPI spec for a given status code.
@@ -34,15 +35,16 @@ function getExampleFromResponse(c, statusCode, responses) {
34
35
  if (!acceptedResponse) {
35
36
  return null;
36
37
  }
38
+ const responseSchema = acceptedResponse.schema ? getResolvedRefDeep(acceptedResponse.schema) : undefined;
37
39
  // Extract example from example property or generate from schema
38
40
  return acceptedResponse.example !== undefined
39
- ? acceptedResponse.example
40
- : acceptedResponse.schema
41
- ? getExampleFromSchema(getResolvedRefDeep(acceptedResponse.schema), {
41
+ ? normalizeResponseBody(acceptedResponse.example, responseSchema)
42
+ : responseSchema
43
+ ? normalizeResponseBody(getExampleFromSchema(responseSchema, {
42
44
  emptyString: 'string',
43
45
  variables: c.req.param(),
44
46
  mode: 'read',
45
- })
47
+ }), responseSchema)
46
48
  : null;
47
49
  }
48
50
  /**
@@ -1,5 +1,18 @@
1
1
  /**
2
- * Find the preferred response key: default, 200, 201 …
2
+ * Find the preferred response key to mock.
3
+ *
4
+ * Preference order:
5
+ * 1. The lowest 2xx success response
6
+ * 2. The lowest non-informational code (3xx/4xx/5xx)
7
+ * 3. `default` — the catch-all for undeclared responses (typically an error)
8
+ * 4. An informational 1xx response, only when nothing else is defined
9
+ *
10
+ * Within each tier an explicit code wins over the range pattern that covers it (e.g. `200` over
11
+ * `2XX`), and range patterns are treated as their lowest member (e.g. `2XX` → `200`). `default`
12
+ * is the catch-all for codes not covered individually, so a defined success or error is preferred
13
+ * over it.
14
+ *
15
+ * @see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.2.md#responses-object
3
16
  */
4
17
  export declare function findPreferredResponseKey(responses?: string[]): string | undefined;
5
18
  //# sourceMappingURL=find-preferred-response-key.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"find-preferred-response-key.d.ts","sourceRoot":"","sources":["../../src/utils/find-preferred-response-key.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,sBAQ5D"}
1
+ {"version":3,"file":"find-preferred-response-key.d.ts","sourceRoot":"","sources":["../../src/utils/find-preferred-response-key.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAiBjF"}
@@ -1,11 +1,45 @@
1
+ /** Matches an explicit HTTP status code (e.g. `200`) or a range pattern (e.g. `2XX`). */
2
+ const STATUS_CODE_REGEX = /^[1-5](\d{2}|XX)$/i;
3
+ /** Whether a status key is a range pattern (e.g. `2XX`) rather than an explicit code. */
4
+ const isRange = (response) => /XX$/i.test(response);
5
+ /** Whether a status key is an informational 1xx response. */
6
+ const isInformational = (response) => response.startsWith('1');
7
+ /** Whether a status key is a 2xx success response. */
8
+ const isSuccess = (response) => response.startsWith('2');
9
+ /** Turn a status key into a comparable number, treating a range like `2XX` as its lowest member (`200`). */
10
+ const toComparableStatus = (response) => Number.parseInt(response.replace(/XX$/i, '00'), 10);
11
+ const sortStatusCodes = (responses) => responses
12
+ .filter((response) => STATUS_CODE_REGEX.test(response))
13
+ .sort((left, right) => {
14
+ const difference = toComparableStatus(left) - toComparableStatus(right);
15
+ // On a tie, prefer an explicit code over a range pattern (e.g. `200` before `2XX`).
16
+ return difference !== 0 ? difference : Number(isRange(left)) - Number(isRange(right));
17
+ });
1
18
  /**
2
- * Find the preferred response key: default, 200, 201 …
19
+ * Find the preferred response key to mock.
20
+ *
21
+ * Preference order:
22
+ * 1. The lowest 2xx success response
23
+ * 2. The lowest non-informational code (3xx/4xx/5xx)
24
+ * 3. `default` — the catch-all for undeclared responses (typically an error)
25
+ * 4. An informational 1xx response, only when nothing else is defined
26
+ *
27
+ * Within each tier an explicit code wins over the range pattern that covers it (e.g. `200` over
28
+ * `2XX`), and range patterns are treated as their lowest member (e.g. `2XX` → `200`). `default`
29
+ * is the catch-all for codes not covered individually, so a defined success or error is preferred
30
+ * over it.
31
+ *
32
+ * @see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.2.md#responses-object
3
33
  */
4
34
  export function findPreferredResponseKey(responses) {
5
- return (
6
- // Regular status codes
7
- ['default', '200', '201', '204', '404', '500'].find((key) => responses?.includes(key) ?? false) ??
8
- // Lowest status code
9
- responses?.sort()[0] ??
10
- undefined);
35
+ if (!responses?.length) {
36
+ return undefined;
37
+ }
38
+ const statusCodes = sortStatusCodes(responses);
39
+ // Within a tier, prefer an explicit status code over the range pattern that covers it.
40
+ const byPreference = (predicate) => statusCodes.find((response) => predicate(response) && !isRange(response)) ?? statusCodes.find(predicate);
41
+ return (byPreference(isSuccess) ??
42
+ byPreference((response) => !isInformational(response)) ??
43
+ (responses.includes('default') ? 'default' : undefined) ??
44
+ byPreference(isInformational));
11
45
  }
@@ -0,0 +1,5 @@
1
+ import type { OpenAPIV3_1 } from '@scalar/openapi-types';
2
+ type Schema = NonNullable<OpenAPIV3_1.ComponentsObject['schemas']>[string];
3
+ export declare const normalizeResponseBody: (body: unknown, schema: Schema) => unknown;
4
+ export {};
5
+ //# sourceMappingURL=normalize-response-body.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-response-body.d.ts","sourceRoot":"","sources":["../../src/utils/normalize-response-body.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,KAAK,MAAM,GAAG,WAAW,CAAC,WAAW,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AA0B1E,eAAO,MAAM,qBAAqB,GAAI,MAAM,OAAO,EAAE,QAAQ,MAAM,KAAG,OAMrE,CAAA"}
@@ -0,0 +1,25 @@
1
+ import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
+ const isArrayResponseSchema = (schema) => {
3
+ if (!schema) {
4
+ return false;
5
+ }
6
+ const resolvedSchema = getResolvedRef(schema);
7
+ if (!resolvedSchema || typeof resolvedSchema !== 'object') {
8
+ return false;
9
+ }
10
+ if ('type' in resolvedSchema) {
11
+ if (resolvedSchema.type === 'array') {
12
+ return true;
13
+ }
14
+ if (Array.isArray(resolvedSchema.type) && resolvedSchema.type.includes('array')) {
15
+ return true;
16
+ }
17
+ }
18
+ return 'items' in resolvedSchema;
19
+ };
20
+ export const normalizeResponseBody = (body, schema) => {
21
+ if (body === null || body === undefined || Array.isArray(body) || !isArrayResponseSchema(schema)) {
22
+ return body;
23
+ }
24
+ return [body];
25
+ };
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "swagger",
17
17
  "cli"
18
18
  ],
19
- "version": "0.11.0",
19
+ "version": "0.11.1",
20
20
  "engines": {
21
21
  "node": ">=22"
22
22
  },
@@ -57,10 +57,10 @@
57
57
  "hono": "^4.12.7",
58
58
  "yaml": "^2.8.3",
59
59
  "@scalar/helpers": "0.8.2",
60
- "@scalar/json-magic": "0.12.16",
61
60
  "@scalar/openapi-types": "0.9.1",
61
+ "@scalar/json-magic": "0.12.16",
62
62
  "@scalar/openapi-upgrader": "0.2.9",
63
- "@scalar/workspace-store": "0.54.3"
63
+ "@scalar/workspace-store": "0.54.4"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/node": "^24.1.0",