@kubb/plugin-msw 6.2.0-canary.20261111T115959 → 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 +357 -212
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +358 -213
- 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,42 +359,15 @@ 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
|
|
170
365
|
* it is, so two of them never share an allocation even when a custom resolver gives both the same
|
|
171
366
|
* name. Leave `scope` empty for the operation's own declaration.
|
|
172
367
|
*
|
|
368
|
+
* The key starts from the operation's input `pointer`, which macros keep, so a plugin whose macro
|
|
369
|
+
* rewrites the method or path still resolves another plugin's names for the same operation.
|
|
370
|
+
*
|
|
173
371
|
* @example
|
|
174
372
|
* ```ts
|
|
175
373
|
* resolver.resolveName({ key: operationKey(node, 'response', 'status', 404), name })
|
|
@@ -178,8 +376,7 @@ function resolveResponseTypes(node, resolver) {
|
|
|
178
376
|
function operationKey(node, ...scope) {
|
|
179
377
|
return [
|
|
180
378
|
"operation",
|
|
181
|
-
node.method,
|
|
182
|
-
node.path,
|
|
379
|
+
...node.pointer ? [node.pointer] : [node.method, node.path],
|
|
183
380
|
...scope.map(String)
|
|
184
381
|
];
|
|
185
382
|
}
|
|
@@ -189,124 +386,25 @@ function operationKey(node, ...scope) {
|
|
|
189
386
|
function getEnumNames(ctx) {
|
|
190
387
|
return ctx.memo("enumNames:set", () => new Set((ctx.driver.inputNode?.schemas ?? []).flatMap((schema) => schema.type === "enum" && schema.name ? [schema.name] : [])));
|
|
191
388
|
}
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
* - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
|
|
198
|
-
* - other groups use the camelCased group (`pet store` → `petStore`).
|
|
199
|
-
*
|
|
200
|
-
* A user-provided `group.name` always wins over the default namer, so callers stay in
|
|
201
|
-
* control of their output folders. Returns `null` when grouping is disabled, matching the
|
|
202
|
-
* per-plugin convention.
|
|
203
|
-
*
|
|
204
|
-
* @param group - The user-supplied group option, or `undefined` to disable grouping.
|
|
205
|
-
*
|
|
206
|
-
* @example
|
|
207
|
-
* ```ts
|
|
208
|
-
* createGroupConfig(group) // shared across every plugin
|
|
209
|
-
* ```
|
|
210
|
-
*/
|
|
211
|
-
function createGroupConfig(group) {
|
|
212
|
-
if (!group) return null;
|
|
213
|
-
const defaultName = (ctx) => {
|
|
214
|
-
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
|
|
215
|
-
return camelCase(ctx.group);
|
|
216
|
-
};
|
|
217
|
-
return {
|
|
218
|
-
...group,
|
|
219
|
-
name: group.name ? group.name : defaultName
|
|
220
|
-
};
|
|
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);
|
|
221
394
|
}
|
|
222
|
-
// kubb-extension.json
|
|
223
|
-
var compatibility = {
|
|
224
|
-
"kubb": ">=5.16.0",
|
|
225
|
-
"node": ">=22.22"
|
|
226
|
-
};
|
|
227
|
-
// package.json
|
|
228
|
-
var version = "6.2.0-canary.20261111T115959";
|
|
229
|
-
// src/generators/handlersGenerator.ts
|
|
230
395
|
/**
|
|
231
|
-
*
|
|
232
|
-
* `handlers.ts` file that re-exports every generated handler in operation
|
|
233
|
-
* order, ready to spread into `setupServer(...handlers)` or
|
|
234
|
-
* `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.
|
|
235
397
|
*/
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const file = resolver.file({
|
|
247
|
-
name: handlersName,
|
|
248
|
-
extname: ".ts",
|
|
249
|
-
root,
|
|
250
|
-
output,
|
|
251
|
-
group: group ?? void 0
|
|
252
|
-
});
|
|
253
|
-
const handlerNames = nodes.map((node) => resolver.handler.name(node));
|
|
254
|
-
const imports = nodes.map((node, index) => {
|
|
255
|
-
const operationName = handlerNames[index];
|
|
256
|
-
const operationFile = resolver.file({
|
|
257
|
-
name: resolver.name(node.operationId),
|
|
258
|
-
extname: ".ts",
|
|
259
|
-
tag: node.tags[0] ?? "default",
|
|
260
|
-
path: node.path,
|
|
261
|
-
root,
|
|
262
|
-
output,
|
|
263
|
-
group: group ?? void 0
|
|
264
|
-
});
|
|
265
|
-
return ast.factory.createImport({
|
|
266
|
-
name: [operationName],
|
|
267
|
-
root: file.path,
|
|
268
|
-
path: operationFile.path
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
const handlers = handlerNames.map((name) => `${name}(undefined, options)`);
|
|
272
|
-
return [ast.factory.createFile({
|
|
273
|
-
baseName: file.baseName,
|
|
274
|
-
path: file.path,
|
|
275
|
-
meta: file.meta,
|
|
276
|
-
banner: resolver.default.banner(ctx.meta, {
|
|
277
|
-
output,
|
|
278
|
-
config,
|
|
279
|
-
file: {
|
|
280
|
-
path: file.path,
|
|
281
|
-
baseName: file.baseName
|
|
282
|
-
}
|
|
283
|
-
}),
|
|
284
|
-
footer: resolver.default.footer(ctx.meta, {
|
|
285
|
-
output,
|
|
286
|
-
config,
|
|
287
|
-
file: {
|
|
288
|
-
path: file.path,
|
|
289
|
-
baseName: file.baseName
|
|
290
|
-
}
|
|
291
|
-
}),
|
|
292
|
-
imports,
|
|
293
|
-
sources: [ast.factory.createSource({
|
|
294
|
-
name: createHandlersName,
|
|
295
|
-
isIndexable: true,
|
|
296
|
-
isExportable: true,
|
|
297
|
-
nodes: [ast.factory.createText(`export function ${createHandlersName}(options?: { baseURL?: string }) {
|
|
298
|
-
return [${handlers.join(", ")}] as const
|
|
299
|
-
}`)]
|
|
300
|
-
}), ast.factory.createSource({
|
|
301
|
-
name: handlersName,
|
|
302
|
-
isIndexable: true,
|
|
303
|
-
isExportable: true,
|
|
304
|
-
nodes: [ast.factory.createText(`export const ${handlersName} = ${createHandlersName}()`)]
|
|
305
|
-
})]
|
|
306
|
-
})];
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
// 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
|
+
}
|
|
310
408
|
/**
|
|
311
409
|
* Returns the base URL as source code. A `${...}` value stays a template literal.
|
|
312
410
|
*/
|
|
@@ -334,13 +432,12 @@ function hasResponseSchema(response) {
|
|
|
334
432
|
* content types, JSON is preferred (the faker mock body is JSON), otherwise the first declared type.
|
|
335
433
|
*/
|
|
336
434
|
function getResponseContentType(response) {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
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;
|
|
340
437
|
}
|
|
341
|
-
function
|
|
342
|
-
const
|
|
343
|
-
return
|
|
438
|
+
function getResponseEntry(response) {
|
|
439
|
+
const contents = response?.content ?? [];
|
|
440
|
+
return contents.find((entry) => ast.mediaType.isJson(entry)) ?? contents[0];
|
|
344
441
|
}
|
|
345
442
|
/**
|
|
346
443
|
* The first content type of a response that the client reads as text, such as `application/xml`, or `null` when none is.
|
|
@@ -348,7 +445,7 @@ function isJson(contentType) {
|
|
|
348
445
|
function getTextContentType(response) {
|
|
349
446
|
return (response?.content ?? []).find((entry) => {
|
|
350
447
|
const schema = toResponseContent(entry, response?.content).schema;
|
|
351
|
-
return !isJson(entry
|
|
448
|
+
return !ast.mediaType.isJson(entry) && schema && ast.ref.getSchema(schema).primitive === "string";
|
|
352
449
|
})?.contentType ?? null;
|
|
353
450
|
}
|
|
354
451
|
/**
|
|
@@ -357,40 +454,88 @@ function getTextContentType(response) {
|
|
|
357
454
|
*/
|
|
358
455
|
function responseReturn(body, status, response) {
|
|
359
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
|
+
}
|
|
360
474
|
const textContentType = hasResponseSchema(response) ? getTextContentType(response) : null;
|
|
361
|
-
if (!textContentType) return `return ${responseLiteral(`JSON.stringify(${body})`, status, contentType)}`;
|
|
362
|
-
|
|
363
|
-
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)}
|
|
364
477
|
|
|
365
|
-
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')`;
|
|
366
485
|
}
|
|
367
486
|
/**
|
|
368
|
-
*
|
|
369
|
-
|
|
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.
|
|
370
514
|
*/
|
|
371
515
|
function getMswMethod(node) {
|
|
372
|
-
return node.method
|
|
516
|
+
return mswMethods.has(node.method) ? node.method.toLowerCase() : "all";
|
|
373
517
|
}
|
|
374
518
|
/**
|
|
375
|
-
* 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.
|
|
376
520
|
*/
|
|
377
|
-
function headersBlock(contentType) {
|
|
378
|
-
|
|
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 },` : "";
|
|
379
524
|
}
|
|
380
525
|
/**
|
|
381
526
|
* The `new HttpResponse<string>(...)` expression a mocked handler returns.
|
|
382
527
|
*/
|
|
383
|
-
function responseLiteral(body, status, contentType) {
|
|
528
|
+
function responseLiteral(body, status, contentType, headers) {
|
|
384
529
|
return `new HttpResponse<string>(${body}, {
|
|
385
|
-
status: ${status},${headersBlock(contentType)}
|
|
530
|
+
status: ${status},${headersBlock(contentType, headers)}
|
|
386
531
|
})`;
|
|
387
532
|
}
|
|
388
533
|
// src/components/Mock.tsx
|
|
389
|
-
function Mock({ baseURL = "", name, fakerName, typeName, requestTypeName, node }) {
|
|
534
|
+
function Mock({ baseURL = "", name, fakerName, responseName, typeName, requestTypeName, node }) {
|
|
390
535
|
const method = getMswMethod(node);
|
|
391
|
-
const methodGuard =
|
|
392
|
-
const successResponse =
|
|
393
|
-
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;
|
|
394
539
|
const url = Url.toPath(node.path);
|
|
395
540
|
const dataType = hasResponseSchema(successResponse) ? typeName : "string | number | boolean | null | object";
|
|
396
541
|
const paramType = fakerName ? typeName : dataType;
|
|
@@ -398,7 +543,10 @@ function Mock({ baseURL = "", name, fakerName, typeName, requestTypeName, node }
|
|
|
398
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 }`;
|
|
399
544
|
const httpCall = requestBodyType ? `http.${method}<Record<string, string>, ${requestBodyType}>` : `http.${method}`;
|
|
400
545
|
const urlLiteral = `\`\${options?.baseURL ?? ${resolveDefaultBaseURL(baseURL)}}${url.replace(/([^/]):/g, "$1\\\\:")}\``;
|
|
401
|
-
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);
|
|
402
550
|
return /* @__PURE__ */ jsx(File.Source, {
|
|
403
551
|
name,
|
|
404
552
|
isIndexable: true,
|
|
@@ -410,16 +558,15 @@ function Mock({ baseURL = "", name, fakerName, typeName, requestTypeName, node }
|
|
|
410
558
|
children: `return ${httpCall}(${urlLiteral}, function handler(info) {
|
|
411
559
|
${methodGuard}if(typeof data === 'function') return data(info)
|
|
412
560
|
|
|
413
|
-
${
|
|
561
|
+
${responseStatements}
|
|
414
562
|
})`
|
|
415
563
|
})
|
|
416
564
|
});
|
|
417
565
|
}
|
|
418
566
|
// src/components/Response.tsx
|
|
419
567
|
function Response({ name, typeName, response }) {
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
const responseName = `${name}Response${statusCode}`;
|
|
568
|
+
const params = `data?: ${typeName}`;
|
|
569
|
+
const responseName = `${name}Response${response.statusCode}`;
|
|
423
570
|
return /* @__PURE__ */ jsx(File.Source, {
|
|
424
571
|
name: responseName,
|
|
425
572
|
isIndexable: true,
|
|
@@ -429,7 +576,7 @@ function Response({ name, typeName, response }) {
|
|
|
429
576
|
export: true,
|
|
430
577
|
params,
|
|
431
578
|
children: `
|
|
432
|
-
${responseReturn("data",
|
|
579
|
+
${responseReturn("data", ast.http.status.toCode(response), response)}`
|
|
433
580
|
})
|
|
434
581
|
});
|
|
435
582
|
}
|
|
@@ -444,7 +591,7 @@ const mswGenerator = defineGenerator({
|
|
|
444
591
|
name: "msw",
|
|
445
592
|
renderer: jsxRenderer,
|
|
446
593
|
operation(node, ctx) {
|
|
447
|
-
if (!ast.
|
|
594
|
+
if (!ast.http.isOperation(node)) return null;
|
|
448
595
|
const { driver, resolver, config, root } = ctx;
|
|
449
596
|
const { output, parser, baseURL, group } = ctx.options;
|
|
450
597
|
const fileName = resolver.name(node.operationId);
|
|
@@ -486,8 +633,8 @@ const mswGenerator = defineGenerator({
|
|
|
486
633
|
}),
|
|
487
634
|
responseName: tsResolver.response.response(node)
|
|
488
635
|
};
|
|
489
|
-
const successResponses =
|
|
490
|
-
const referencedNames =
|
|
636
|
+
const successResponses = ast.http.responses.filterSuccess(node);
|
|
637
|
+
const referencedNames = ast.http.responses.getImportedNames(node.responses);
|
|
491
638
|
const enumOptions = pluginTs.options?.enum;
|
|
492
639
|
const enumNames = getEnumNames(ctx);
|
|
493
640
|
const hasResponseNameCollision = referencedNames.some((name) => {
|
|
@@ -498,14 +645,15 @@ const mswGenerator = defineGenerator({
|
|
|
498
645
|
isEnum: enumNames.has(name)
|
|
499
646
|
}) === type.responseName;
|
|
500
647
|
});
|
|
501
|
-
const types =
|
|
502
|
-
|
|
503
|
-
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)];
|
|
504
650
|
});
|
|
505
|
-
const responseTypeNames = types.map(([, typeName]) => typeName)
|
|
651
|
+
const responseTypeNames = types.map(([, typeName]) => typeName);
|
|
506
652
|
const mockResponseName = hasResponseNameCollision && successResponses[0] ? tsResolver.response.status(node, successResponses[0].statusCode) : type.responseName;
|
|
507
653
|
const hasSuccessSchema = successResponses.some((response) => hasResponseSchema(response));
|
|
508
|
-
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;
|
|
509
657
|
return /* @__PURE__ */ jsxs(File, {
|
|
510
658
|
baseName: mock.file.baseName,
|
|
511
659
|
path: mock.file.path,
|
|
@@ -556,20 +704,17 @@ const mswGenerator = defineGenerator({
|
|
|
556
704
|
root: mock.file.path,
|
|
557
705
|
path: faker.file.path
|
|
558
706
|
}),
|
|
559
|
-
types.
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
response,
|
|
565
|
-
name: mock.name
|
|
566
|
-
}, typeName);
|
|
567
|
-
}),
|
|
707
|
+
types.map(([response, typeName]) => /* @__PURE__ */ jsx(Response, {
|
|
708
|
+
typeName,
|
|
709
|
+
response,
|
|
710
|
+
name: mock.name
|
|
711
|
+
}, response.statusCode)),
|
|
568
712
|
/* @__PURE__ */ jsx(Mock, {
|
|
569
713
|
name: mock.name,
|
|
570
714
|
typeName: mockResponseName,
|
|
571
715
|
requestTypeName: requestName,
|
|
572
716
|
fakerName: faker && hasSuccessSchema ? faker.name : void 0,
|
|
717
|
+
responseName: responseHelperName,
|
|
573
718
|
node,
|
|
574
719
|
baseURL
|
|
575
720
|
})
|
|
@@ -596,7 +741,7 @@ const mswGenerator = defineGenerator({
|
|
|
596
741
|
const resolverMsw = createResolver({
|
|
597
742
|
pluginName: "plugin-msw",
|
|
598
743
|
name(name) {
|
|
599
|
-
return this.memo(`name:${name}`, () =>
|
|
744
|
+
return this.memo(`name:${name}`, () => casing.camel(name, { suffix: "handler" }));
|
|
600
745
|
},
|
|
601
746
|
handler: {
|
|
602
747
|
name(node) {
|
|
@@ -671,7 +816,7 @@ const pluginMsw = defineExtension({
|
|
|
671
816
|
output,
|
|
672
817
|
parser,
|
|
673
818
|
baseURL,
|
|
674
|
-
group:
|
|
819
|
+
group: group ?? null,
|
|
675
820
|
exclude,
|
|
676
821
|
include,
|
|
677
822
|
override,
|