@kubb/plugin-msw 6.2.0-canary.20261111T122007 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +353 -210
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +354 -211
- package/dist/index.js.map +1 -1
- package/docs/options.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.0";
|
|
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,7 +445,7 @@ 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
|
}
|
|
356
451
|
/**
|
|
@@ -359,40 +454,88 @@ function getTextContentType(response) {
|
|
|
359
454
|
*/
|
|
360
455
|
function responseReturn(body, status, response) {
|
|
361
456
|
const contentType = getContentType(response);
|
|
457
|
+
const headers = getMockHeaders(response);
|
|
458
|
+
const name = isValidVarName(body) ? body : "body";
|
|
459
|
+
const prelude = name === body ? "" : `const body = ${body}\n `;
|
|
460
|
+
if (contentType && ast.mediaType.getBase(contentType) === "text/event-stream") {
|
|
461
|
+
const entry = getResponseEntry(response);
|
|
462
|
+
const serialized = entry?.examples?.find((example) => example.serializedValue !== void 0)?.serializedValue;
|
|
463
|
+
const fallback = serialized === void 0 ? "''" : toLiteral(serialized);
|
|
464
|
+
if (entry?.stream?.shape !== "event") return `${prelude}return ${responseLiteral(`${name} === undefined ? ${fallback} : \`\${${eventDataLines(name)}}\\n\\n\``, status, contentType, headers)}`;
|
|
465
|
+
return `${prelude}const event = ${name} as { event?: unknown; id?: unknown; retry?: unknown; data?: unknown } | undefined
|
|
466
|
+
const lines = [
|
|
467
|
+
...(['event', 'id', 'retry'] as const).filter((field) => event?.[field] !== undefined).map((field) => \`\${field}: \${event?.[field]}\`),
|
|
468
|
+
// A client dispatches an event only once it has a \`data\` line, so an event without data still sends an empty one.
|
|
469
|
+
event?.data === undefined ? 'data:' : ${eventDataLines("event.data")},
|
|
470
|
+
]
|
|
471
|
+
|
|
472
|
+
return ${responseLiteral(`event === undefined ? ${fallback} : \`\${lines.join('\\n')}\\n\\n\``, status, contentType, headers)}`;
|
|
473
|
+
}
|
|
362
474
|
const textContentType = hasResponseSchema(response) ? getTextContentType(response) : null;
|
|
363
|
-
if (!textContentType) return `return ${responseLiteral(`JSON.stringify(${body})`, status, contentType)}`;
|
|
364
|
-
|
|
365
|
-
return `${name === body ? "" : `const body = ${body}\n `}if (typeof ${name} === 'string') return ${responseLiteral(name, status, textContentType)}
|
|
475
|
+
if (!textContentType) return `return ${responseLiteral(`JSON.stringify(${body})`, status, contentType, headers)}`;
|
|
476
|
+
return `${prelude}if (typeof ${name} === 'string') return ${responseLiteral(name, status, textContentType, headers)}
|
|
366
477
|
|
|
367
|
-
return ${responseLiteral(`JSON.stringify(${name})`, status, contentType)}`;
|
|
478
|
+
return ${responseLiteral(`JSON.stringify(${name})`, status, contentType, headers)}`;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* The `data:` lines of one Server-Sent Event. A string goes out as it is, since a client keeps string `data` as text.
|
|
482
|
+
*/
|
|
483
|
+
function eventDataLines(body) {
|
|
484
|
+
return `(typeof ${body} === 'string' ? ${body} : JSON.stringify(${body})).split('\\n').map((line) => \`data: \${line}\`).join('\\n')`;
|
|
368
485
|
}
|
|
369
486
|
/**
|
|
370
|
-
*
|
|
371
|
-
|
|
487
|
+
* The spec example of the response entry the mock sends, as source code, or `null` when that entry has none.
|
|
488
|
+
*/
|
|
489
|
+
function getExampleBody(response) {
|
|
490
|
+
const entry = getResponseEntry(response);
|
|
491
|
+
const example = entry?.examples?.find((item) => item.value !== void 0 || item.serializedValue !== void 0);
|
|
492
|
+
if (!entry || !example) return null;
|
|
493
|
+
if (example.value !== void 0) return toLiteral(example.value);
|
|
494
|
+
if (!ast.mediaType.isJson(entry)) return ast.mediaType.getBase(entry) !== "text/event-stream" && getTextContentType(response) === entry.contentType ? toLiteral(example.serializedValue) : null;
|
|
495
|
+
try {
|
|
496
|
+
return toLiteral(JSON.parse(example.serializedValue));
|
|
497
|
+
} catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const mswMethods = /* @__PURE__ */ new Set([
|
|
502
|
+
"GET",
|
|
503
|
+
"POST",
|
|
504
|
+
"PUT",
|
|
505
|
+
"PATCH",
|
|
506
|
+
"DELETE",
|
|
507
|
+
"HEAD",
|
|
508
|
+
"OPTIONS"
|
|
509
|
+
]);
|
|
510
|
+
/**
|
|
511
|
+
* Converts an HTTP method to its lowercase MSW equivalent (e.g., 'POST' → 'post'). MSW has no `http.query`, `http.trace` or
|
|
512
|
+
* handler for a custom `additionalOperations` method, so such an operation maps to `http.all` and its handler checks the
|
|
513
|
+
* method itself.
|
|
372
514
|
*/
|
|
373
515
|
function getMswMethod(node) {
|
|
374
|
-
return node.method
|
|
516
|
+
return mswMethods.has(node.method) ? node.method.toLowerCase() : "all";
|
|
375
517
|
}
|
|
376
518
|
/**
|
|
377
|
-
* The `headers` entry of a mocked `Response` init, or an empty string when there is no
|
|
519
|
+
* The `headers` entry of a mocked `Response` init, or an empty string when there is no header to send.
|
|
378
520
|
*/
|
|
379
|
-
function headersBlock(contentType) {
|
|
380
|
-
|
|
521
|
+
function headersBlock(contentType, headers) {
|
|
522
|
+
const entries = [...contentType ? [`'Content-Type': ${text.quote(contentType)}`] : [], ...headers.map(([name, value]) => `${text.quote(name)}: ${text.quote(value)}`)];
|
|
523
|
+
return entries.length ? `\n headers: {\n ${entries.join(",\n ")}\n },` : "";
|
|
381
524
|
}
|
|
382
525
|
/**
|
|
383
526
|
* The `new HttpResponse<string>(...)` expression a mocked handler returns.
|
|
384
527
|
*/
|
|
385
|
-
function responseLiteral(body, status, contentType) {
|
|
528
|
+
function responseLiteral(body, status, contentType, headers) {
|
|
386
529
|
return `new HttpResponse<string>(${body}, {
|
|
387
|
-
status: ${status},${headersBlock(contentType)}
|
|
530
|
+
status: ${status},${headersBlock(contentType, headers)}
|
|
388
531
|
})`;
|
|
389
532
|
}
|
|
390
533
|
// src/components/Mock.tsx
|
|
391
|
-
function Mock({ baseURL = "", name, fakerName, typeName, requestTypeName, node }) {
|
|
534
|
+
function Mock({ baseURL = "", name, fakerName, responseName, typeName, requestTypeName, node }) {
|
|
392
535
|
const method = getMswMethod(node);
|
|
393
|
-
const methodGuard =
|
|
394
|
-
const successResponse =
|
|
395
|
-
const statusCode = successResponse ?
|
|
536
|
+
const methodGuard = method === "all" ? `if (info.request.method !== ${text.quote(node.method)}) return\n\n ` : "";
|
|
537
|
+
const successResponse = ast.http.responses.findPrimary(node);
|
|
538
|
+
const statusCode = successResponse ? ast.http.status.toCode(successResponse) : 200;
|
|
396
539
|
const url = Url.toPath(node.path);
|
|
397
540
|
const dataType = hasResponseSchema(successResponse) ? typeName : "string | number | boolean | null | object";
|
|
398
541
|
const paramType = fakerName ? typeName : dataType;
|
|
@@ -400,7 +543,10 @@ function Mock({ baseURL = "", name, fakerName, typeName, requestTypeName, node }
|
|
|
400
543
|
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
544
|
const httpCall = requestBodyType ? `http.${method}<Record<string, string>, ${requestBodyType}>` : `http.${method}`;
|
|
402
545
|
const urlLiteral = `\`\${options?.baseURL ?? ${resolveDefaultBaseURL(baseURL)}}${url.replace(/([^/]):/g, "$1\\\\:")}\``;
|
|
403
|
-
const
|
|
546
|
+
const example = fakerName ? null : getExampleBody(successResponse);
|
|
547
|
+
const exampleBody = example ? `data === undefined ? ${example} : data` : "data";
|
|
548
|
+
const responseBody = fakerName ? `data ?? ${fakerName}()` : exampleBody;
|
|
549
|
+
const responseStatements = responseName && !example ? `return ${responseName}(${responseBody})` : responseReturn(responseBody, statusCode, successResponse);
|
|
404
550
|
return /* @__PURE__ */ jsx(File.Source, {
|
|
405
551
|
name,
|
|
406
552
|
isIndexable: true,
|
|
@@ -412,16 +558,15 @@ function Mock({ baseURL = "", name, fakerName, typeName, requestTypeName, node }
|
|
|
412
558
|
children: `return ${httpCall}(${urlLiteral}, function handler(info) {
|
|
413
559
|
${methodGuard}if(typeof data === 'function') return data(info)
|
|
414
560
|
|
|
415
|
-
${
|
|
561
|
+
${responseStatements}
|
|
416
562
|
})`
|
|
417
563
|
})
|
|
418
564
|
});
|
|
419
565
|
}
|
|
420
566
|
// src/components/Response.tsx
|
|
421
567
|
function Response({ name, typeName, response }) {
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const responseName = `${name}Response${statusCode}`;
|
|
568
|
+
const params = `data?: ${typeName}`;
|
|
569
|
+
const responseName = `${name}Response${response.statusCode}`;
|
|
425
570
|
return /* @__PURE__ */ jsx(File.Source, {
|
|
426
571
|
name: responseName,
|
|
427
572
|
isIndexable: true,
|
|
@@ -431,7 +576,7 @@ function Response({ name, typeName, response }) {
|
|
|
431
576
|
export: true,
|
|
432
577
|
params,
|
|
433
578
|
children: `
|
|
434
|
-
${responseReturn("data",
|
|
579
|
+
${responseReturn("data", ast.http.status.toCode(response), response)}`
|
|
435
580
|
})
|
|
436
581
|
});
|
|
437
582
|
}
|
|
@@ -446,7 +591,7 @@ const mswGenerator = defineGenerator({
|
|
|
446
591
|
name: "msw",
|
|
447
592
|
renderer: jsxRenderer,
|
|
448
593
|
operation(node, ctx) {
|
|
449
|
-
if (!ast.
|
|
594
|
+
if (!ast.http.isOperation(node)) return null;
|
|
450
595
|
const { driver, resolver, config, root } = ctx;
|
|
451
596
|
const { output, parser, baseURL, group } = ctx.options;
|
|
452
597
|
const fileName = resolver.name(node.operationId);
|
|
@@ -488,8 +633,8 @@ const mswGenerator = defineGenerator({
|
|
|
488
633
|
}),
|
|
489
634
|
responseName: tsResolver.response.response(node)
|
|
490
635
|
};
|
|
491
|
-
const successResponses =
|
|
492
|
-
const referencedNames =
|
|
636
|
+
const successResponses = ast.http.responses.filterSuccess(node);
|
|
637
|
+
const referencedNames = ast.http.responses.getImportedNames(node.responses);
|
|
493
638
|
const enumOptions = pluginTs.options?.enum;
|
|
494
639
|
const enumNames = getEnumNames(ctx);
|
|
495
640
|
const hasResponseNameCollision = referencedNames.some((name) => {
|
|
@@ -500,14 +645,15 @@ const mswGenerator = defineGenerator({
|
|
|
500
645
|
isEnum: enumNames.has(name)
|
|
501
646
|
}) === type.responseName;
|
|
502
647
|
});
|
|
503
|
-
const types =
|
|
504
|
-
|
|
505
|
-
return [code, hasResponseNameCollision && successResponse ? tsResolver.response.status(node, successResponse.statusCode) : typeName];
|
|
648
|
+
const types = node.responses.filter((response) => response.statusCode !== "default" && ast.http.status.getClass(response) !== 1).map((response) => {
|
|
649
|
+
return [response, ast.http.status.isSuccess(response) && !hasResponseNameCollision ? type.responseName : tsResolver.response.status(node, response.statusCode)];
|
|
506
650
|
});
|
|
507
|
-
const responseTypeNames = types.map(([, typeName]) => typeName)
|
|
651
|
+
const responseTypeNames = types.map(([, typeName]) => typeName);
|
|
508
652
|
const mockResponseName = hasResponseNameCollision && successResponses[0] ? tsResolver.response.status(node, successResponses[0].statusCode) : type.responseName;
|
|
509
653
|
const hasSuccessSchema = successResponses.some((response) => hasResponseSchema(response));
|
|
510
|
-
const
|
|
654
|
+
const primaryResponse = ast.http.responses.findPrimary(node);
|
|
655
|
+
const responseHelperName = primaryResponse && hasResponseSchema(primaryResponse) && types.some(([response, typeName]) => response.statusCode === primaryResponse.statusCode && typeName === mockResponseName) ? `${mock.name}Response${primaryResponse.statusCode}` : void 0;
|
|
656
|
+
const requestName = ast.http.requestBody.getSchema(node) ? tsResolver.response.body(node) : null;
|
|
511
657
|
return /* @__PURE__ */ jsxs(File, {
|
|
512
658
|
baseName: mock.file.baseName,
|
|
513
659
|
path: mock.file.path,
|
|
@@ -558,20 +704,17 @@ const mswGenerator = defineGenerator({
|
|
|
558
704
|
root: mock.file.path,
|
|
559
705
|
path: faker.file.path
|
|
560
706
|
}),
|
|
561
|
-
types.
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
response,
|
|
567
|
-
name: mock.name
|
|
568
|
-
}, typeName);
|
|
569
|
-
}),
|
|
707
|
+
types.map(([response, typeName]) => /* @__PURE__ */ jsx(Response, {
|
|
708
|
+
typeName,
|
|
709
|
+
response,
|
|
710
|
+
name: mock.name
|
|
711
|
+
}, response.statusCode)),
|
|
570
712
|
/* @__PURE__ */ jsx(Mock, {
|
|
571
713
|
name: mock.name,
|
|
572
714
|
typeName: mockResponseName,
|
|
573
715
|
requestTypeName: requestName,
|
|
574
716
|
fakerName: faker && hasSuccessSchema ? faker.name : void 0,
|
|
717
|
+
responseName: responseHelperName,
|
|
575
718
|
node,
|
|
576
719
|
baseURL
|
|
577
720
|
})
|
|
@@ -598,7 +741,7 @@ const mswGenerator = defineGenerator({
|
|
|
598
741
|
const resolverMsw = createResolver({
|
|
599
742
|
pluginName: "plugin-msw",
|
|
600
743
|
name(name) {
|
|
601
|
-
return this.memo(`name:${name}`, () =>
|
|
744
|
+
return this.memo(`name:${name}`, () => casing.camel(name, { suffix: "handler" }));
|
|
602
745
|
},
|
|
603
746
|
handler: {
|
|
604
747
|
name(node) {
|
|
@@ -673,7 +816,7 @@ const pluginMsw = defineExtension({
|
|
|
673
816
|
output,
|
|
674
817
|
parser,
|
|
675
818
|
baseURL,
|
|
676
|
-
group:
|
|
819
|
+
group: group ?? null,
|
|
677
820
|
exclude,
|
|
678
821
|
include,
|
|
679
822
|
override,
|