@kubb/plugin-msw 5.0.0-beta.10 → 5.0.0-beta.101

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.js CHANGED
@@ -1,58 +1,495 @@
1
- import "./chunk--u3MIqq1.js";
2
- import { c as camelCase } from "./components-Cm17DMTE.js";
3
- import { n as handlersGenerator, t as mswGenerator } from "./generators-D9gvdP7Z.js";
4
- import { definePlugin, defineResolver } from "@kubb/core";
1
+ import "./rolldown-runtime-C0LytTxp.js";
2
+ import { Resolver, Url, ast, createResolver, defineGenerator, definePlugin } from "kubb/kit";
5
3
  import { pluginFakerName } from "@kubb/plugin-faker";
6
- import { pluginTsName } from "@kubb/plugin-ts";
4
+ import { createFunctionParameter, createFunctionParameters, functionPrinter, pluginTsName } from "@kubb/plugin-ts";
5
+ import { File, Function, jsxRenderer } from "kubb/jsx";
6
+ import { jsx, jsxs } from "kubb/jsx/jsx-runtime";
7
+ //#region ../../internals/shared/src/operation.ts
8
+ function getStatusCodeNumber(statusCode) {
9
+ const code = Number(statusCode);
10
+ return Number.isNaN(code) ? null : code;
11
+ }
12
+ function isSuccessStatusCode(statusCode) {
13
+ const code = getStatusCodeNumber(statusCode);
14
+ return code !== null && code >= 200 && code < 300;
15
+ }
16
+ function getSuccessResponses(responses) {
17
+ return responses.filter((response) => isSuccessStatusCode(response.statusCode));
18
+ }
19
+ function getOperationSuccessResponses(node) {
20
+ return getSuccessResponses(node.responses);
21
+ }
22
+ function getPrimarySuccessResponse(node) {
23
+ return getOperationSuccessResponses(node)[0] ?? null;
24
+ }
25
+ function resolveResponseTypes(node, resolver) {
26
+ const types = [];
27
+ for (const response of node.responses) {
28
+ if (response.statusCode === "default") {
29
+ types.push(["default", resolver.response.response(node)]);
30
+ continue;
31
+ }
32
+ const code = getStatusCodeNumber(response.statusCode);
33
+ if (code === null) continue;
34
+ types.push([code, isSuccessStatusCode(code) ? resolver.response.response(node) : resolver.response.status(node, response.statusCode)]);
35
+ }
36
+ return types;
37
+ }
38
+ //#endregion
39
+ //#region ../../internals/utils/src/casing.ts
40
+ /**
41
+ * Shared implementation for camelCase and PascalCase conversion.
42
+ * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
43
+ * and capitalizes each word according to `pascal`.
44
+ *
45
+ * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
46
+ */
47
+ function toCamelOrPascal(text, pascal) {
48
+ return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
49
+ if (word.length > 1 && word === word.toUpperCase()) return word;
50
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
51
+ }).join("").replace(/[^a-zA-Z0-9]/g, "");
52
+ }
53
+ /**
54
+ * Converts `text` to camelCase.
55
+ *
56
+ * @example Word boundaries
57
+ * `camelCase('hello-world') // 'helloWorld'`
58
+ *
59
+ * @example With a prefix
60
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
61
+ */
62
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
63
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
64
+ }
65
+ //#endregion
66
+ //#region ../../internals/shared/src/group.ts
67
+ /**
68
+ * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
69
+ * shared default naming so every plugin groups output consistently:
70
+ *
71
+ * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
72
+ * - other groups use the camelCased group (`pet store` → `petStore`).
73
+ *
74
+ * A user-provided `group.name` always wins over the default namer, so callers stay in
75
+ * control of their output folders. Returns `null` when grouping is disabled, matching the
76
+ * per-plugin convention.
77
+ *
78
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * createGroupConfig(group) // shared across every plugin
83
+ * ```
84
+ */
85
+ function createGroupConfig(group) {
86
+ if (!group) return null;
87
+ const defaultName = (ctx) => {
88
+ if (group.type === "path") return `${ctx.group.split("/")[1]}`;
89
+ return camelCase(ctx.group);
90
+ };
91
+ return {
92
+ ...group,
93
+ name: group.name ? group.name : defaultName
94
+ };
95
+ }
96
+ //#endregion
97
+ //#region src/generators/handlersGenerator.ts
98
+ /**
99
+ * Aggregate generator enabled by `pluginMsw({ handlers: true })`. Emits a
100
+ * `handlers.ts` file that re-exports every generated handler in operation
101
+ * order, ready to spread into `setupServer(...handlers)` or
102
+ * `setupWorker(...handlers)`.
103
+ */
104
+ const handlersGenerator = defineGenerator({
105
+ name: "plugin-msw",
106
+ operations(nodes, ctx) {
107
+ const { resolver, config, root } = ctx;
108
+ const { output, group } = ctx.options;
109
+ const handlersName = resolver.handler.listName();
110
+ const file = resolver.file({
111
+ name: handlersName,
112
+ extname: ".ts",
113
+ root,
114
+ output,
115
+ group: group ?? void 0
116
+ });
117
+ const imports = nodes.map((node) => {
118
+ const operationName = resolver.handler.name(node);
119
+ const operationFile = resolver.file({
120
+ name: resolver.name(node.operationId),
121
+ extname: ".ts",
122
+ tag: node.tags[0] ?? "default",
123
+ path: node.path,
124
+ root,
125
+ output,
126
+ group: group ?? void 0
127
+ });
128
+ return ast.factory.createImport({
129
+ name: [operationName],
130
+ root: file.path,
131
+ path: operationFile.path
132
+ });
133
+ });
134
+ const handlers = nodes.map((node) => `${resolver.handler.name(node)}()`);
135
+ return [ast.factory.createFile({
136
+ baseName: file.baseName,
137
+ path: file.path,
138
+ meta: file.meta,
139
+ banner: resolver.default.banner(ctx.meta, {
140
+ output,
141
+ config,
142
+ file: {
143
+ path: file.path,
144
+ baseName: file.baseName
145
+ }
146
+ }),
147
+ footer: resolver.default.footer(ctx.meta, {
148
+ output,
149
+ config,
150
+ file: {
151
+ path: file.path,
152
+ baseName: file.baseName
153
+ }
154
+ }),
155
+ imports,
156
+ sources: [ast.factory.createSource({
157
+ name: handlersName,
158
+ isIndexable: true,
159
+ isExportable: true,
160
+ nodes: [ast.factory.createText(`export const ${handlersName} = ${JSON.stringify(handlers).replaceAll("\"", "")} as const`)]
161
+ })]
162
+ })];
163
+ }
164
+ });
165
+ //#endregion
166
+ //#region src/utils.ts
167
+ /**
168
+ * Gets the content type from a response, defaulting to 'application/json' if a schema exists.
169
+ */
170
+ function getContentType(response) {
171
+ if (!hasResponseSchema(response)) return null;
172
+ return getResponseContentType(response) ?? "application/json";
173
+ }
174
+ /**
175
+ * Determines if a response has a schema that is not void or any.
176
+ */
177
+ function hasResponseSchema(response) {
178
+ const schema = response?.content?.find((entry) => entry.schema)?.schema;
179
+ return !!schema && schema.type !== "void" && schema.type !== "any";
180
+ }
181
+ /**
182
+ * Picks the content type used for the mocked response header. When a response declares multiple
183
+ * content types, JSON is preferred (the faker mock body is JSON), otherwise the first declared type.
184
+ */
185
+ function getResponseContentType(response) {
186
+ const contents = response?.content ?? [];
187
+ const value = (contents.find((entry) => {
188
+ const baseType = entry.contentType?.split(";")[0]?.trim().toLowerCase();
189
+ return baseType === "application/json" || baseType?.endsWith("+json");
190
+ }) ?? contents[0])?.contentType;
191
+ return typeof value === "string" && value.length > 0 ? value : null;
192
+ }
193
+ /**
194
+ * Converts an HTTP method to its lowercase MSW equivalent (e.g., 'POST' → 'post').
195
+ */
196
+ function getMswMethod(node) {
197
+ return ast.isHttpOperationNode(node) ? node.method.toLowerCase() : "";
198
+ }
199
+ /**
200
+ * Resolves faker metadata for an MSW operation, including response name and file path.
201
+ */
202
+ function resolveFakerMeta(node, options) {
203
+ const { root, fakerResolver, fakerOutput, fakerGroup } = options;
204
+ const tag = node.tags[0] ?? "default";
205
+ return {
206
+ name: fakerResolver.response.response(node),
207
+ file: fakerResolver.file({
208
+ name: node.operationId,
209
+ extname: ".ts",
210
+ tag,
211
+ path: node.path,
212
+ root,
213
+ output: fakerOutput,
214
+ group: fakerGroup ?? void 0
215
+ })
216
+ };
217
+ }
218
+ //#endregion
219
+ //#region src/components/Mock.tsx
220
+ const declarationPrinter$1 = functionPrinter({ mode: "declaration" });
221
+ function Mock({ baseURL = "", name, fakerName, typeName, requestTypeName, node }) {
222
+ const method = getMswMethod(node);
223
+ const successResponse = getPrimarySuccessResponse(node);
224
+ const statusCode = successResponse ? Number(successResponse.statusCode) : 200;
225
+ const contentType = getContentType(successResponse);
226
+ const url = ast.isHttpOperationNode(node) ? Url.toPath(node.path) : "";
227
+ const headers = [contentType ? `'Content-Type': '${contentType}'` : null].filter(Boolean);
228
+ const dataType = hasResponseSchema(successResponse) ? typeName : "string | number | boolean | null | object";
229
+ const paramType = fakerName ? typeName : dataType;
230
+ const callbackType = requestTypeName ? `HttpResponseResolver<Record<string, string>, ${requestTypeName}>` : `((info: Parameters<Parameters<typeof http.${method}>[1]>[0]) => Response | Promise<Response>)`;
231
+ const params = declarationPrinter$1.print(createFunctionParameters({ params: [createFunctionParameter({
232
+ name: "data",
233
+ type: `${paramType} | ${callbackType}`,
234
+ optional: true
235
+ })] }));
236
+ const httpCall = requestTypeName ? `http.${method}<Record<string, string>, ${requestTypeName}>` : `http.${method}`;
237
+ const requestUrl = `${baseURL}${url.replace(/([^/]):/g, "$1\\\\:")}`;
238
+ const urlLiteral = fakerName ? `'${requestUrl}'` : `\`${requestUrl}\``;
239
+ const responseBody = fakerName ? `JSON.stringify(data || ${fakerName}(data))` : "JSON.stringify(data)";
240
+ return /* @__PURE__ */ jsx(File.Source, {
241
+ name,
242
+ isIndexable: true,
243
+ isExportable: true,
244
+ children: /* @__PURE__ */ jsx(Function, {
245
+ name,
246
+ export: true,
247
+ params: params ?? "",
248
+ children: `return ${httpCall}(${urlLiteral}, function handler(info) {
249
+ if(typeof data === 'function') return data(info)
250
+
251
+ return new Response(${responseBody}, {
252
+ status: ${statusCode},
253
+ ${headers.length ? ` headers: {
254
+ ${headers.join(", \n")}
255
+ },` : ""}
256
+ })
257
+ })`
258
+ })
259
+ });
260
+ }
261
+ //#endregion
262
+ //#region src/components/Response.tsx
263
+ const declarationPrinter = functionPrinter({ mode: "declaration" });
264
+ function Response({ name, typeName, response }) {
265
+ const statusCode = Number(response.statusCode);
266
+ const contentType = getContentType(response);
267
+ const headers = [contentType ? `'Content-Type': '${contentType}'` : null].filter(Boolean);
268
+ const params = declarationPrinter.print(createFunctionParameters({ params: [createFunctionParameter({
269
+ name: "data",
270
+ type: typeName,
271
+ optional: !hasResponseSchema(response)
272
+ })] }));
273
+ const responseName = `${name}Response${statusCode}`;
274
+ return /* @__PURE__ */ jsx(File.Source, {
275
+ name: responseName,
276
+ isIndexable: true,
277
+ isExportable: true,
278
+ children: /* @__PURE__ */ jsx(Function, {
279
+ name: responseName,
280
+ export: true,
281
+ params: params ?? "",
282
+ children: `
283
+ return new Response(JSON.stringify(data), {
284
+ status: ${statusCode},
285
+ ${headers.length ? ` headers: {
286
+ ${headers.join(", \n")}
287
+ },` : ""}
288
+ })`
289
+ })
290
+ });
291
+ }
292
+ //#endregion
293
+ //#region src/generators/mswGenerator.tsx
294
+ /**
295
+ * Built-in operation generator for `@kubb/plugin-msw`. Emits one MSW handler
296
+ * per OpenAPI operation. With `parser: 'faker'` the handler returns a value
297
+ * from `@kubb/plugin-faker`; with `parser: 'data'` it returns a typed empty
298
+ * payload for tests to fill in.
299
+ */
300
+ const mswGenerator = defineGenerator({
301
+ name: "msw",
302
+ renderer: jsxRenderer,
303
+ operation(node, ctx) {
304
+ if (!ast.isHttpOperationNode(node)) return null;
305
+ const { driver, resolver, config, root } = ctx;
306
+ const { output, parser, baseURL, group } = ctx.options;
307
+ const fileName = resolver.name(node.operationId);
308
+ const mock = {
309
+ name: resolver.handler.name(node),
310
+ file: resolver.file({
311
+ name: fileName,
312
+ extname: ".ts",
313
+ tag: node.tags[0] ?? "default",
314
+ path: node.path,
315
+ root,
316
+ output,
317
+ group: group ?? void 0
318
+ })
319
+ };
320
+ const fakerPlugin = parser === "faker" ? driver.getPlugin(pluginFakerName) : null;
321
+ const faker = parser === "faker" && fakerPlugin ? resolveFakerMeta(node, {
322
+ root,
323
+ fakerResolver: driver.getResolver(pluginFakerName),
324
+ fakerOutput: fakerPlugin.options?.output ?? output,
325
+ fakerGroup: fakerPlugin.options?.group ?? null
326
+ }) : null;
327
+ const pluginTs = driver.getPlugin(pluginTsName);
328
+ if (!pluginTs) return null;
329
+ const tsResolver = driver.getResolver(pluginTsName);
330
+ const type = {
331
+ file: tsResolver.file({
332
+ name: node.operationId,
333
+ extname: ".ts",
334
+ tag: node.tags[0] ?? "default",
335
+ path: node.path,
336
+ root,
337
+ output: pluginTs.options?.output ?? output,
338
+ group: pluginTs.options?.group ?? void 0
339
+ }),
340
+ responseName: tsResolver.response.response(node)
341
+ };
342
+ const types = resolveResponseTypes(node, tsResolver);
343
+ const hasSuccessSchema = getOperationSuccessResponses(node).some((response) => !!response.content?.[0]?.schema);
344
+ const requestName = node.requestBody?.content?.[0]?.schema ? tsResolver.response.body(node) : null;
345
+ return /* @__PURE__ */ jsxs(File, {
346
+ baseName: mock.file.baseName,
347
+ path: mock.file.path,
348
+ meta: mock.file.meta,
349
+ banner: resolver.default.banner(ctx.meta, {
350
+ output,
351
+ config,
352
+ file: {
353
+ path: mock.file.path,
354
+ baseName: mock.file.baseName
355
+ }
356
+ }),
357
+ footer: resolver.default.footer(ctx.meta, {
358
+ output,
359
+ config,
360
+ file: {
361
+ path: mock.file.path,
362
+ baseName: mock.file.baseName
363
+ }
364
+ }),
365
+ children: [
366
+ /* @__PURE__ */ jsx(File.Import, {
367
+ name: ["http"],
368
+ path: "msw"
369
+ }),
370
+ /* @__PURE__ */ jsx(File.Import, {
371
+ name: ["HttpResponseResolver"],
372
+ isTypeOnly: true,
373
+ path: "msw"
374
+ }),
375
+ /* @__PURE__ */ jsx(File.Import, {
376
+ name: Array.from(/* @__PURE__ */ new Set([
377
+ type.responseName,
378
+ ...types.map((t) => t[1]),
379
+ ...requestName ? [requestName] : []
380
+ ])),
381
+ path: type.file.path,
382
+ root: mock.file.path,
383
+ isTypeOnly: true
384
+ }),
385
+ parser === "faker" && faker && /* @__PURE__ */ jsx(File.Import, {
386
+ name: [faker.name],
387
+ root: mock.file.path,
388
+ path: faker.file.path
389
+ }),
390
+ types.filter(([code]) => code !== "default").map(([code, typeName]) => {
391
+ const response = node.responses.find((item) => item.statusCode === String(code));
392
+ if (!response) return null;
393
+ return /* @__PURE__ */ jsx(Response, {
394
+ typeName,
395
+ response,
396
+ name: mock.name
397
+ }, typeName);
398
+ }),
399
+ parser === "faker" && faker && hasSuccessSchema ? /* @__PURE__ */ jsx(Mock, {
400
+ name: mock.name,
401
+ typeName: type.responseName,
402
+ requestTypeName: requestName,
403
+ fakerName: faker.name,
404
+ node,
405
+ baseURL
406
+ }) : /* @__PURE__ */ jsx(Mock, {
407
+ name: mock.name,
408
+ typeName: type.responseName,
409
+ requestTypeName: requestName,
410
+ node,
411
+ baseURL
412
+ })
413
+ ]
414
+ });
415
+ }
416
+ });
417
+ //#endregion
7
418
  //#region src/resolvers/resolverMsw.ts
8
419
  /**
9
- * Naming convention resolver for MSW plugin.
420
+ * Default resolver used by `@kubb/plugin-msw`. Decides the names and file
421
+ * paths for every generated MSW handler. Function names get a `Handler`
422
+ * suffix; the aggregate export is always `handlers`.
423
+ *
424
+ * The top-level `name` applies the `handler` suffix, and `file` falls back to the built-in
425
+ * `toFilePath` casing. Operation-specific naming is grouped under the `handler` namespace.
10
426
  *
11
- * Provides default naming helpers using camelCase with a `handler` suffix.
427
+ * @example Resolve a handler name
428
+ * ```ts
429
+ * import { resolverMsw } from '@kubb/plugin-msw'
430
+ *
431
+ * resolverMsw.name('addPet') // 'addPetHandler'
432
+ * ```
12
433
  */
13
- const resolverMsw = defineResolver(() => ({
14
- name: "default",
434
+ const resolverMsw = createResolver({
15
435
  pluginName: "plugin-msw",
16
- default(name, type) {
17
- return camelCase(name, { isFile: type === "file" });
18
- },
19
- resolveName(name) {
436
+ name(name) {
20
437
  return camelCase(name, { suffix: "handler" });
21
438
  },
22
- resolvePathName(name, type) {
23
- return this.default(name, type);
24
- },
25
- resolveHandlerName(node) {
26
- return this.resolveName(node.operationId);
27
- },
28
- resolveHandlersName() {
29
- return "handlers";
439
+ handler: {
440
+ name(node) {
441
+ return this.name(node.operationId);
442
+ },
443
+ listName() {
444
+ return "handlers";
445
+ }
30
446
  }
31
- }));
447
+ });
32
448
  //#endregion
33
449
  //#region src/plugin.ts
450
+ /**
451
+ * Canonical plugin name for `@kubb/plugin-msw`. Used for driver lookups and
452
+ * cross-plugin dependency references.
453
+ */
34
454
  const pluginMswName = "plugin-msw";
455
+ /**
456
+ * Generates MSW request handlers from an OpenAPI spec. Drop them into your
457
+ * test setup or service worker to mock the API end-to-end. Request path,
458
+ * method, status, and response body all stay in sync with the spec. Combine
459
+ * with `@kubb/plugin-faker` (via `parser: 'faker'`) to seed handlers with
460
+ * realistic data.
461
+ *
462
+ * @example
463
+ * ```ts
464
+ * import { defineConfig } from 'kubb/config'
465
+ * import { pluginTs } from '@kubb/plugin-ts'
466
+ * import { pluginMsw } from '@kubb/plugin-msw'
467
+ *
468
+ * export default defineConfig({
469
+ * input: './petStore.yaml',
470
+ * output: { path: './src/gen' },
471
+ * plugins: [
472
+ * pluginTs(),
473
+ * pluginMsw({
474
+ * output: { path: './handlers' },
475
+ * handlers: true,
476
+ * }),
477
+ * ],
478
+ * })
479
+ * ```
480
+ */
35
481
  const pluginMsw = definePlugin((options) => {
36
482
  const { output = {
37
483
  path: "handlers",
38
- barrelType: "named"
39
- }, group, exclude = [], include, override = [], handlers = false, parser = "data", baseURL, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
40
- const groupConfig = group ? {
41
- ...group,
42
- name: group.name ? group.name : (ctx) => {
43
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
44
- return `${camelCase(ctx.group)}Controller`;
45
- }
46
- } : void 0;
484
+ barrel: { type: "named" }
485
+ }, group, exclude = [], include, override = [], handlers = false, parser = "data", baseURL, resolver: userResolver, macros: userMacros } = options;
486
+ const groupConfig = createGroupConfig(group);
47
487
  return {
48
488
  name: pluginMswName,
49
489
  options,
50
- dependencies: [pluginTsName, parser === "faker" ? pluginFakerName : void 0].filter((dependency) => Boolean(dependency)),
490
+ dependencies: [pluginTsName, parser === "faker" ? pluginFakerName : null].filter((dependency) => Boolean(dependency)),
51
491
  hooks: { "kubb:plugin:setup"(ctx) {
52
- const resolver = userResolver ? {
53
- ...resolverMsw,
54
- ...userResolver
55
- } : resolverMsw;
492
+ const resolver = userResolver ? Resolver.merge(resolverMsw, userResolver) : resolverMsw;
56
493
  ctx.setOptions({
57
494
  output,
58
495
  parser,
@@ -65,14 +502,13 @@ const pluginMsw = definePlugin((options) => {
65
502
  resolver
66
503
  });
67
504
  ctx.setResolver(resolver);
68
- if (userTransformer) ctx.setTransformer(userTransformer);
505
+ if (userMacros?.length) ctx.setMacros(userMacros);
69
506
  ctx.addGenerator(mswGenerator);
70
507
  if (handlers) ctx.addGenerator(handlersGenerator);
71
- for (const gen of userGenerators) ctx.addGenerator(gen);
72
508
  } }
73
509
  };
74
510
  });
75
511
  //#endregion
76
- export { pluginMsw as default, pluginMsw, pluginMswName };
512
+ export { pluginMsw as default, pluginMsw, pluginMswName, resolverMsw };
77
513
 
78
514
  //# sourceMappingURL=index.js.map