swaggie 1.8.8 → 1.9.0-alpha.10

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.
Files changed (47) hide show
  1. package/README.md +116 -31
  2. package/dist/browser.js +24 -5
  3. package/dist/cli.js +53 -3
  4. package/dist/gen/genMocks.js +316 -0
  5. package/dist/gen/genOperations.js +79 -5
  6. package/dist/gen/genTypes.js +17 -7
  7. package/dist/gen/header.js +1 -0
  8. package/dist/gen/index.js +9 -0
  9. package/dist/gen/jsDocs.js +35 -1
  10. package/dist/generated/bundledTemplates.js +21 -23
  11. package/dist/index.js +17 -1
  12. package/dist/swagger/typesExtractor.js +10 -3
  13. package/dist/types.d.ts +54 -4
  14. package/dist/utils/fileUtils.js +22 -0
  15. package/dist/utils/templateEngine.js +20 -0
  16. package/dist/utils/templateManager.js +123 -14
  17. package/dist/utils/templateValidator.js +127 -0
  18. package/package.json +1 -1
  19. package/templates/axios/baseClient.ejs +0 -11
  20. package/templates/axios/operation.ejs +4 -0
  21. package/templates/fetch/baseClient.ejs +0 -11
  22. package/templates/fetch/operation.ejs +4 -0
  23. package/templates/ng1/baseClient.ejs +0 -11
  24. package/templates/ng1/operation.ejs +3 -3
  25. package/templates/ng2/baseClient.ejs +1 -61
  26. package/templates/ng2/client.ejs +3 -7
  27. package/templates/ng2/operation.ejs +20 -13
  28. package/templates/swr/baseClient.ejs +7 -0
  29. package/templates/swr/client.ejs +63 -0
  30. package/templates/swr/swrMutationOperation.ejs +32 -0
  31. package/templates/swr/swrOperation.ejs +18 -0
  32. package/templates/tsq/baseClient.ejs +1 -0
  33. package/templates/tsq/client.ejs +67 -0
  34. package/templates/tsq/mutationOperation.ejs +31 -0
  35. package/templates/tsq/queryOperation.ejs +19 -0
  36. package/templates/xior/baseClient.ejs +1 -12
  37. package/templates/xior/operation.ejs +4 -0
  38. package/templates/swr-axios/barrel.ejs +0 -58
  39. package/templates/swr-axios/baseClient.ejs +0 -30
  40. package/templates/swr-axios/client.ejs +0 -21
  41. package/templates/swr-axios/operation.ejs +0 -40
  42. package/templates/swr-axios/swrOperation.ejs +0 -50
  43. package/templates/tsq-xior/barrel.ejs +0 -0
  44. package/templates/tsq-xior/baseClient.ejs +0 -25
  45. package/templates/tsq-xior/client.ejs +0 -22
  46. package/templates/tsq-xior/operation.ejs +0 -40
  47. package/templates/tsq-xior/queryOperation.ejs +0 -30
package/README.md CHANGED
@@ -21,7 +21,8 @@ See the [Example section](#example) for a quick demo, or visit the full document
21
21
  ## Features
22
22
 
23
23
  - Generates TypeScript code from OpenAPI 3.0, 3.1, and 3.2 specs
24
- - Supports multiple HTTP client libraries out of the box: `fetch`, `axios`, `xior`, `SWR + axios`, `Angular 1`, `Angular 2+`, and `TanStack Query`
24
+ - Supports multiple HTTP client libraries out of the box: `fetch`, `axios`, `xior`, `Angular 1`, `Angular 2+`; with optional reactive layers (`swr`, `tsq`) that compose with any compatible HTTP client
25
+ - **Auto-generated mock/stub files** for Vitest and Jest — typed spies for every client method and hook, with ergonomic helpers like `mockSWR()` and `mockQuery()`
25
26
  - Custom templates — bring your own to fit your existing codebase
26
27
  - Supports `allOf`, `oneOf`, `anyOf`, `$ref`, nullable types, and various enum definitions
27
28
  - Handles multiple content types: JSON, plain text, multipart/form-data, URL-encoded, and binary
@@ -72,24 +73,27 @@ swaggie -s https://petstore3.swagger.io/api/v3/openapi.json -o ./client/petstore
72
73
  **Available options:**
73
74
 
74
75
  ```
75
- -V, --version Output the version number
76
- -c, --config <path> Path to a JSON configuration file
77
- -s, --src <url|path> URL or file path to the OpenAPI spec
78
- -o, --out <filePath> Output file path (omit to print to stdout)
79
- -b, --baseUrl <string> Default base URL for the generated client (default: "")
80
- -t, --template <string> Template to use for code generation (default: "axios")
81
- -m, --mode <mode> Generation mode: "full" or "schemas" (default: "full")
82
- -d, --schemaStyle <style> Schema object style: "interface" or "type" (default: "interface")
83
- --enumStyle <style> Enum style for plain string enums: "union" or "enum" (default: "union")
84
- --enumNamesStyle <s> Enum member name casing: "original" or "PascalCase" (default: "original")
85
- --dateFormat <format> Date handling in schemas: "Date" or "string"
86
- --nullables <strategy> Nullable handling: "include", "nullableAsOptional", or "ignore"
87
- --preferAny Use "any" instead of "unknown" for untyped values (default: false)
88
- --skipDeprecated Exclude deprecated operations from the output (default: false)
89
- --servicePrefix Prefix for service names — useful when generating multiple APIs
90
- --allowDots Use dot notation to serialize nested object query params
91
- --arrayFormat How arrays are serialized: "indices", "repeat", or "brackets"
92
- -h, --help Show help
76
+ -V, --version Output the version number
77
+ -c, --config <path> Path to a JSON configuration file
78
+ -s, --src <url|path> URL or file path to the OpenAPI spec
79
+ -o, --out <filePath> Output file path (omit to print to stdout)
80
+ -b, --baseUrl <string> Base URL that will be used as a default value in the clients
81
+ -t, --template <string> Template to use. Single name: "axios", "fetch", "xior", "ng1", "ng2", "swr", "tsq". Reactive pair: "swr,axios" / "tsq,xior" / etc. (default: "axios")
82
+ -m, --mode <mode> Generation mode: "full" or "schemas" (default: "full")
83
+ -d, --schemaStyle <style> Schema object style: "interface" or "type" (default: "interface")
84
+ --enumStyle <style> Enum style for plain string enums: "union" or "enum" (default: "union")
85
+ --enumNamesStyle <s> Enum member name casing: "original" or "PascalCase" (default: "original")
86
+ --dateFormat <format> How date fields are emitted in generated types
87
+ --nullables <strategy> Nullable handling: "include", "nullableAsOptional", or "ignore"
88
+ --preferAny Use "any" instead of "unknown" for untyped values (default: false)
89
+ --skipDeprecated Exclude deprecated operations from the output (default: false)
90
+ --servicePrefix Prefix for service names — useful when generating multiple APIs
91
+ --allowDots Use dot notation to serialize nested object query params
92
+ --arrayFormat How arrays are serialized: "indices", "repeat", or "brackets"
93
+ -C, --useClient Prepend 'use client'; directive (Next.js App Router + SWR/TSQ)
94
+ --mocks <path> Output path for a generated mock/stub file (requires --testingFramework and --out)
95
+ -T, --testingFramework <name> Framework for generated mocks: "vitest" or "jest" (requires --mocks and --out)
96
+ -h, --help Show help
93
97
  ```
94
98
 
95
99
  ### Formatting the Output
@@ -142,24 +146,72 @@ The `$schema` field enables autocompletion and inline documentation in most edit
142
146
 
143
147
  ## Templates
144
148
 
145
- Swaggie ships with the following built-in templates:
149
+ Swaggie's templates are split into two independent layers that you can combine freely.
146
150
 
147
- | Template | Description |
148
- | ----------- | ----------------------------------------------------------------------------------------- |
149
- | `axios` | Default. Recommended for React, Vue, and similar frameworks |
150
- | `xior` | Lightweight modern alternative to axios ([xior](https://github.com/suhaotian/xior#intro)) |
151
- | `swr-axios` | SWR hooks for GET requests, backed by axios |
152
- | `tsq-xior` | TanStack Query hooks for GET requests, backed by xior |
153
- | `fetch` | Uses the native browser Fetch API |
154
- | `ng1` | Angular 1 client |
155
- | `ng2` | Angular 2+ client (uses HttpClient and InjectionTokens) |
151
+ ### HTTP client templates (L1)
156
152
 
157
- To use a custom template, pass the path to your template directory:
153
+ These are standalone and cover the most common client libraries:
154
+
155
+ | Template | Description |
156
+ | -------- | ----------- |
157
+ | `axios` | Default. Recommended for React, Vue, and most Node.js projects |
158
+ | `fetch` | Native browser/Node 18+ Fetch API — zero runtime dependencies |
159
+ | `xior` | Lightweight Axios-compatible alternative ([xior](https://github.com/suhaotian/xior#intro)) |
160
+ | `ng1` | Angular 1 client |
161
+ | `ng2` | Angular 2+ client (uses `HttpClient` and `InjectionToken`) |
162
+
163
+ ### Reactive query layer templates (L2)
164
+
165
+ These add a reactive data-fetching layer (SWR or TanStack Query hooks) on top of any compatible L1 client. They cannot be used alone — pair them with an L1 template using a 2-element array:
166
+
167
+ | Template | Description |
168
+ | -------- | ----------- |
169
+ | `swr` | [SWR](https://swr.vercel.app) hooks for queries and mutations |
170
+ | `tsq` | [TanStack Query](https://tanstack.com/query) hooks for queries and mutations |
171
+
172
+ Compatible L1 templates for L2: `axios`, `fetch`, `xior`. Angular clients are not compatible with reactive layers.
173
+
174
+ ### Usage examples
175
+
176
+ **Single L1 template (existing behaviour):**
177
+
178
+ ```json
179
+ { "template": "axios" }
180
+ ```
181
+
182
+ ```bash
183
+ swaggie -s ./openapi.json -o ./client.ts -t axios
184
+ ```
185
+
186
+ **L2 + L1 combination — in config:**
187
+
188
+ ```json
189
+ { "template": ["swr", "axios"] }
190
+ ```
191
+
192
+ ```bash
193
+ # CLI: comma-separated pair
194
+ swaggie -s ./openapi.json -o ./client.ts -t swr,axios
195
+ swaggie -s ./openapi.json -o ./client.ts -t tsq,xior
196
+ swaggie -s ./openapi.json -o ./client.ts -t swr,fetch
197
+ ```
198
+
199
+ **L2 alone — defaults to `fetch` as the L1:**
200
+
201
+ ```json
202
+ { "template": "swr" }
203
+ ```
204
+
205
+ ### Custom templates
206
+
207
+ Pass the path to your own template directory as the template value:
158
208
 
159
209
  ```bash
160
210
  swaggie -s https://petstore3.swagger.io/api/v3/openapi.json -o ./client/petstore.ts --template ./my-swaggie-template/
161
211
  ```
162
212
 
213
+ Custom paths also work as part of a composite pair: `["./my-l2", "axios"]`.
214
+
163
215
  ---
164
216
 
165
217
  ## Example
@@ -179,7 +231,7 @@ import Axios, { AxiosPromise } from 'axios';
179
231
 
180
232
  const axios = Axios.create({
181
233
  baseURL: '/api',
182
- paramsSerializer: (params) =>
234
+ paramsSerializer: (params: any) =>
183
235
  encodeParams(params, null, {
184
236
  allowDots: true,
185
237
  arrayFormat: 'repeat',
@@ -359,6 +411,39 @@ Either tool needs to be installed separately and configured for your project.
359
411
 
360
412
  ---
361
413
 
414
+ ## Generating Mocks
415
+
416
+ Swaggie can generate a companion mock file alongside your client — a set of typed spy stubs for every method and hook, ready to drop into your tests.
417
+
418
+ ```bash
419
+ swaggie -s ./spec.json -o ./src/api/client.ts -t swr,axios \
420
+ --mocks ./src/__mocks__/api.ts --testingFramework vitest
421
+ ```
422
+
423
+ Or in a config file:
424
+
425
+ ```json
426
+ {
427
+ "src": "./openapi.json",
428
+ "out": "./src/api/client.ts",
429
+ "template": ["swr", "axios"],
430
+ "mocks": "./src/__mocks__/api.ts",
431
+ "testingFramework": "vitest"
432
+ }
433
+ ```
434
+
435
+ The generated mock file exports the same names as the real client, so `vi.mock('./api', () => import('./__mocks__/api'))` is all you need in tests. For L2 (SWR/TSQ) templates, hook stubs come with shorthand helpers:
436
+
437
+ ```ts
438
+ pet.queries.usePetById.mockSWR({ data: { id: 1, name: 'Rex' } });
439
+ pet.mutations.useAddPet.mockSWRMutation({ isMutating: false });
440
+ // TanStack Query equivalents: mockQuery() / mockMutation()
441
+ ```
442
+
443
+ See the [Mocking guide](https://yhnavein.github.io/swaggie/guide/mocking) for full details.
444
+
445
+ ---
446
+
362
447
  ## Using Swaggie Programmatically
363
448
 
364
449
  You can also call Swaggie directly from Node.js/bun/deno/etc:
package/dist/browser.js CHANGED
@@ -9,6 +9,8 @@ var _bundledTemplates = require('./generated/bundledTemplates');
9
9
  var _utils = require('./utils/utils');
10
10
  var _documentLoaderbrowser = require('./utils/documentLoader.browser');
11
11
  var _templateEngine = require('./utils/templateEngine');
12
+ var _templateValidator = require('./utils/templateValidator');
13
+ var _templateManager = require('./utils/templateManager');
12
14
 
13
15
  /**
14
16
  * Browser-friendly code generation entrypoint.
@@ -52,12 +54,27 @@ async function generateCode(spec, options) {
52
54
  return _header.FILE_HEADER + _genTypes2.default.call(void 0, spec, options, false);
53
55
  }
54
56
 
55
- const templateFiles = _bundledTemplates.BUNDLED_TEMPLATES[options.template];
56
- if (!templateFiles) {
57
- throw new Error(`Bundled templates for '${options.template}' are not available`);
57
+ _templateValidator.validateTemplate.call(void 0, options.template);
58
+
59
+ if (Array.isArray(options.template)) {
60
+ const [l2, l1] = options.template;
61
+ const l1Files = _bundledTemplates.BUNDLED_TEMPLATES[l1 ];
62
+ const l2Files = _bundledTemplates.BUNDLED_TEMPLATES[l2 ];
63
+ if (!l1Files) {
64
+ throw new Error(`Bundled templates for L1 '${l1}' are not available`);
65
+ }
66
+ if (!l2Files) {
67
+ throw new Error(`Bundled templates for L2 '${l2}' are not available`);
68
+ }
69
+ _templateEngine.initTemplateEngineFromBundled.call(void 0, _templateManager.mergeTemplateFiles.call(void 0, l1Files, l2Files));
70
+ } else {
71
+ const templateFiles = _bundledTemplates.BUNDLED_TEMPLATES[options.template ];
72
+ if (!templateFiles) {
73
+ throw new Error(`Bundled templates for '${options.template}' are not available`);
74
+ }
75
+ _templateEngine.initTemplateEngineFromBundled.call(void 0, templateFiles);
58
76
  }
59
77
 
60
- _templateEngine.initTemplateEngineFromBundled.call(void 0, templateFiles);
61
78
  const operationsCode = await _genOperations2.default.call(void 0, spec, options);
62
79
 
63
80
  return operationsCode + _genTypes2.default.call(void 0, spec, options);
@@ -70,6 +87,7 @@ async function generateCode(spec, options) {
70
87
  const {
71
88
  allowDots,
72
89
  arrayFormat,
90
+ queryParamsAsObject,
73
91
  mode,
74
92
  schemaStyle,
75
93
  enumStyle,
@@ -86,11 +104,12 @@ async function generateCode(spec, options) {
86
104
  ),
87
105
  ...(allowDots !== undefined ? { allowDots } : {}),
88
106
  ...(arrayFormat !== undefined ? { arrayFormat } : {}),
107
+ ...(queryParamsAsObject !== undefined ? { queryParamsAsObject } : {}),
89
108
  };
90
109
 
91
110
  return {
92
111
  ...rest,
93
- template: _nullishCoalesce(template, () => ( _swagger.APP_DEFAULTS.template)),
112
+ template: _templateValidator.normalizeTemplate.call(void 0, _nullishCoalesce(template, () => ( _swagger.APP_DEFAULTS.template))),
94
113
  servicePrefix: _nullishCoalesce(rest.servicePrefix, () => ( _swagger.APP_DEFAULTS.servicePrefix)),
95
114
  nullableStrategy: _nullishCoalesce(_nullishCoalesce(nullables, () => ( rest.nullableStrategy)), () => ( _swagger.APP_DEFAULTS.nullableStrategy)),
96
115
  generationMode: _nullishCoalesce(_nullishCoalesce(mode, () => ( rest.generationMode)), () => ( _swagger.APP_DEFAULTS.generationMode)),
package/dist/cli.js CHANGED
@@ -13,6 +13,11 @@ const arrayFormatOption = new (0, _commander.Option)(
13
13
  'Determines how arrays should be serialized'
14
14
  ).choices(['indices', 'repeat', 'brackets']);
15
15
 
16
+ const testingFrameworkOption = new (0, _commander.Option)(
17
+ '-T, --testingFramework <framework>',
18
+ 'Test framework for generated mock stubs (requires --mocks and --out)'
19
+ ).choices(['vitest', 'jest']);
20
+
16
21
  const packageJson = readPackageJson();
17
22
 
18
23
  const modeOption = new (0, _commander.Option)('-m, --mode <mode>', 'Generation mode').choices([
@@ -39,6 +44,10 @@ const nullableStrategyOption = new (0, _commander.Option)(
39
44
  '--nullables <strategy>',
40
45
  "Controls how OpenAPI 'nullable' is translated into TypeScript types"
41
46
  ).choices(['include', 'nullableAsOptional', 'ignore']);
47
+ const queryParamsAsObjectOption = new (0, _commander.Option)(
48
+ '--queryParamsAsObject [threshold]',
49
+ 'Group query params into a single object; pass a number to group only when query params count is greater than threshold'
50
+ ).argParser(parseQueryParamsAsObjectArg);
42
51
 
43
52
  const program = new (0, _commander.Command)();
44
53
  program
@@ -63,9 +72,18 @@ program
63
72
  .option('-b, --baseUrl <string>', 'Base URL that will be used as a default value in the clients')
64
73
  .option(
65
74
  '-t, --template <string>',
66
- 'Template used for generating API client. Default: "axios". Other: "fetch", "ng1", "ng2", "swr-axios", "xior", "tsq-xior"'
75
+ 'Template used for generating API client. ' +
76
+ 'L1 (HTTP client) templates: "axios" (default), "fetch", "xior", "ng1", "ng2". ' +
77
+ 'L2 (reactive layer) templates must be paired with an L1 using a comma-separated value: ' +
78
+ '"swr,axios", "swr,fetch", "tsq,xior", "tsq,fetch", etc. ' +
79
+ 'Providing only an L2 name (e.g. "swr") defaults to "fetch" as the L1.',
80
+ parseTemplateArg
67
81
  )
68
82
  .option('--preferAny', 'Use "any" type instead of "unknown"')
83
+ .option(
84
+ '-C, --useClient',
85
+ "Prepend 'use client'; to the generated file. Required for Next.js App Router with SWR or TanStack Query hooks"
86
+ )
69
87
  .option(
70
88
  '--skipDeprecated',
71
89
  'Skip deprecated operations. When enabled, deprecated operations will be skipped from the generated code'
@@ -84,13 +102,19 @@ program
84
102
  .addOption(enumStyleOption)
85
103
  .addOption(enumNamesStyleOption)
86
104
  .addOption(dateFormatOption)
87
- .addOption(nullableStrategyOption);
105
+ .addOption(nullableStrategyOption)
106
+ .addOption(queryParamsAsObjectOption)
107
+ .option(
108
+ '--mocks <path>',
109
+ 'Output path for the generated mock/stub file (requires --testingFramework and --out)'
110
+ )
111
+ .addOption(testingFrameworkOption);
88
112
 
89
113
  program.parse(process.argv);
90
114
 
91
115
  const options = program.opts();
92
116
 
93
- _index.runCodeGenerator.call(void 0, options).then(complete, error);
117
+ _index.runCodeGenerator.call(void 0, options ).then(complete, error);
94
118
 
95
119
  function complete([code, opts]) {
96
120
  if (opts.out) {
@@ -116,3 +140,29 @@ function readPackageJson() {
116
140
  throw new Error('Could not read package.json file');
117
141
  }
118
142
  }
143
+
144
+ function parseTemplateArg(value) {
145
+ const parts = value.split(',').map((s) => s.trim());
146
+ if (parts.length === 1) {
147
+ return parts[0];
148
+ }
149
+ if (parts.length === 2) {
150
+ return [parts[0], parts[1]];
151
+ }
152
+ throw new Error(
153
+ `--template accepts at most 2 comma-separated values (e.g. "swr,axios"). Got ${parts.length}.`
154
+ );
155
+ }
156
+
157
+ function parseQueryParamsAsObjectArg(value) {
158
+ if (value === undefined) {
159
+ return true;
160
+ }
161
+
162
+ const threshold = Number(value);
163
+ if (!Number.isInteger(threshold) || threshold < 0) {
164
+ throw new Error('--queryParamsAsObject threshold must be a non-negative integer');
165
+ }
166
+
167
+ return threshold;
168
+ }
@@ -0,0 +1,316 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _case = require('case');
2
+
3
+
4
+ var _swagger = require('../swagger');
5
+ var _utils = require('../utils/utils');
6
+ var _templateValidator = require('../utils/templateValidator');
7
+ var _genOperations = require('./genOperations');
8
+
9
+
10
+
11
+ const MOCK_FILE_HEADER = `\
12
+ /* istanbul ignore file -- auto-generated test helper */
13
+ /* tslint:disable */
14
+ /* eslint-disable */
15
+ //----------------------
16
+ // <auto-generated>
17
+ // Generated using Swaggie (https://github.com/yhnavein/swaggie)
18
+ // Please avoid doing any manual changes in this file
19
+ // </auto-generated>
20
+ //----------------------
21
+ // biome-ignore-all lint: auto-generated code
22
+ // deno-lint-ignore-file
23
+ `;
24
+
25
+ /**
26
+ * Generates a companion mock/stub file for a generated API client.
27
+ *
28
+ * The mock file exports:
29
+ * - Typed spy stubs for every `*Client` object method
30
+ * - When an L2 template (swr/tsq) is active: hook stubs wrapped with
31
+ * `withMockSWR` / `withMockQuery` / `withMockMutation` helpers
32
+ * - `queryKeys` passthrough (re-exported from the real client — not mocked)
33
+ *
34
+ * @param spec The OpenAPI document
35
+ * @param options Generator options (must have `testingFramework` set)
36
+ * @param relativeApiImport Relative import path from the mock file to the real client file
37
+ */
38
+ function generateMocks(
39
+ spec,
40
+ options,
41
+ relativeApiImport
42
+ ) {
43
+ const fw = options.testingFramework;
44
+ const template = options.template;
45
+ const l2 = getL2Name(template);
46
+
47
+ const operations = _swagger.getOperations.call(void 0, spec);
48
+ const groups = _utils.groupOperationsByGroupName.call(void 0, operations);
49
+
50
+ const groupNames = Object.keys(groups);
51
+ const servicePrefix = options.servicePrefix;
52
+
53
+ let result = MOCK_FILE_HEADER;
54
+
55
+ // Framework import
56
+ result += buildFrameworkImport(fw);
57
+ result += '\n';
58
+
59
+ // Real client import — needed for queryKeys passthrough (value import).
60
+ // Only emitted when there is an L2 (hooks layer).
61
+ if (l2) {
62
+ result += `import * as realApi from '${relativeApiImport}';\n`;
63
+ result += '\n';
64
+ }
65
+
66
+ // Helper functions (only when L2 is present)
67
+ if (l2 === 'swr') {
68
+ result += buildSwrHelpers(fw);
69
+ result += '\n';
70
+ } else if (l2 === 'tsq') {
71
+ result += buildTsqHelpers(fw);
72
+ result += '\n';
73
+ }
74
+
75
+ // Per-group stubs
76
+ for (const name of groupNames) {
77
+ const group = groups[name];
78
+ const fullName = servicePrefix + name;
79
+ const camelName = _case.camel.call(void 0, fullName);
80
+ const preparedOps = _genOperations.prepareOperations.call(void 0, group, options, spec.components);
81
+
82
+ if (preparedOps.length === 0) {
83
+ continue;
84
+ }
85
+
86
+ // *Client stub object
87
+ result += buildClientStub(camelName, preparedOps, fw);
88
+ result += '\n';
89
+
90
+ // Hooks stub object (L2 only)
91
+ if (l2 === 'swr') {
92
+ result += buildSwrHooksStub(camelName, preparedOps, fw);
93
+ result += '\n';
94
+ } else if (l2 === 'tsq') {
95
+ result += buildTsqHooksStub(camelName, preparedOps, fw);
96
+ result += '\n';
97
+ }
98
+ }
99
+
100
+ return result;
101
+ } exports.default = generateMocks;
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Helpers: L2 detection
105
+ // ---------------------------------------------------------------------------
106
+
107
+ function getL2Name(template) {
108
+ if (!Array.isArray(template)) {
109
+ return null;
110
+ }
111
+ const [l2] = template;
112
+ if (_templateValidator.isL2Template.call(void 0, l2)) {
113
+ return l2 ;
114
+ }
115
+ return null;
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Framework import line
120
+ // ---------------------------------------------------------------------------
121
+
122
+ function buildFrameworkImport(fw) {
123
+ if (fw === 'vitest') {
124
+ return "import { vi } from 'vitest';\n";
125
+ }
126
+ return "import { jest } from '@jest/globals';\n";
127
+ }
128
+
129
+ function fn(fw) {
130
+ return fw === 'vitest' ? 'vi.fn()' : 'jest.fn()';
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // SWR helper functions
135
+ // ---------------------------------------------------------------------------
136
+
137
+ function buildSwrHelpers(fw) {
138
+ const fnRef = fw === 'vitest' ? 'vi' : 'jest';
139
+ return `\
140
+ // ─── SWR mock helpers ────────────────────────────────────────────────────────
141
+
142
+ /** Augments a spy with a \`mockSWR\` shorthand for useSWR query hooks. */
143
+ function withMockSWR<T extends ReturnType<typeof ${fnRef}.fn>>(spy: T) {
144
+ return Object.assign(spy, {
145
+ mockSWR({ data, isLoading, error }: { data?: unknown; isLoading?: boolean; error?: Error }) {
146
+ spy.mockReturnValue({ data, isLoading: isLoading ?? false, error: error ?? null, mutate: ${fn(fw)} });
147
+ },
148
+ });
149
+ }
150
+
151
+ /** Augments a spy with a \`mockSWRMutation\` shorthand for useSWRMutation hooks. */
152
+ function withMockSWRMutation<T extends ReturnType<typeof ${fnRef}.fn>>(spy: T) {
153
+ return Object.assign(spy, {
154
+ mockSWRMutation({ data, isMutating, error }: { data?: unknown; isMutating?: boolean; error?: Error }) {
155
+ spy.mockReturnValue({ trigger: ${fn(fw)}, isMutating: isMutating ?? false, error: error ?? null, data });
156
+ },
157
+ });
158
+ }
159
+ `;
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // TanStack Query helper functions
164
+ // ---------------------------------------------------------------------------
165
+
166
+ function buildTsqHelpers(fw) {
167
+ const fnRef = fw === 'vitest' ? 'vi' : 'jest';
168
+ return `\
169
+ // ─── TanStack Query mock helpers ─────────────────────────────────────────────
170
+
171
+ /** Augments a spy with a \`mockQuery\` shorthand for useQuery hooks. */
172
+ function withMockQuery<T extends ReturnType<typeof ${fnRef}.fn>>(spy: T) {
173
+ return Object.assign(spy, {
174
+ mockQuery({ data, isLoading, error }: { data?: unknown; isLoading?: boolean; error?: Error }) {
175
+ const pending = isLoading ?? false;
176
+ spy.mockReturnValue({
177
+ data,
178
+ isLoading: pending,
179
+ isPending: pending,
180
+ isFetching: false,
181
+ isSuccess: data !== undefined && !pending,
182
+ error: error ?? null,
183
+ refetch: ${fn(fw)},
184
+ status: pending ? 'pending' : 'success',
185
+ });
186
+ },
187
+ });
188
+ }
189
+
190
+ /** Augments a spy with a \`mockMutation\` shorthand for useMutation hooks. */
191
+ function withMockMutation<T extends ReturnType<typeof ${fnRef}.fn>>(spy: T) {
192
+ return Object.assign(spy, {
193
+ mockMutation({ data, isPending, error }: { data?: unknown; isPending?: boolean; error?: Error }) {
194
+ spy.mockReturnValue({
195
+ mutate: ${fn(fw)},
196
+ mutateAsync: ${fn(fw)},
197
+ isPending: isPending ?? false,
198
+ isSuccess: false,
199
+ error: error ?? null,
200
+ data,
201
+ reset: ${fn(fw)},
202
+ });
203
+ },
204
+ });
205
+ }
206
+ `;
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // *Client stub object
211
+ // ---------------------------------------------------------------------------
212
+
213
+ function buildClientStub(
214
+ camelName,
215
+ operations,
216
+ fw
217
+ ) {
218
+ const lines = operations.map((op) => ` ${op.name}: ${fn(fw)},`);
219
+ return `export const ${camelName}Client = {\n${lines.join('\n')}\n};\n`;
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // SWR hooks stub object
224
+ // ---------------------------------------------------------------------------
225
+
226
+ function buildSwrHooksStub(
227
+ camelName,
228
+ operations,
229
+ fw
230
+ ) {
231
+ const getOps = operations.filter((o) => o.method === 'GET');
232
+ const mutOps = operations.filter((o) => o.method !== 'GET');
233
+
234
+ const queryLines = getOps.map((op) => {
235
+ const hookName = toHookName(op.name, 'use');
236
+ return ` ${hookName}: withMockSWR(${fn(fw)}),`;
237
+ });
238
+
239
+ const mutationLines = mutOps.map((op) => {
240
+ const hookName = 'use' + _case.pascal.call(void 0, op.name);
241
+ return ` ${hookName}: withMockSWRMutation(${fn(fw)}),`;
242
+ });
243
+
244
+ return [
245
+ `export const ${camelName} = {`,
246
+ ` queries: {`,
247
+ ...queryLines,
248
+ ` },`,
249
+ ``,
250
+ ` mutations: {`,
251
+ ...mutationLines,
252
+ ` },`,
253
+ ``,
254
+ ` // queryKeys are pure functions — no mocking needed`,
255
+ ` queryKeys: realApi.${camelName}.queryKeys,`,
256
+ `};\n`,
257
+ ].join('\n');
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // TanStack Query hooks stub object
262
+ // ---------------------------------------------------------------------------
263
+
264
+ function buildTsqHooksStub(
265
+ camelName,
266
+ operations,
267
+ fw
268
+ ) {
269
+ const getOps = operations.filter((o) => o.method === 'GET');
270
+ const mutOps = operations.filter((o) => o.method !== 'GET');
271
+
272
+ const queryLines = getOps.map((op) => {
273
+ const hookName = toHookName(op.name, 'use');
274
+ return ` ${hookName}: withMockQuery(${fn(fw)}),`;
275
+ });
276
+
277
+ const mutationLines = mutOps.map((op) => {
278
+ const hookName = 'use' + _case.pascal.call(void 0, op.name);
279
+ return ` ${hookName}: withMockMutation(${fn(fw)}),`;
280
+ });
281
+
282
+ return [
283
+ `export const ${camelName} = {`,
284
+ ` queries: {`,
285
+ ...queryLines,
286
+ ` },`,
287
+ ``,
288
+ ` mutations: {`,
289
+ ...mutationLines,
290
+ ` },`,
291
+ ``,
292
+ ` // queryKeys are pure functions — no mocking needed`,
293
+ ` queryKeys: realApi.${camelName}.queryKeys,`,
294
+ `};\n`,
295
+ ].join('\n');
296
+ }
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Shared name helpers
300
+ // ---------------------------------------------------------------------------
301
+
302
+ /**
303
+ * Converts an operation name to a React hook name.
304
+ * Strips a leading "get" prefix (case-insensitive) and capitalises, then
305
+ * prepends the given prefix (e.g. "use").
306
+ *
307
+ * @example toHookName('findPetsByStatus', 'use') → 'useFindPetsByStatus'
308
+ * @example toHookName('getPetById', 'use') → 'usePetById'
309
+ */
310
+ function toHookName(operationName, prefix) {
311
+ const stripped = operationName.toLowerCase().startsWith('get')
312
+ ? operationName.slice(3)
313
+ : operationName;
314
+ const capitalised = stripped.charAt(0).toUpperCase() + stripped.slice(1);
315
+ return prefix + capitalised;
316
+ }