@kubb/plugin-msw 6.2.0-canary.20261111T122007 → 6.2.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/dist/index.cjs +365 -210
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +366 -211
- package/dist/index.js.map +1 -1
- package/docs/guides/mock-api.md +17 -12
- package/docs/options.md +14 -8
- package/docs/overview.md +1 -1
- package/kubb-extension.json +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,8 +1,240 @@
|
|
|
1
|
-
import { Resolver, Url, addGenerator, ast,
|
|
1
|
+
import { Resolver, Url, addGenerator, ast, casing, createResolver, defineExtension, defineGenerator, setMacros, setResolver, text } from "@kubb/kit";
|
|
2
2
|
import { pluginFakerName } from "@kubb/plugin-faker";
|
|
3
3
|
import { enumTypeName, pluginTsName } from "@kubb/plugin-ts";
|
|
4
4
|
import { File, Function, jsxRenderer } from "@kubb/renderer-jsx";
|
|
5
5
|
import { jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
|
|
6
|
+
// kubb-extension.json
|
|
7
|
+
var compatibility = {
|
|
8
|
+
"kubb": ">=5.18.4",
|
|
9
|
+
"node": ">=22.22"
|
|
10
|
+
};
|
|
11
|
+
// package.json
|
|
12
|
+
var version = "6.2.1";
|
|
13
|
+
// src/generators/handlersGenerator.ts
|
|
14
|
+
/**
|
|
15
|
+
* Aggregate generator enabled by `pluginMsw({ handlers: true })`. Emits a
|
|
16
|
+
* `handlers.ts` file that re-exports every generated handler in operation
|
|
17
|
+
* order, ready to spread into `setupServer(...handlers)` or
|
|
18
|
+
* `setupWorker(...handlers)`.
|
|
19
|
+
*/
|
|
20
|
+
const handlersGenerator = defineGenerator({
|
|
21
|
+
name: "plugin-msw",
|
|
22
|
+
operations(nodes, ctx) {
|
|
23
|
+
const { resolver, config, root } = ctx;
|
|
24
|
+
const { output, group } = ctx.options;
|
|
25
|
+
const handlersName = resolver.handler.listName();
|
|
26
|
+
const createHandlersName = resolver.resolveName({
|
|
27
|
+
key: ["handlers", "create"],
|
|
28
|
+
name: casing.camel(handlersName, { prefix: "create" })
|
|
29
|
+
});
|
|
30
|
+
const file = resolver.file({
|
|
31
|
+
name: handlersName,
|
|
32
|
+
extname: ".ts",
|
|
33
|
+
root,
|
|
34
|
+
output,
|
|
35
|
+
group: group ?? void 0
|
|
36
|
+
});
|
|
37
|
+
const handlerNames = nodes.map((node) => resolver.handler.name(node));
|
|
38
|
+
const imports = nodes.map((node, index) => {
|
|
39
|
+
const operationName = handlerNames[index];
|
|
40
|
+
const operationFile = resolver.file({
|
|
41
|
+
name: resolver.name(node.operationId),
|
|
42
|
+
extname: ".ts",
|
|
43
|
+
tag: node.tags[0] ?? "default",
|
|
44
|
+
path: node.path,
|
|
45
|
+
root,
|
|
46
|
+
output,
|
|
47
|
+
group: group ?? void 0
|
|
48
|
+
});
|
|
49
|
+
return ast.factory.createImport({
|
|
50
|
+
name: [operationName],
|
|
51
|
+
root: file.path,
|
|
52
|
+
path: operationFile.path
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
const handlers = handlerNames.map((name) => `${name}(undefined, options)`);
|
|
56
|
+
return [ast.factory.createFile({
|
|
57
|
+
baseName: file.baseName,
|
|
58
|
+
path: file.path,
|
|
59
|
+
meta: file.meta,
|
|
60
|
+
banner: resolver.default.banner(ctx.meta, {
|
|
61
|
+
output,
|
|
62
|
+
config,
|
|
63
|
+
file: {
|
|
64
|
+
path: file.path,
|
|
65
|
+
baseName: file.baseName
|
|
66
|
+
}
|
|
67
|
+
}),
|
|
68
|
+
footer: resolver.default.footer(ctx.meta, {
|
|
69
|
+
output,
|
|
70
|
+
config,
|
|
71
|
+
file: {
|
|
72
|
+
path: file.path,
|
|
73
|
+
baseName: file.baseName
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
imports,
|
|
77
|
+
sources: [ast.factory.createSource({
|
|
78
|
+
name: createHandlersName,
|
|
79
|
+
isIndexable: true,
|
|
80
|
+
isExportable: true,
|
|
81
|
+
nodes: [ast.factory.createText(`export function ${createHandlersName}(options?: { baseURL?: string }) {
|
|
82
|
+
return [${handlers.join(", ")}] as const
|
|
83
|
+
}`)]
|
|
84
|
+
}), ast.factory.createSource({
|
|
85
|
+
name: handlersName,
|
|
86
|
+
isIndexable: true,
|
|
87
|
+
isExportable: true,
|
|
88
|
+
nodes: [ast.factory.createText(`export const ${handlersName} = ${createHandlersName}()`)]
|
|
89
|
+
})]
|
|
90
|
+
})];
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// ../../internals/utils/src/reserved.ts
|
|
94
|
+
/**
|
|
95
|
+
* JavaScript and Java reserved words.
|
|
96
|
+
* @link https://github.com/jonschlinkert/reserved/blob/master/index.js
|
|
97
|
+
*/
|
|
98
|
+
const reservedWords = /* @__PURE__ */ new Set([
|
|
99
|
+
"abstract",
|
|
100
|
+
"arguments",
|
|
101
|
+
"await",
|
|
102
|
+
"boolean",
|
|
103
|
+
"break",
|
|
104
|
+
"byte",
|
|
105
|
+
"case",
|
|
106
|
+
"catch",
|
|
107
|
+
"char",
|
|
108
|
+
"class",
|
|
109
|
+
"const",
|
|
110
|
+
"continue",
|
|
111
|
+
"debugger",
|
|
112
|
+
"default",
|
|
113
|
+
"delete",
|
|
114
|
+
"do",
|
|
115
|
+
"double",
|
|
116
|
+
"else",
|
|
117
|
+
"enum",
|
|
118
|
+
"eval",
|
|
119
|
+
"export",
|
|
120
|
+
"extends",
|
|
121
|
+
"false",
|
|
122
|
+
"final",
|
|
123
|
+
"finally",
|
|
124
|
+
"float",
|
|
125
|
+
"for",
|
|
126
|
+
"function",
|
|
127
|
+
"goto",
|
|
128
|
+
"if",
|
|
129
|
+
"implements",
|
|
130
|
+
"import",
|
|
131
|
+
"in",
|
|
132
|
+
"instanceof",
|
|
133
|
+
"int",
|
|
134
|
+
"interface",
|
|
135
|
+
"let",
|
|
136
|
+
"long",
|
|
137
|
+
"native",
|
|
138
|
+
"new",
|
|
139
|
+
"null",
|
|
140
|
+
"package",
|
|
141
|
+
"private",
|
|
142
|
+
"protected",
|
|
143
|
+
"public",
|
|
144
|
+
"return",
|
|
145
|
+
"short",
|
|
146
|
+
"static",
|
|
147
|
+
"super",
|
|
148
|
+
"switch",
|
|
149
|
+
"synchronized",
|
|
150
|
+
"this",
|
|
151
|
+
"throw",
|
|
152
|
+
"throws",
|
|
153
|
+
"transient",
|
|
154
|
+
"true",
|
|
155
|
+
"try",
|
|
156
|
+
"typeof",
|
|
157
|
+
"var",
|
|
158
|
+
"void",
|
|
159
|
+
"volatile",
|
|
160
|
+
"while",
|
|
161
|
+
"with",
|
|
162
|
+
"yield",
|
|
163
|
+
"Array",
|
|
164
|
+
"Date",
|
|
165
|
+
"hasOwnProperty",
|
|
166
|
+
"Infinity",
|
|
167
|
+
"isFinite",
|
|
168
|
+
"isNaN",
|
|
169
|
+
"isPrototypeOf",
|
|
170
|
+
"length",
|
|
171
|
+
"Math",
|
|
172
|
+
"name",
|
|
173
|
+
"NaN",
|
|
174
|
+
"Number",
|
|
175
|
+
"Object",
|
|
176
|
+
"prototype",
|
|
177
|
+
"String",
|
|
178
|
+
"toString",
|
|
179
|
+
"undefined",
|
|
180
|
+
"valueOf"
|
|
181
|
+
]);
|
|
182
|
+
/**
|
|
183
|
+
* Returns `true` when `name` is a syntactically valid JavaScript variable name.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* isValidVarName('status') // true
|
|
188
|
+
* isValidVarName('class') // false (reserved word)
|
|
189
|
+
* isValidVarName('42foo') // false (starts with digit)
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
function isValidVarName(name) {
|
|
193
|
+
if (!name || reservedWords.has(name)) return false;
|
|
194
|
+
return isIdentifier(name);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
|
|
198
|
+
*
|
|
199
|
+
* Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
|
|
200
|
+
* even though they are not valid variable names, so use this (not {@link isValidVarName}) when
|
|
201
|
+
* deciding whether an object key needs quoting.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```ts
|
|
205
|
+
* isIdentifier('name') // true
|
|
206
|
+
* isIdentifier('x-total')// false
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
function isIdentifier(name) {
|
|
210
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
211
|
+
}
|
|
212
|
+
// ../../internals/utils/src/codegen.ts
|
|
213
|
+
/**
|
|
214
|
+
* An object key, quoted only when it is not identifier-shaped. Reserved words such as `class` stay bare.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* objectKey('x-total') // "'x-total'"
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
function objectKey(name) {
|
|
222
|
+
return isIdentifier(name) ? name : text.quote(name);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* A JSON value as a JavaScript literal, with single-quoted strings and identifier keys left unquoted.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* toLiteral({ id: 1, 'x-tag': ['a'] }) // "{ id: 1, 'x-tag': ['a'] }"
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
function toLiteral(value) {
|
|
233
|
+
if (typeof value === "string") return text.quote(value);
|
|
234
|
+
if (Array.isArray(value)) return `[${value.map(toLiteral).join(", ")}]`;
|
|
235
|
+
if (value && typeof value === "object") return `{ ${Object.entries(value).map(([key, item]) => `${objectKey(key)}: ${toLiteral(item)}`).join(", ")} }`;
|
|
236
|
+
return String(value);
|
|
237
|
+
}
|
|
6
238
|
// ../../internals/shared/src/operation.ts
|
|
7
239
|
/**
|
|
8
240
|
* Builds the `ResolverFileParams` every operation generator passes to
|
|
@@ -68,19 +300,12 @@ function resolveOperationFile(options) {
|
|
|
68
300
|
return file;
|
|
69
301
|
});
|
|
70
302
|
}
|
|
71
|
-
function baseContentType(contentType) {
|
|
72
|
-
return contentType.split(";")[0].trim().toLowerCase();
|
|
73
|
-
}
|
|
74
303
|
/**
|
|
75
|
-
*
|
|
76
|
-
* `application/x-ndjson`, `application/json-seq`, or any media type with an OpenAPI 3.2 `itemSchema`.
|
|
304
|
+
* How a content entry is framed as a stream: `'events'` for Server-Sent Events, `'lines'` for JSON Lines.
|
|
77
305
|
*/
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
function isJsonContentType(baseType) {
|
|
83
|
-
return baseType === "application/json" || baseType.endsWith("+json") || baseType === "text/json";
|
|
306
|
+
function getStreamFraming(entry) {
|
|
307
|
+
if (ast.mediaType.getBase(entry) === "text/event-stream") return "events";
|
|
308
|
+
return entry.stream && ast.mediaType.isStream(entry) ? "lines" : void 0;
|
|
84
309
|
}
|
|
85
310
|
/**
|
|
86
311
|
* Parser for one response content entry.
|
|
@@ -91,10 +316,10 @@ function isJsonContentType(baseType) {
|
|
|
91
316
|
* `image/*`, `audio/*`, and `video/*` are blobs.
|
|
92
317
|
*/
|
|
93
318
|
function responseTypeForContent(entry) {
|
|
94
|
-
const baseType =
|
|
319
|
+
const baseType = ast.mediaType.getBase(entry);
|
|
95
320
|
if (!baseType) return void 0;
|
|
96
|
-
if (
|
|
97
|
-
if (
|
|
321
|
+
if (getStreamFraming(entry)) return "stream";
|
|
322
|
+
if (ast.mediaType.isJson(baseType)) return void 0;
|
|
98
323
|
if (entry.schema?.type === "blob") return "blob";
|
|
99
324
|
if (baseType.startsWith("text/")) return "text";
|
|
100
325
|
if (isBinaryContentType(baseType)) return "blob";
|
|
@@ -111,14 +336,14 @@ function isBinaryContentType(baseType) {
|
|
|
111
336
|
function toResponseContent(entry, entries = [entry]) {
|
|
112
337
|
const { contentType, schema } = entry;
|
|
113
338
|
if (!schema || entry.stream) return entry;
|
|
114
|
-
const baseType =
|
|
115
|
-
const resolved = ast.
|
|
339
|
+
const baseType = ast.mediaType.getBase(contentType);
|
|
340
|
+
const resolved = ast.ref.getSchema(schema);
|
|
116
341
|
if (isBinaryContentType(baseType)) return resolved.type === "blob" ? entry : {
|
|
117
342
|
...entry,
|
|
118
343
|
schema: ast.factory.createSchema({ type: "blob" }),
|
|
119
344
|
keysToOmit: null
|
|
120
345
|
};
|
|
121
|
-
const isPlainMediaType = Boolean(baseType) && !
|
|
346
|
+
const isPlainMediaType = Boolean(baseType) && !ast.mediaType.isJson(baseType) && !baseType.includes("*");
|
|
122
347
|
if (resolved.type === "blob") return isPlainMediaType && !entries.every((item) => item.schema?.type === "blob") ? {
|
|
123
348
|
...entry,
|
|
124
349
|
schema: ast.factory.createSchema({ type: "string" }),
|
|
@@ -134,36 +359,6 @@ function toResponseContent(entry, entries = [entry]) {
|
|
|
134
359
|
keysToOmit: null
|
|
135
360
|
} : entry;
|
|
136
361
|
}
|
|
137
|
-
function getStatusCodeNumber(statusCode) {
|
|
138
|
-
const code = Number(statusCode);
|
|
139
|
-
return Number.isNaN(code) ? null : code;
|
|
140
|
-
}
|
|
141
|
-
function isSuccessStatusCode(statusCode) {
|
|
142
|
-
const code = getStatusCodeNumber(statusCode);
|
|
143
|
-
return code !== null && code >= 200 && code < 300;
|
|
144
|
-
}
|
|
145
|
-
function getSuccessResponses(responses) {
|
|
146
|
-
return responses.filter((response) => isSuccessStatusCode(response.statusCode));
|
|
147
|
-
}
|
|
148
|
-
function getOperationSuccessResponses(node) {
|
|
149
|
-
return getSuccessResponses(node.responses);
|
|
150
|
-
}
|
|
151
|
-
function getPrimarySuccessResponse(node) {
|
|
152
|
-
return getOperationSuccessResponses(node)[0] ?? null;
|
|
153
|
-
}
|
|
154
|
-
function resolveResponseTypes(node, resolver) {
|
|
155
|
-
const types = [];
|
|
156
|
-
for (const response of node.responses) {
|
|
157
|
-
if (response.statusCode === "default") {
|
|
158
|
-
types.push(["default", resolver.response.response(node)]);
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
const code = getStatusCodeNumber(response.statusCode);
|
|
162
|
-
if (code === null) continue;
|
|
163
|
-
types.push([code, isSuccessStatusCode(code) ? resolver.response.response(node) : resolver.response.status(node, response.statusCode)]);
|
|
164
|
-
}
|
|
165
|
-
return types;
|
|
166
|
-
}
|
|
167
362
|
// ../../internals/shared/src/resolver.ts
|
|
168
363
|
/**
|
|
169
364
|
* Builds the allocation key for a declaration an operation owns. `scope` names which declaration
|
|
@@ -191,124 +386,25 @@ function operationKey(node, ...scope) {
|
|
|
191
386
|
function getEnumNames(ctx) {
|
|
192
387
|
return ctx.memo("enumNames:set", () => new Set((ctx.driver.inputNode?.schemas ?? []).flatMap((schema) => schema.type === "enum" && schema.name ? [schema.name] : [])));
|
|
193
388
|
}
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
* - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
|
|
200
|
-
* - other groups use the camelCased group (`pet store` → `petStore`).
|
|
201
|
-
*
|
|
202
|
-
* A user-provided `group.name` always wins over the default namer, so callers stay in
|
|
203
|
-
* control of their output folders. Returns `null` when grouping is disabled, matching the
|
|
204
|
-
* per-plugin convention.
|
|
205
|
-
*
|
|
206
|
-
* @param group - The user-supplied group option, or `undefined` to disable grouping.
|
|
207
|
-
*
|
|
208
|
-
* @example
|
|
209
|
-
* ```ts
|
|
210
|
-
* createGroupConfig(group) // shared across every plugin
|
|
211
|
-
* ```
|
|
212
|
-
*/
|
|
213
|
-
function createGroupConfig(group) {
|
|
214
|
-
if (!group) return null;
|
|
215
|
-
const defaultName = (ctx) => {
|
|
216
|
-
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
|
|
217
|
-
return camelCase(ctx.group);
|
|
218
|
-
};
|
|
219
|
-
return {
|
|
220
|
-
...group,
|
|
221
|
-
name: group.name ? group.name : defaultName
|
|
222
|
-
};
|
|
389
|
+
// src/utils.ts
|
|
390
|
+
function toSimpleStyle(value) {
|
|
391
|
+
if (Array.isArray(value)) return value.join(",");
|
|
392
|
+
if (typeof value === "object" && value !== null) return Object.entries(value).flat().join(",");
|
|
393
|
+
return String(value);
|
|
223
394
|
}
|
|
224
|
-
// kubb-extension.json
|
|
225
|
-
var compatibility = {
|
|
226
|
-
"kubb": ">=5.16.0",
|
|
227
|
-
"node": ">=22.22"
|
|
228
|
-
};
|
|
229
|
-
// package.json
|
|
230
|
-
var version = "6.2.0-canary.20261111T122007";
|
|
231
|
-
// src/generators/handlersGenerator.ts
|
|
232
395
|
/**
|
|
233
|
-
*
|
|
234
|
-
* `handlers.ts` file that re-exports every generated handler in operation
|
|
235
|
-
* order, ready to spread into `setupServer(...handlers)` or
|
|
236
|
-
* `setupWorker(...handlers)`.
|
|
396
|
+
* The declared headers a mock sends, each with its example's `serializedValue` or `value`, else its schema's `default` or first example.
|
|
237
397
|
*/
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const file = resolver.file({
|
|
249
|
-
name: handlersName,
|
|
250
|
-
extname: ".ts",
|
|
251
|
-
root,
|
|
252
|
-
output,
|
|
253
|
-
group: group ?? void 0
|
|
254
|
-
});
|
|
255
|
-
const handlerNames = nodes.map((node) => resolver.handler.name(node));
|
|
256
|
-
const imports = nodes.map((node, index) => {
|
|
257
|
-
const operationName = handlerNames[index];
|
|
258
|
-
const operationFile = resolver.file({
|
|
259
|
-
name: resolver.name(node.operationId),
|
|
260
|
-
extname: ".ts",
|
|
261
|
-
tag: node.tags[0] ?? "default",
|
|
262
|
-
path: node.path,
|
|
263
|
-
root,
|
|
264
|
-
output,
|
|
265
|
-
group: group ?? void 0
|
|
266
|
-
});
|
|
267
|
-
return ast.factory.createImport({
|
|
268
|
-
name: [operationName],
|
|
269
|
-
root: file.path,
|
|
270
|
-
path: operationFile.path
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
const handlers = handlerNames.map((name) => `${name}(undefined, options)`);
|
|
274
|
-
return [ast.factory.createFile({
|
|
275
|
-
baseName: file.baseName,
|
|
276
|
-
path: file.path,
|
|
277
|
-
meta: file.meta,
|
|
278
|
-
banner: resolver.default.banner(ctx.meta, {
|
|
279
|
-
output,
|
|
280
|
-
config,
|
|
281
|
-
file: {
|
|
282
|
-
path: file.path,
|
|
283
|
-
baseName: file.baseName
|
|
284
|
-
}
|
|
285
|
-
}),
|
|
286
|
-
footer: resolver.default.footer(ctx.meta, {
|
|
287
|
-
output,
|
|
288
|
-
config,
|
|
289
|
-
file: {
|
|
290
|
-
path: file.path,
|
|
291
|
-
baseName: file.baseName
|
|
292
|
-
}
|
|
293
|
-
}),
|
|
294
|
-
imports,
|
|
295
|
-
sources: [ast.factory.createSource({
|
|
296
|
-
name: createHandlersName,
|
|
297
|
-
isIndexable: true,
|
|
298
|
-
isExportable: true,
|
|
299
|
-
nodes: [ast.factory.createText(`export function ${createHandlersName}(options?: { baseURL?: string }) {
|
|
300
|
-
return [${handlers.join(", ")}] as const
|
|
301
|
-
}`)]
|
|
302
|
-
}), ast.factory.createSource({
|
|
303
|
-
name: handlersName,
|
|
304
|
-
isIndexable: true,
|
|
305
|
-
isExportable: true,
|
|
306
|
-
nodes: [ast.factory.createText(`export const ${handlersName} = ${createHandlersName}()`)]
|
|
307
|
-
})]
|
|
308
|
-
})];
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
// src/utils.ts
|
|
398
|
+
function getMockHeaders(response) {
|
|
399
|
+
return (response?.headers ?? []).flatMap((header) => {
|
|
400
|
+
if (header.name.toLowerCase() === "content-type") return [];
|
|
401
|
+
const example = header.examples?.find((entry) => entry.serializedValue !== void 0 || entry.value !== void 0);
|
|
402
|
+
const headerValue = example?.serializedValue ?? example?.value ?? header.schema.default ?? header.schema.examples?.[0];
|
|
403
|
+
if (headerValue === void 0 || headerValue === null) return [];
|
|
404
|
+
const headerText = toSimpleStyle(headerValue);
|
|
405
|
+
return /^[\t\x20-\x7E\x80-\xFF]*$/.test(headerText) ? [[header.name, headerText]] : [];
|
|
406
|
+
});
|
|
407
|
+
}
|
|
312
408
|
/**
|
|
313
409
|
* Returns the base URL as source code. A `${...}` value stays a template literal.
|
|
314
410
|
*/
|
|
@@ -336,13 +432,12 @@ function hasResponseSchema(response) {
|
|
|
336
432
|
* content types, JSON is preferred (the faker mock body is JSON), otherwise the first declared type.
|
|
337
433
|
*/
|
|
338
434
|
function getResponseContentType(response) {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
435
|
+
const contentType = getResponseEntry(response)?.contentType;
|
|
436
|
+
return typeof contentType === "string" && contentType.length > 0 ? contentType : null;
|
|
342
437
|
}
|
|
343
|
-
function
|
|
344
|
-
const
|
|
345
|
-
return
|
|
438
|
+
function getResponseEntry(response) {
|
|
439
|
+
const contents = response?.content ?? [];
|
|
440
|
+
return contents.find((entry) => ast.mediaType.isJson(entry)) ?? contents[0];
|
|
346
441
|
}
|
|
347
442
|
/**
|
|
348
443
|
* The first content type of a response that the client reads as text, such as `application/xml`, or `null` when none is.
|
|
@@ -350,49 +445,109 @@ function isJson(contentType) {
|
|
|
350
445
|
function getTextContentType(response) {
|
|
351
446
|
return (response?.content ?? []).find((entry) => {
|
|
352
447
|
const schema = toResponseContent(entry, response?.content).schema;
|
|
353
|
-
return !isJson(entry
|
|
448
|
+
return !ast.mediaType.isJson(entry) && schema && ast.ref.getSchema(schema).primitive === "string";
|
|
354
449
|
})?.contentType ?? null;
|
|
355
450
|
}
|
|
451
|
+
const bigintReplacer = `(_key, value) => {
|
|
452
|
+
if (typeof value !== 'bigint') return value
|
|
453
|
+
if (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER) throw new TypeError(\`Cannot serialize \${value}n as JSON without losing precision.\`)
|
|
454
|
+
return Number(value)
|
|
455
|
+
}`;
|
|
456
|
+
/** The `JSON.stringify` call for `body`, with a replacer when the response holds an `int64`, which `JSON.stringify` throws on. */
|
|
457
|
+
function stringify(body, response) {
|
|
458
|
+
return (response?.content ?? []).some((entry) => entry.schema && ast.schema.find(entry.schema, {
|
|
459
|
+
predicate: (schema) => schema.type === "bigint",
|
|
460
|
+
followRefs: true
|
|
461
|
+
}) !== void 0) ? `JSON.stringify(${body}, ${bigintReplacer})` : `JSON.stringify(${body})`;
|
|
462
|
+
}
|
|
356
463
|
/**
|
|
357
464
|
* The statements that return a mocked `Response` for `body`. A string body for a response the client reads as text goes
|
|
358
465
|
* out as raw text with that content type, so an XML mock is not sent as a quoted JSON string. Any other body is JSON.
|
|
359
466
|
*/
|
|
360
467
|
function responseReturn(body, status, response) {
|
|
361
468
|
const contentType = getContentType(response);
|
|
469
|
+
const headers = getMockHeaders(response);
|
|
470
|
+
const name = isValidVarName(body) ? body : "body";
|
|
471
|
+
const prelude = name === body ? "" : `const body = ${body}\n `;
|
|
472
|
+
if (contentType && ast.mediaType.getBase(contentType) === "text/event-stream") {
|
|
473
|
+
const entry = getResponseEntry(response);
|
|
474
|
+
const serialized = entry?.examples?.find((example) => example.serializedValue !== void 0)?.serializedValue;
|
|
475
|
+
const fallback = serialized === void 0 ? "''" : toLiteral(serialized);
|
|
476
|
+
if (entry?.stream?.shape !== "event") return `${prelude}return ${responseLiteral(`${name} === undefined ? ${fallback} : \`\${${eventDataLines(name, response)}}\\n\\n\``, status, contentType, headers)}`;
|
|
477
|
+
return `${prelude}const event = ${name} as { event?: unknown; id?: unknown; retry?: unknown; data?: unknown } | undefined
|
|
478
|
+
const lines = [
|
|
479
|
+
...(['event', 'id', 'retry'] as const).filter((field) => event?.[field] !== undefined).map((field) => \`\${field}: \${event?.[field]}\`),
|
|
480
|
+
// A client dispatches an event only once it has a \`data\` line, so an event without data still sends an empty one.
|
|
481
|
+
event?.data === undefined ? 'data:' : ${eventDataLines("event.data", response)},
|
|
482
|
+
]
|
|
483
|
+
|
|
484
|
+
return ${responseLiteral(`event === undefined ? ${fallback} : \`\${lines.join('\\n')}\\n\\n\``, status, contentType, headers)}`;
|
|
485
|
+
}
|
|
362
486
|
const textContentType = hasResponseSchema(response) ? getTextContentType(response) : null;
|
|
363
|
-
if (!textContentType) return `return ${responseLiteral(
|
|
364
|
-
|
|
365
|
-
return `${name === body ? "" : `const body = ${body}\n `}if (typeof ${name} === 'string') return ${responseLiteral(name, status, textContentType)}
|
|
487
|
+
if (!textContentType) return `return ${responseLiteral(stringify(body, response), status, contentType, headers)}`;
|
|
488
|
+
return `${prelude}if (typeof ${name} === 'string') return ${responseLiteral(name, status, textContentType, headers)}
|
|
366
489
|
|
|
367
|
-
return ${responseLiteral(
|
|
490
|
+
return ${responseLiteral(stringify(name, response), status, contentType, headers)}`;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* The `data:` lines of one Server-Sent Event. A string goes out as it is, since a client keeps string `data` as text.
|
|
494
|
+
*/
|
|
495
|
+
function eventDataLines(body, response) {
|
|
496
|
+
return `(typeof ${body} === 'string' ? ${body} : ${stringify(body, response)}).split('\\n').map((line) => \`data: \${line}\`).join('\\n')`;
|
|
368
497
|
}
|
|
369
498
|
/**
|
|
370
|
-
*
|
|
371
|
-
|
|
499
|
+
* The spec example of the response entry the mock sends, as source code, or `null` when that entry has none.
|
|
500
|
+
*/
|
|
501
|
+
function getExampleBody(response) {
|
|
502
|
+
const entry = getResponseEntry(response);
|
|
503
|
+
const example = entry?.examples?.find((item) => item.value !== void 0 || item.serializedValue !== void 0);
|
|
504
|
+
if (!entry || !example) return null;
|
|
505
|
+
if (example.value !== void 0) return toLiteral(example.value);
|
|
506
|
+
if (!ast.mediaType.isJson(entry)) return ast.mediaType.getBase(entry) !== "text/event-stream" && getTextContentType(response) === entry.contentType ? toLiteral(example.serializedValue) : null;
|
|
507
|
+
try {
|
|
508
|
+
return toLiteral(JSON.parse(example.serializedValue));
|
|
509
|
+
} catch {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const mswMethods = /* @__PURE__ */ new Set([
|
|
514
|
+
"GET",
|
|
515
|
+
"POST",
|
|
516
|
+
"PUT",
|
|
517
|
+
"PATCH",
|
|
518
|
+
"DELETE",
|
|
519
|
+
"HEAD",
|
|
520
|
+
"OPTIONS"
|
|
521
|
+
]);
|
|
522
|
+
/**
|
|
523
|
+
* Converts an HTTP method to its lowercase MSW equivalent (e.g., 'POST' → 'post'). MSW has no `http.query`, `http.trace` or
|
|
524
|
+
* handler for a custom `additionalOperations` method, so such an operation maps to `http.all` and its handler checks the
|
|
525
|
+
* method itself.
|
|
372
526
|
*/
|
|
373
527
|
function getMswMethod(node) {
|
|
374
|
-
return node.method
|
|
528
|
+
return mswMethods.has(node.method) ? node.method.toLowerCase() : "all";
|
|
375
529
|
}
|
|
376
530
|
/**
|
|
377
|
-
* The `headers` entry of a mocked `Response` init, or an empty string when there is no
|
|
531
|
+
* The `headers` entry of a mocked `Response` init, or an empty string when there is no header to send.
|
|
378
532
|
*/
|
|
379
|
-
function headersBlock(contentType) {
|
|
380
|
-
|
|
533
|
+
function headersBlock(contentType, headers) {
|
|
534
|
+
const entries = [...contentType ? [`'Content-Type': ${text.quote(contentType)}`] : [], ...headers.map(([name, value]) => `${text.quote(name)}: ${text.quote(value)}`)];
|
|
535
|
+
return entries.length ? `\n headers: {\n ${entries.join(",\n ")}\n },` : "";
|
|
381
536
|
}
|
|
382
537
|
/**
|
|
383
538
|
* The `new HttpResponse<string>(...)` expression a mocked handler returns.
|
|
384
539
|
*/
|
|
385
|
-
function responseLiteral(body, status, contentType) {
|
|
540
|
+
function responseLiteral(body, status, contentType, headers) {
|
|
386
541
|
return `new HttpResponse<string>(${body}, {
|
|
387
|
-
status: ${status},${headersBlock(contentType)}
|
|
542
|
+
status: ${status},${headersBlock(contentType, headers)}
|
|
388
543
|
})`;
|
|
389
544
|
}
|
|
390
545
|
// src/components/Mock.tsx
|
|
391
|
-
function Mock({ baseURL = "", name, fakerName, typeName, requestTypeName, node }) {
|
|
546
|
+
function Mock({ baseURL = "", name, fakerName, responseName, typeName, requestTypeName, node }) {
|
|
392
547
|
const method = getMswMethod(node);
|
|
393
|
-
const methodGuard =
|
|
394
|
-
const successResponse =
|
|
395
|
-
const statusCode = successResponse ?
|
|
548
|
+
const methodGuard = method === "all" ? `if (info.request.method !== ${text.quote(node.method)}) return\n\n ` : "";
|
|
549
|
+
const successResponse = ast.http.responses.findPrimary(node);
|
|
550
|
+
const statusCode = successResponse ? ast.http.status.toCode(successResponse) : 200;
|
|
396
551
|
const url = Url.toPath(node.path);
|
|
397
552
|
const dataType = hasResponseSchema(successResponse) ? typeName : "string | number | boolean | null | object";
|
|
398
553
|
const paramType = fakerName ? typeName : dataType;
|
|
@@ -400,7 +555,10 @@ function Mock({ baseURL = "", name, fakerName, typeName, requestTypeName, node }
|
|
|
400
555
|
const params = `data?: ${paramType} | ${requestBodyType ? `HttpResponseResolver<Record<string, string>, ${requestBodyType}>` : `((info: Parameters<Parameters<typeof http.${method}>[1]>[0]) => Response | Promise<Response>)`}, options?: { baseURL?: string }`;
|
|
401
556
|
const httpCall = requestBodyType ? `http.${method}<Record<string, string>, ${requestBodyType}>` : `http.${method}`;
|
|
402
557
|
const urlLiteral = `\`\${options?.baseURL ?? ${resolveDefaultBaseURL(baseURL)}}${url.replace(/([^/]):/g, "$1\\\\:")}\``;
|
|
403
|
-
const
|
|
558
|
+
const example = fakerName ? null : getExampleBody(successResponse);
|
|
559
|
+
const exampleBody = example ? `data === undefined ? ${example} : data` : "data";
|
|
560
|
+
const responseBody = fakerName ? `data ?? ${fakerName}()` : exampleBody;
|
|
561
|
+
const responseStatements = responseName && !example ? `return ${responseName}(${responseBody})` : responseReturn(responseBody, statusCode, successResponse);
|
|
404
562
|
return /* @__PURE__ */ jsx(File.Source, {
|
|
405
563
|
name,
|
|
406
564
|
isIndexable: true,
|
|
@@ -412,16 +570,15 @@ function Mock({ baseURL = "", name, fakerName, typeName, requestTypeName, node }
|
|
|
412
570
|
children: `return ${httpCall}(${urlLiteral}, function handler(info) {
|
|
413
571
|
${methodGuard}if(typeof data === 'function') return data(info)
|
|
414
572
|
|
|
415
|
-
${
|
|
573
|
+
${responseStatements}
|
|
416
574
|
})`
|
|
417
575
|
})
|
|
418
576
|
});
|
|
419
577
|
}
|
|
420
578
|
// src/components/Response.tsx
|
|
421
579
|
function Response({ name, typeName, response }) {
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const responseName = `${name}Response${statusCode}`;
|
|
580
|
+
const params = `data?: ${typeName}`;
|
|
581
|
+
const responseName = `${name}Response${response.statusCode}`;
|
|
425
582
|
return /* @__PURE__ */ jsx(File.Source, {
|
|
426
583
|
name: responseName,
|
|
427
584
|
isIndexable: true,
|
|
@@ -431,7 +588,7 @@ function Response({ name, typeName, response }) {
|
|
|
431
588
|
export: true,
|
|
432
589
|
params,
|
|
433
590
|
children: `
|
|
434
|
-
${responseReturn("data",
|
|
591
|
+
${responseReturn("data", ast.http.status.toCode(response), response)}`
|
|
435
592
|
})
|
|
436
593
|
});
|
|
437
594
|
}
|
|
@@ -446,7 +603,7 @@ const mswGenerator = defineGenerator({
|
|
|
446
603
|
name: "msw",
|
|
447
604
|
renderer: jsxRenderer,
|
|
448
605
|
operation(node, ctx) {
|
|
449
|
-
if (!ast.
|
|
606
|
+
if (!ast.http.isOperation(node)) return null;
|
|
450
607
|
const { driver, resolver, config, root } = ctx;
|
|
451
608
|
const { output, parser, baseURL, group } = ctx.options;
|
|
452
609
|
const fileName = resolver.name(node.operationId);
|
|
@@ -488,8 +645,8 @@ const mswGenerator = defineGenerator({
|
|
|
488
645
|
}),
|
|
489
646
|
responseName: tsResolver.response.response(node)
|
|
490
647
|
};
|
|
491
|
-
const successResponses =
|
|
492
|
-
const referencedNames =
|
|
648
|
+
const successResponses = ast.http.responses.filterSuccess(node);
|
|
649
|
+
const referencedNames = ast.http.responses.getImportedNames(node.responses);
|
|
493
650
|
const enumOptions = pluginTs.options?.enum;
|
|
494
651
|
const enumNames = getEnumNames(ctx);
|
|
495
652
|
const hasResponseNameCollision = referencedNames.some((name) => {
|
|
@@ -500,14 +657,15 @@ const mswGenerator = defineGenerator({
|
|
|
500
657
|
isEnum: enumNames.has(name)
|
|
501
658
|
}) === type.responseName;
|
|
502
659
|
});
|
|
503
|
-
const types =
|
|
504
|
-
|
|
505
|
-
return [code, hasResponseNameCollision && successResponse ? tsResolver.response.status(node, successResponse.statusCode) : typeName];
|
|
660
|
+
const types = node.responses.filter((response) => response.statusCode !== "default" && ast.http.status.getClass(response) !== 1).map((response) => {
|
|
661
|
+
return [response, ast.http.status.isSuccess(response) && !hasResponseNameCollision ? type.responseName : tsResolver.response.status(node, response.statusCode)];
|
|
506
662
|
});
|
|
507
|
-
const responseTypeNames = types.map(([, typeName]) => typeName)
|
|
663
|
+
const responseTypeNames = types.map(([, typeName]) => typeName);
|
|
508
664
|
const mockResponseName = hasResponseNameCollision && successResponses[0] ? tsResolver.response.status(node, successResponses[0].statusCode) : type.responseName;
|
|
509
665
|
const hasSuccessSchema = successResponses.some((response) => hasResponseSchema(response));
|
|
510
|
-
const
|
|
666
|
+
const primaryResponse = ast.http.responses.findPrimary(node);
|
|
667
|
+
const responseHelperName = primaryResponse && hasResponseSchema(primaryResponse) && types.some(([response, typeName]) => response.statusCode === primaryResponse.statusCode && typeName === mockResponseName) ? `${mock.name}Response${primaryResponse.statusCode}` : void 0;
|
|
668
|
+
const requestName = ast.http.requestBody.getSchema(node) ? tsResolver.response.body(node) : null;
|
|
511
669
|
return /* @__PURE__ */ jsxs(File, {
|
|
512
670
|
baseName: mock.file.baseName,
|
|
513
671
|
path: mock.file.path,
|
|
@@ -558,20 +716,17 @@ const mswGenerator = defineGenerator({
|
|
|
558
716
|
root: mock.file.path,
|
|
559
717
|
path: faker.file.path
|
|
560
718
|
}),
|
|
561
|
-
types.
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
response,
|
|
567
|
-
name: mock.name
|
|
568
|
-
}, typeName);
|
|
569
|
-
}),
|
|
719
|
+
types.map(([response, typeName]) => /* @__PURE__ */ jsx(Response, {
|
|
720
|
+
typeName,
|
|
721
|
+
response,
|
|
722
|
+
name: mock.name
|
|
723
|
+
}, response.statusCode)),
|
|
570
724
|
/* @__PURE__ */ jsx(Mock, {
|
|
571
725
|
name: mock.name,
|
|
572
726
|
typeName: mockResponseName,
|
|
573
727
|
requestTypeName: requestName,
|
|
574
728
|
fakerName: faker && hasSuccessSchema ? faker.name : void 0,
|
|
729
|
+
responseName: responseHelperName,
|
|
575
730
|
node,
|
|
576
731
|
baseURL
|
|
577
732
|
})
|
|
@@ -598,7 +753,7 @@ const mswGenerator = defineGenerator({
|
|
|
598
753
|
const resolverMsw = createResolver({
|
|
599
754
|
pluginName: "plugin-msw",
|
|
600
755
|
name(name) {
|
|
601
|
-
return this.memo(`name:${name}`, () =>
|
|
756
|
+
return this.memo(`name:${name}`, () => casing.camel(name, { suffix: "handler" }));
|
|
602
757
|
},
|
|
603
758
|
handler: {
|
|
604
759
|
name(node) {
|
|
@@ -673,7 +828,7 @@ const pluginMsw = defineExtension({
|
|
|
673
828
|
output,
|
|
674
829
|
parser,
|
|
675
830
|
baseURL,
|
|
676
|
-
group:
|
|
831
|
+
group: group ?? null,
|
|
677
832
|
exclude,
|
|
678
833
|
include,
|
|
679
834
|
override,
|