swaggie 2.0.0 → 2.1.0-alpha.4
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/README.md +52 -2
- package/dist/cli.js +19 -1
- package/dist/gen/genMocks.js +48 -19
- package/dist/gen/genOperations.js +226 -5
- package/dist/gen/index.js +61 -3
- package/dist/generated/bundledTemplates.js +23 -10
- package/dist/index.js +28 -0
- package/dist/types.d.ts +32 -2
- package/dist/utils/fileUtils.js +20 -0
- package/dist/utils/templateValidator.js +29 -1
- package/package.json +1 -1
- package/templates/axios/barrel.ejs +1 -1
- package/templates/axios/baseClientSetup.ejs +46 -0
- package/templates/fetch/barrel.ejs +1 -1
- package/templates/fetch/baseClientSetup.ejs +38 -0
- package/templates/ky/barrel.ejs +57 -0
- package/templates/ky/baseClient.ejs +5 -0
- package/templates/ky/baseClientSetup.ejs +60 -0
- package/templates/ky/baseClientWithSetup.ejs +34 -0
- package/templates/ky/client.ejs +6 -0
- package/templates/ky/operation.ejs +50 -0
- package/templates/ng2/barrel.ejs +1 -1
- package/templates/swr/client.ejs +9 -17
- package/templates/swr/hooksClient.ejs +62 -0
- package/templates/swr/swrMutationOperation.ejs +2 -1
- package/templates/swr/swrOperation.ejs +2 -1
- package/templates/tsq/client.ejs +9 -20
- package/templates/tsq/hooksClient.ejs +63 -0
- package/templates/tsq/mutationOperation.ejs +2 -1
- package/templates/tsq/queryOperation.ejs +2 -1
- package/templates/xior/barrel.ejs +1 -0
- package/templates/xior/baseClientSetup.ejs +50 -0
package/README.md
CHANGED
|
@@ -91,9 +91,12 @@ swaggie -s https://petstore3.swagger.io/api/v3/openapi.json -o ./client/petstore
|
|
|
91
91
|
--servicePrefix Prefix for service names — useful when generating multiple APIs
|
|
92
92
|
--allowDots Use dot notation to serialize nested object query params
|
|
93
93
|
--arrayFormat How arrays are serialized: "indices", "repeat", or "brackets"
|
|
94
|
-
-C, --useClient Prepend 'use client';
|
|
94
|
+
-C, --useClient Prepend 'use client'; to the hooks file (with --hooksOut) or the main file (single-file mode)
|
|
95
|
+
--hooksOut <filePath> Output path for the generated hooks file (L2 templates only). Splits hooks into a separate server-safe file
|
|
95
96
|
--mocks <path> Output path for a generated mock/stub file (requires --testingFramework and --out)
|
|
96
97
|
-T, --testingFramework <name> Framework for generated mocks: "vitest" or "jest" (requires --mocks and --out)
|
|
98
|
+
--clientSetup <path> Output path for the write-once client setup file. Generated on first run; never overwritten unless --forceSetup is set. For the ky template, the generated api.ts imports from this file. For other templates, it is a standalone scaffold. Requires --out
|
|
99
|
+
--forceSetup Overwrite the setup file even if it already exists (requires --clientSetup)
|
|
97
100
|
-h, --help Show help
|
|
98
101
|
```
|
|
99
102
|
|
|
@@ -158,6 +161,7 @@ These are standalone and cover the most common client libraries:
|
|
|
158
161
|
| `axios` | Default. Recommended for React, Vue, and most Node.js projects |
|
|
159
162
|
| `fetch` | Native browser/Node 18+ Fetch API — zero runtime dependencies |
|
|
160
163
|
| `xior` | Lightweight Axios-compatible alternative ([xior](https://github.com/suhaotian/xior#intro)) |
|
|
164
|
+
| `ky` | Modern fetch-based HTTP client with hooks ([ky](https://github.com/sindresorhus/ky)) |
|
|
161
165
|
| `ng1` | Angular 1 client |
|
|
162
166
|
| `ng2` | Angular 2+ client (uses `HttpClient` and `InjectionToken`) |
|
|
163
167
|
|
|
@@ -170,7 +174,7 @@ These add a reactive data-fetching layer (SWR or TanStack Query hooks) on top of
|
|
|
170
174
|
| `swr` | [SWR](https://swr.vercel.app) hooks for queries and mutations |
|
|
171
175
|
| `tsq` | [TanStack Query](https://tanstack.com/query) hooks for queries and mutations |
|
|
172
176
|
|
|
173
|
-
Compatible http client templates: `axios`, `fetch`, `xior`. Angular clients are not compatible with reactive layers.
|
|
177
|
+
Compatible http client templates: `axios`, `fetch`, `xior`, `ky`. Angular clients are not compatible with reactive layers.
|
|
174
178
|
|
|
175
179
|
### Usage examples
|
|
176
180
|
|
|
@@ -412,6 +416,52 @@ Either tool needs to be installed separately and configured for your project.
|
|
|
412
416
|
|
|
413
417
|
---
|
|
414
418
|
|
|
419
|
+
## Next.js App Router — Split-file Mode
|
|
420
|
+
|
|
421
|
+
SWR and TanStack Query hooks can only run in React Client Components. In Next.js App Router projects you may want:
|
|
422
|
+
|
|
423
|
+
- The HTTP clients and types available on **both** the server and client sides
|
|
424
|
+
- The reactive hooks restricted to **Client Components only** (with `'use client';`)
|
|
425
|
+
|
|
426
|
+
Use `--hooksOut` to generate two separate files:
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
swaggie -s ./openapi.yaml \
|
|
430
|
+
-o ./src/api/client.ts \
|
|
431
|
+
--hooksOut ./src/api/hooks.ts \
|
|
432
|
+
-t swr,axios \
|
|
433
|
+
--useClient
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Or in a config file:
|
|
437
|
+
|
|
438
|
+
```json
|
|
439
|
+
{
|
|
440
|
+
"src": "./openapi.yaml",
|
|
441
|
+
"out": "./src/api/client.ts",
|
|
442
|
+
"hooksOut": "./src/api/hooks.ts",
|
|
443
|
+
"template": ["swr", "axios"],
|
|
444
|
+
"useClient": true
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
This produces:
|
|
449
|
+
|
|
450
|
+
- `client.ts` — HTTP client objects + TypeScript types. No `'use client'` directive. Safe to import in Server Components and API routes.
|
|
451
|
+
- `hooks.ts` — Reactive hook namespaces. Has `'use client';` at the top. Imports the main file as `import * as API from './client'`.
|
|
452
|
+
|
|
453
|
+
In your components:
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
// Server Component or API route — no 'use client' needed
|
|
457
|
+
import { petClient } from './api/client';
|
|
458
|
+
|
|
459
|
+
// Client Component only
|
|
460
|
+
import { pet } from './api/hooks';
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
415
465
|
## Generating Mocks
|
|
416
466
|
|
|
417
467
|
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.
|
package/dist/cli.js
CHANGED
|
@@ -101,11 +101,29 @@ program
|
|
|
101
101
|
.addOption(dateFormatOption)
|
|
102
102
|
.addOption(nullableStrategyOption)
|
|
103
103
|
.addOption(queryParamsAsObjectOption)
|
|
104
|
+
.option(
|
|
105
|
+
'--hooksOut <filePath>',
|
|
106
|
+
'Output path for the generated hooks file (L2 templates only). ' +
|
|
107
|
+
'When set, reactive hooks are written to this file and the main --out file contains only HTTP clients and types. ' +
|
|
108
|
+
'The hooks file imports the main file as `import * as API from \'./api\'`. ' +
|
|
109
|
+
'Use together with --useClient for Next.js App Router.'
|
|
110
|
+
)
|
|
104
111
|
.option(
|
|
105
112
|
'--mocks <path>',
|
|
106
113
|
'Output path for the generated mock/stub file (requires --testingFramework and --out)'
|
|
107
114
|
)
|
|
108
|
-
.addOption(testingFrameworkOption)
|
|
115
|
+
.addOption(testingFrameworkOption)
|
|
116
|
+
.option(
|
|
117
|
+
'--clientSetup <path>',
|
|
118
|
+
'Output path for the write-once client setup file. ' +
|
|
119
|
+
'Generated on the first run; never overwritten on subsequent runs (use --forceSetup to override). ' +
|
|
120
|
+
'For the ky template, the generated api.ts imports from this file. ' +
|
|
121
|
+
'For other templates, it is a standalone scaffold. Requires --out.'
|
|
122
|
+
)
|
|
123
|
+
.option(
|
|
124
|
+
'--forceSetup',
|
|
125
|
+
'Overwrite the client setup file even if it already exists (requires --clientSetup)'
|
|
126
|
+
);
|
|
109
127
|
|
|
110
128
|
program.parse(process.argv);
|
|
111
129
|
|
package/dist/gen/genMocks.js
CHANGED
|
@@ -31,14 +31,18 @@ const MOCK_FILE_HEADER = `\
|
|
|
31
31
|
* helpers (`mockSWR`, `mockQuery`, etc.) — L2 templates only (swr/tsq)
|
|
32
32
|
* - `ApiHookMocks` type alias — L2 templates only
|
|
33
33
|
*
|
|
34
|
-
* @param spec
|
|
35
|
-
* @param options
|
|
36
|
-
* @param relativeApiImport
|
|
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 main client file
|
|
37
|
+
* @param relativeHooksImport Relative import path from the mock file to the hooks file
|
|
38
|
+
* (only provided when --hooksOut is set). When present, hook mocks
|
|
39
|
+
* reference the hooks file instead of the main file.
|
|
37
40
|
*/
|
|
38
41
|
function generateMocks(
|
|
39
42
|
spec,
|
|
40
43
|
options,
|
|
41
|
-
relativeApiImport
|
|
44
|
+
relativeApiImport,
|
|
45
|
+
relativeHooksImport
|
|
42
46
|
) {
|
|
43
47
|
const fw = options.testingFramework;
|
|
44
48
|
const template = options.template;
|
|
@@ -91,8 +95,14 @@ const MOCK_FILE_HEADER = `\
|
|
|
91
95
|
|
|
92
96
|
// ── Standard path (axios / fetch / xior / swr / tsq) ───────────────────────
|
|
93
97
|
|
|
94
|
-
// Real client import — needed for spyOn targets
|
|
98
|
+
// Real client import — needed for spyOn targets on *Client methods
|
|
95
99
|
result += `import * as realApi from '${relativeApiImport}';\n`;
|
|
100
|
+
|
|
101
|
+
// When hooks live in a separate file (--hooksOut), import that file too for hook spies
|
|
102
|
+
const hooksAlias = relativeHooksImport ? 'realHooks' : 'realApi';
|
|
103
|
+
if (relativeHooksImport && l2) {
|
|
104
|
+
result += `import * as realHooks from '${relativeHooksImport}';\n`;
|
|
105
|
+
}
|
|
96
106
|
result += '\n';
|
|
97
107
|
|
|
98
108
|
// SWR/TSQ helper functions and default return values (L2 only)
|
|
@@ -116,12 +126,12 @@ const MOCK_FILE_HEADER = `\
|
|
|
116
126
|
// ── createApiHookMocks (L2 only) ────────────────────────────────────────────
|
|
117
127
|
if (l2 === 'swr') {
|
|
118
128
|
result += '\n';
|
|
119
|
-
result += buildCreateSwrHookMocks(preparedGroups, fw);
|
|
129
|
+
result += buildCreateSwrHookMocks(preparedGroups, fw, hooksAlias);
|
|
120
130
|
result += '\n';
|
|
121
131
|
result += 'export type ApiHookMocks = ReturnType<typeof createApiHookMocks>;\n';
|
|
122
132
|
} else if (l2 === 'tsq') {
|
|
123
133
|
result += '\n';
|
|
124
|
-
result += buildCreateTsqHookMocks(preparedGroups, fw);
|
|
134
|
+
result += buildCreateTsqHookMocks(preparedGroups, fw, hooksAlias);
|
|
125
135
|
result += '\n';
|
|
126
136
|
result += 'export type ApiHookMocks = ReturnType<typeof createApiHookMocks>;\n';
|
|
127
137
|
}
|
|
@@ -187,7 +197,7 @@ function buildNg2CreateClientMocks(
|
|
|
187
197
|
|
|
188
198
|
function buildFrameworkImport(fw) {
|
|
189
199
|
return fw === 'vitest'
|
|
190
|
-
? "import { vi } from 'vitest';\n"
|
|
200
|
+
? "import { type MockInstance, vi } from 'vitest';\n"
|
|
191
201
|
: "import { jest } from '@jest/globals';\n";
|
|
192
202
|
}
|
|
193
203
|
|
|
@@ -302,14 +312,22 @@ const defaultMutationReturn = {
|
|
|
302
312
|
}
|
|
303
313
|
|
|
304
314
|
function buildTsqHelpers(fw) {
|
|
305
|
-
const ref = fwRef(fw);
|
|
306
315
|
return `\
|
|
307
316
|
// ─── TanStack Query mock helpers ─────────────────────────────────────────────
|
|
308
317
|
|
|
318
|
+
interface MockQueryReturn {
|
|
319
|
+
/** The data to return from the mock */
|
|
320
|
+
data: unknown;
|
|
321
|
+
/** Whether to return a loading state (default: false) */
|
|
322
|
+
isLoading?: boolean;
|
|
323
|
+
/** Whether to return an error (default: undefined) */
|
|
324
|
+
error?: Error | null;
|
|
325
|
+
}
|
|
326
|
+
|
|
309
327
|
/** Augments a spy with a \`mockQuery\` shorthand for useQuery hooks. */
|
|
310
|
-
function withMockQuery<T extends
|
|
328
|
+
function withMockQuery<T extends MockInstance>(spy: T) {
|
|
311
329
|
return Object.assign(spy, {
|
|
312
|
-
mockQuery({ data, isLoading, error }:
|
|
330
|
+
mockQuery({ data, isLoading, error }: MockQueryReturn) {
|
|
313
331
|
const pending = isLoading ?? false;
|
|
314
332
|
spy.mockReturnValue({
|
|
315
333
|
...defaultQueryReturn,
|
|
@@ -324,10 +342,19 @@ function withMockQuery<T extends ReturnType<typeof ${ref}.spyOn>>(spy: T) {
|
|
|
324
342
|
});
|
|
325
343
|
}
|
|
326
344
|
|
|
345
|
+
interface MockMutationReturn {
|
|
346
|
+
/** The data to return from the mock */
|
|
347
|
+
data: unknown;
|
|
348
|
+
/** Whether to return a mutating state (default: false) */
|
|
349
|
+
isPending?: boolean;
|
|
350
|
+
/** Whether to return an error (default: undefined) */
|
|
351
|
+
error?: Error | null;
|
|
352
|
+
}
|
|
353
|
+
|
|
327
354
|
/** Augments a spy with a \`mockMutation\` shorthand for useMutation hooks. */
|
|
328
|
-
function withMockMutation<T extends
|
|
355
|
+
function withMockMutation<T extends MockInstance>(spy: T) {
|
|
329
356
|
return Object.assign(spy, {
|
|
330
|
-
mockMutation({ data, isPending, error }:
|
|
357
|
+
mockMutation({ data, isPending, error }: MockMutationReturn) {
|
|
331
358
|
spy.mockReturnValue({
|
|
332
359
|
...defaultMutationReturn,
|
|
333
360
|
mutate: ${fn(fw)},
|
|
@@ -366,7 +393,8 @@ function buildCreateClientMocks(
|
|
|
366
393
|
|
|
367
394
|
function buildCreateSwrHookMocks(
|
|
368
395
|
groups,
|
|
369
|
-
fw
|
|
396
|
+
fw,
|
|
397
|
+
hooksAlias
|
|
370
398
|
) {
|
|
371
399
|
const spy = spyFn(fw);
|
|
372
400
|
const lines = ['export function createApiHookMocks() {', ' return {'];
|
|
@@ -380,7 +408,7 @@ function buildCreateSwrHookMocks(
|
|
|
380
408
|
for (const op of getOps) {
|
|
381
409
|
const hookName = toHookName(op.name, 'use');
|
|
382
410
|
lines.push(
|
|
383
|
-
` ${hookName}: withMockSWR(${spy}(
|
|
411
|
+
` ${hookName}: withMockSWR(${spy}(${hooksAlias}.${camelName}.queries, '${hookName}').mockReturnValue(defaultSWRReturn)),`
|
|
384
412
|
);
|
|
385
413
|
}
|
|
386
414
|
lines.push(' },');
|
|
@@ -388,7 +416,7 @@ function buildCreateSwrHookMocks(
|
|
|
388
416
|
for (const op of mutOps) {
|
|
389
417
|
const hookName = 'use' + _case.pascal.call(void 0, op.name);
|
|
390
418
|
lines.push(
|
|
391
|
-
` ${hookName}: withMockSWRMutation(${spy}(
|
|
419
|
+
` ${hookName}: withMockSWRMutation(${spy}(${hooksAlias}.${camelName}.mutations, '${hookName}').mockReturnValue(defaultSWRMutationReturn as any)),`
|
|
392
420
|
);
|
|
393
421
|
}
|
|
394
422
|
lines.push(' },');
|
|
@@ -402,7 +430,8 @@ function buildCreateSwrHookMocks(
|
|
|
402
430
|
|
|
403
431
|
function buildCreateTsqHookMocks(
|
|
404
432
|
groups,
|
|
405
|
-
fw
|
|
433
|
+
fw,
|
|
434
|
+
hooksAlias
|
|
406
435
|
) {
|
|
407
436
|
const spy = spyFn(fw);
|
|
408
437
|
const lines = ['export function createApiHookMocks() {', ' return {'];
|
|
@@ -416,7 +445,7 @@ function buildCreateTsqHookMocks(
|
|
|
416
445
|
for (const op of getOps) {
|
|
417
446
|
const hookName = toHookName(op.name, 'use');
|
|
418
447
|
lines.push(
|
|
419
|
-
` ${hookName}: withMockQuery(${spy}(
|
|
448
|
+
` ${hookName}: withMockQuery(${spy}(${hooksAlias}.${camelName}.queries, '${hookName}').mockReturnValue(defaultQueryReturn as any)),`
|
|
420
449
|
);
|
|
421
450
|
}
|
|
422
451
|
lines.push(' },');
|
|
@@ -424,7 +453,7 @@ function buildCreateTsqHookMocks(
|
|
|
424
453
|
for (const op of mutOps) {
|
|
425
454
|
const hookName = 'use' + _case.pascal.call(void 0, op.name);
|
|
426
455
|
lines.push(
|
|
427
|
-
` ${hookName}: withMockMutation(${spy}(
|
|
456
|
+
` ${hookName}: withMockMutation(${spy}(${hooksAlias}.${camelName}.mutations, '${hookName}').mockReturnValue(defaultMutationReturn as any)),`
|
|
428
457
|
);
|
|
429
458
|
}
|
|
430
459
|
lines.push(' },');
|
|
@@ -25,30 +25,48 @@ var _jsDocs = require('./jsDocs');
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Function that will analyze paths in the spec and generate the code for all the operations.
|
|
28
|
+
*
|
|
29
|
+
* @param relativeSetupImport - When `--clientSetup` is active for the ky template, the relative
|
|
30
|
+
* import path from the generated `api.ts` to the setup file (e.g. `'./api.setup'`).
|
|
31
|
+
* Used to embed the import in `baseClientWithSetup.ejs` and passed to each operation
|
|
32
|
+
* as `httpAccessor` (`'getKyHttp()'` vs the default `'http'`).
|
|
28
33
|
*/
|
|
29
34
|
async function generateOperations(
|
|
30
35
|
spec,
|
|
31
|
-
options
|
|
36
|
+
options,
|
|
37
|
+
relativeSetupImport
|
|
32
38
|
) {
|
|
33
39
|
const operations = _swagger.getOperations.call(void 0, spec);
|
|
34
40
|
const groups = _utils.groupOperationsByGroupName.call(void 0, operations);
|
|
35
41
|
const servicePrefix = options.servicePrefix;
|
|
42
|
+
const isKyWithSetup = _templateValidator.getL1Template.call(void 0, options.template) === 'ky' && !!options.clientSetup;
|
|
43
|
+
|
|
36
44
|
const baseClientData = {
|
|
37
45
|
servicePrefix,
|
|
38
46
|
baseUrl: options.baseUrl,
|
|
39
47
|
...options.queryParamsSerialization,
|
|
48
|
+
// For the ky+setup variant, embed the relative import to the setup file
|
|
49
|
+
...(isKyWithSetup && relativeSetupImport ? { relativeSetupImport } : {}),
|
|
40
50
|
};
|
|
41
51
|
|
|
42
52
|
// When a composite [L2, L1] template is used, the L2 base client contains
|
|
43
53
|
// reactive library imports (e.g. useSWR, useQuery). These are placed first
|
|
44
54
|
// so all imports appear at the top of the file before the HTTP client setup.
|
|
55
|
+
// When hooksOut is set, the L2 base client goes into the hooks file instead.
|
|
45
56
|
let baseClients = '';
|
|
46
|
-
if (_templateEngine.hasTemplateFile.call(void 0, 'baseClientL2.ejs')) {
|
|
57
|
+
if (_templateEngine.hasTemplateFile.call(void 0, 'baseClientL2.ejs') && !options.hooksOut) {
|
|
47
58
|
baseClients += _templateEngine.renderFile.call(void 0, 'baseClientL2.ejs', baseClientData);
|
|
48
59
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
// For ky with --clientSetup, use the lazy initKyHttp/getKyHttp pattern.
|
|
61
|
+
// We rely on the template being present (always bundled for ky), so no
|
|
62
|
+
// hasTemplateFile guard — hasTemplateFile returns false for directory templates.
|
|
63
|
+
const baseClientTemplate =
|
|
64
|
+
isKyWithSetup ? 'baseClientWithSetup.ejs' : 'baseClient.ejs';
|
|
65
|
+
baseClients += _templateEngine.renderFile.call(void 0, baseClientTemplate, baseClientData);
|
|
66
|
+
|
|
67
|
+
// When hooksOut is set, the 'use client' directive belongs in the hooks file only.
|
|
68
|
+
// In single-file mode (no hooksOut), keep prepending it to the main file.
|
|
69
|
+
const clientDirective = options.useClient && !options.hooksOut ? "'use client';\n" : '';
|
|
52
70
|
let result = clientDirective + _header.FILE_HEADER + baseClients;
|
|
53
71
|
|
|
54
72
|
for (const name in groups) {
|
|
@@ -63,6 +81,16 @@ var _jsDocs = require('./jsDocs');
|
|
|
63
81
|
...clientData,
|
|
64
82
|
servicePrefix,
|
|
65
83
|
httpConfigType: _templateValidator.getHttpConfigType.call(void 0, options.template),
|
|
84
|
+
responseMapper: _templateValidator.getResponseMapper.call(void 0, options.template),
|
|
85
|
+
// For the ky+setup variant, operations call getKyHttp() instead of the
|
|
86
|
+
// module-level `http` singleton.
|
|
87
|
+
httpAccessor: isKyWithSetup ? 'getKyHttp()' : 'http',
|
|
88
|
+
// In split-file mode, the hooks namespace is generated in a separate file.
|
|
89
|
+
// Pass splitMode=true so the client.ejs template skips the hooks block.
|
|
90
|
+
splitMode: !!options.hooksOut,
|
|
91
|
+
// Template helper functions — defined once here, used in all L2 templates.
|
|
92
|
+
toOpName,
|
|
93
|
+
safeOperation,
|
|
66
94
|
});
|
|
67
95
|
|
|
68
96
|
result += renderedFile;
|
|
@@ -73,6 +101,109 @@ var _jsDocs = require('./jsDocs');
|
|
|
73
101
|
return result;
|
|
74
102
|
} exports.default = generateOperations;
|
|
75
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Generates the content of the write-once client setup scaffold file.
|
|
106
|
+
*
|
|
107
|
+
* For the `ky` template, renders `baseClientSetup.ejs` which exports a
|
|
108
|
+
* `createKyConfig()` function imported by the generated `api.ts`.
|
|
109
|
+
* For other templates (`axios`, `xior`, `fetch`), renders a standalone
|
|
110
|
+
* interceptor scaffold that is NOT imported by `api.ts`.
|
|
111
|
+
*
|
|
112
|
+
* @param relativeApiImport - Relative import path from the setup file back to api.ts
|
|
113
|
+
* @param relativeSetupImport - Relative import path from api.ts to the setup file
|
|
114
|
+
* (only used in the ky scaffold comment to show the correct usage example)
|
|
115
|
+
*/
|
|
116
|
+
function generateClientSetup(
|
|
117
|
+
options,
|
|
118
|
+
relativeApiImport,
|
|
119
|
+
relativeSetupImport
|
|
120
|
+
) {
|
|
121
|
+
const setupData = {
|
|
122
|
+
baseUrl: options.baseUrl,
|
|
123
|
+
relativeApiImport,
|
|
124
|
+
relativeSetupImport,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
return _templateEngine.renderFile.call(void 0, 'baseClientSetup.ejs', setupData);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
// Template not available for this L1 (e.g. ng1/ng2 don't have a setup template)
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
} exports.generateClientSetup = generateClientSetup;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generates the reactive hooks file content (for use with --hooksOut).
|
|
137
|
+
*
|
|
138
|
+
* The hooks file contains:
|
|
139
|
+
* - An optional `'use client';` directive (when useClient is set)
|
|
140
|
+
* - The L2 reactive library imports (useSWR / useQuery etc.)
|
|
141
|
+
* - A namespace import of the main file: `import * as API from '<relativeMainImport>'`
|
|
142
|
+
* - One `export const <name> = { queries, mutations, queryKeys }` per tag group,
|
|
143
|
+
* referencing HTTP client methods via `API.<name>Client.*`
|
|
144
|
+
*
|
|
145
|
+
* This function requires the template engine to already be initialized with the
|
|
146
|
+
* L2 template files (i.e. `loadAllTemplateFiles` must have been called first).
|
|
147
|
+
*/
|
|
148
|
+
async function generateHooks(
|
|
149
|
+
spec,
|
|
150
|
+
options,
|
|
151
|
+
relativeMainImport
|
|
152
|
+
) {
|
|
153
|
+
const operations = _swagger.getOperations.call(void 0, spec);
|
|
154
|
+
const groups = _utils.groupOperationsByGroupName.call(void 0, operations);
|
|
155
|
+
const servicePrefix = options.servicePrefix;
|
|
156
|
+
const baseClientData = {
|
|
157
|
+
servicePrefix,
|
|
158
|
+
baseUrl: options.baseUrl,
|
|
159
|
+
...options.queryParamsSerialization,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// L2 base client contains the reactive library imports (useSWR, useQuery, etc.)
|
|
163
|
+
let header = '';
|
|
164
|
+
if (_templateEngine.hasTemplateFile.call(void 0, 'baseClientL2.ejs')) {
|
|
165
|
+
header += _templateEngine.renderFile.call(void 0, 'baseClientL2.ejs', baseClientData);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const clientDirective = options.useClient ? "'use client';\n" : '';
|
|
169
|
+
let result = clientDirective + _header.FILE_HEADER + header;
|
|
170
|
+
|
|
171
|
+
// Import the L1 $httpConfig type directly from its source package so it is
|
|
172
|
+
// available in the hooks file without having to prefix it with API.
|
|
173
|
+
const l1HttpTypeImport = getL1HttpTypeImport(options.template);
|
|
174
|
+
if (l1HttpTypeImport) {
|
|
175
|
+
result += l1HttpTypeImport + '\n';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Import the main file as a namespace so we can reference API.*Client, API.encodeParams,
|
|
179
|
+
// and API.DomainTypes (used in hook generics via prefixApiType in the templates).
|
|
180
|
+
result += `import * as API from '${relativeMainImport}';\n\n`;
|
|
181
|
+
|
|
182
|
+
for (const name in groups) {
|
|
183
|
+
const group = groups[name];
|
|
184
|
+
const clientData = prepareClient(servicePrefix + name, group, spec.components, options);
|
|
185
|
+
|
|
186
|
+
if (!clientData) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const renderedFile = _templateEngine.renderFile.call(void 0, 'hooksClient.ejs', {
|
|
191
|
+
...clientData,
|
|
192
|
+
servicePrefix,
|
|
193
|
+
httpConfigType: _templateValidator.getHttpConfigType.call(void 0, options.template),
|
|
194
|
+
responseMapper: _templateValidator.getResponseMapper.call(void 0, options.template),
|
|
195
|
+
// Template helper functions — defined once here, used in all L2 templates.
|
|
196
|
+
toOpName,
|
|
197
|
+
safeOperation,
|
|
198
|
+
prefixApiType,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
result += renderedFile;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result;
|
|
205
|
+
} exports.generateHooks = generateHooks;
|
|
206
|
+
|
|
76
207
|
function prepareClient(
|
|
77
208
|
name,
|
|
78
209
|
operations,
|
|
@@ -429,6 +560,96 @@ function getRequestBody(
|
|
|
429
560
|
return null;
|
|
430
561
|
}
|
|
431
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Returns a TypeScript import statement for the L1 HTTP config type so that it
|
|
565
|
+
* is available in the hooks file by its bare name (e.g. `AxiosRequestConfig`)
|
|
566
|
+
* without needing an `API.` prefix.
|
|
567
|
+
*
|
|
568
|
+
* Returns `null` for `fetch` (uses the built-in `RequestInit` — no import needed)
|
|
569
|
+
* and for custom/unknown L1 templates.
|
|
570
|
+
*/
|
|
571
|
+
// ─── Template helper functions ─────────────────────────────────────────────
|
|
572
|
+
// Defined here in TypeScript and passed into template data so the EJS files
|
|
573
|
+
// have no local function definitions and no duplication across templates.
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Converts an operation name to the PascalCase suffix used in hook/query key
|
|
577
|
+
* names: strips a leading "get" prefix (case-insensitive), then capitalises.
|
|
578
|
+
*
|
|
579
|
+
* @example toOpName('getPetById') → 'PetById'
|
|
580
|
+
* @example toOpName('findPetsByStatus') → 'FindPetsByStatus'
|
|
581
|
+
*/
|
|
582
|
+
function toOpName(name) {
|
|
583
|
+
const n = name.toLowerCase().startsWith('get') ? name.slice(3) : name;
|
|
584
|
+
return n.charAt(0).toUpperCase() + n.slice(1);
|
|
585
|
+
} exports.toOpName = toOpName;
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Returns a copy of `operation` where any parameter whose name matches
|
|
589
|
+
* `clientName` is renamed to `_<name>` to avoid TypeScript variable shadowing
|
|
590
|
+
* inside the hook object literal.
|
|
591
|
+
*/
|
|
592
|
+
function safeOperation(
|
|
593
|
+
operation,
|
|
594
|
+
clientName
|
|
595
|
+
) {
|
|
596
|
+
const safeParams = operation.parameters.map((p) =>
|
|
597
|
+
p.name === clientName ? { ...p, name: `_${p.name}` } : p
|
|
598
|
+
);
|
|
599
|
+
return { ...operation, parameters: safeParams };
|
|
600
|
+
} exports.safeOperation = safeOperation;
|
|
601
|
+
|
|
602
|
+
// `API` is included here because it is the namespace we emit ourselves — it
|
|
603
|
+
// must never be prefixed with a second `API.` in the output.
|
|
604
|
+
const PRIMITIVES =
|
|
605
|
+
/^(API|unknown|string|number|boolean|void|null|undefined|any|never|Date|object|Record|Array|Pick|Omit|Required|Partial|Readonly|NonNullable|ReturnType|InstanceType|Parameters|ConstructorParameters)$/;
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Prefixes named (non-primitive) TypeScript type names in a type string with
|
|
609
|
+
* the `API.` namespace so they resolve to types exported from the main file
|
|
610
|
+
* when the hooks are in a separate file (split-file / --hooksOut mode).
|
|
611
|
+
*
|
|
612
|
+
* Works on any type string structure — bare names, arrays, unions, intersections,
|
|
613
|
+
* and inline object types (including PascalCase names used as property value types
|
|
614
|
+
* inside `{ key: SomeType }` shapes).
|
|
615
|
+
*
|
|
616
|
+
* @example prefixApiType('Pet') → 'API.Pet'
|
|
617
|
+
* @example prefixApiType('Pet[]') → 'API.Pet[]'
|
|
618
|
+
* @example prefixApiType('Pet | null') → 'API.Pet | null'
|
|
619
|
+
* @example prefixApiType('unknown') → 'unknown'
|
|
620
|
+
* @example prefixApiType('{ id: number }') → '{ id: number }'
|
|
621
|
+
* @example prefixApiType('{ profile: MyEnum; }') → '{ profile: API.MyEnum; }'
|
|
622
|
+
* @example prefixApiType('API.Pet') → 'API.Pet' (API itself is in the exclude list)
|
|
623
|
+
*/
|
|
624
|
+
function prefixApiType(typeStr) {
|
|
625
|
+
if (!typeStr) {
|
|
626
|
+
return typeStr;
|
|
627
|
+
}
|
|
628
|
+
// The negative lookbehind `(?<!\.)` skips identifiers that are already part
|
|
629
|
+
// of a namespace reference (e.g. `API.Pet` — when the regex reaches `Pet` it
|
|
630
|
+
// is preceded by `.`, so we leave it alone).
|
|
631
|
+
return typeStr.replace(/(?<!\.)\b([A-Z][A-Za-z0-9_]*)\b/g, (match) =>
|
|
632
|
+
PRIMITIVES.test(match) ? match : `API.${match}`
|
|
633
|
+
);
|
|
634
|
+
} exports.prefixApiType = prefixApiType;
|
|
635
|
+
|
|
636
|
+
function getL1HttpTypeImport(template) {
|
|
637
|
+
const l1 = _templateValidator.getL1Template.call(void 0, template);
|
|
638
|
+
switch (l1) {
|
|
639
|
+
case 'axios':
|
|
640
|
+
return "import type { AxiosRequestConfig } from 'axios';";
|
|
641
|
+
case 'xior':
|
|
642
|
+
return "import type { XiorRequestConfig } from 'xior';";
|
|
643
|
+
case 'fetch':
|
|
644
|
+
// RequestInit is a global built-in — no import needed
|
|
645
|
+
return null;
|
|
646
|
+
case 'ky':
|
|
647
|
+
return "import type { Options as KyOptions } from 'ky';";
|
|
648
|
+
default:
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
432
653
|
function upsertFixedHeader(headers, headerName, value) {
|
|
433
654
|
const headerIndex = headers.findIndex(
|
|
434
655
|
(header) => header.originalName.toLowerCase() === headerName.toLowerCase()
|
package/dist/gen/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _createNamedExportFrom(obj, localName, importedName) { Object.defineProperty(exports, localName, {enumerable: true, configurable: true, get: () => obj[importedName]}); }
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _createNamedExportFrom(obj, localName, importedName) { Object.defineProperty(exports, localName, {enumerable: true, configurable: true, get: () => obj[importedName]}); } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
|
|
2
2
|
|
|
3
3
|
var _genOperations = require('./genOperations'); var _genOperations2 = _interopRequireDefault(_genOperations);
|
|
4
4
|
var _genMocks = require('./genMocks'); var _genMocks2 = _interopRequireDefault(_genMocks);
|
|
@@ -15,10 +15,28 @@ var _utils = require('../utils');
|
|
|
15
15
|
) {
|
|
16
16
|
let fileContents = '';
|
|
17
17
|
|
|
18
|
+
// Pre-compute setup file paths so they can be embedded in the generated api.ts
|
|
19
|
+
// (ky with --clientSetup imports from the setup file at build time).
|
|
20
|
+
let resolvedSetupPath = null;
|
|
21
|
+
let relativeSetupImportForMain = null;
|
|
22
|
+
|
|
23
|
+
if (options.clientSetup && options.out) {
|
|
24
|
+
resolvedSetupPath = _utils.prepareOutputFilename.call(void 0, options.clientSetup);
|
|
25
|
+
const resolvedOutPath = _utils.prepareOutputFilename.call(void 0, options.out);
|
|
26
|
+
if (resolvedSetupPath && resolvedOutPath) {
|
|
27
|
+
// From api.ts → setup file (used inside baseClientWithSetup.ejs import)
|
|
28
|
+
relativeSetupImportForMain = _utils.deriveRelativeImport.call(void 0, resolvedOutPath, resolvedSetupPath);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
if (options.generationMode === 'schemas') {
|
|
19
33
|
fileContents = _header.FILE_HEADER + _genTypes2.default.call(void 0, spec, options, false);
|
|
20
34
|
} else {
|
|
21
|
-
fileContents = await _genOperations2.default.call(void 0,
|
|
35
|
+
fileContents = await _genOperations2.default.call(void 0,
|
|
36
|
+
spec,
|
|
37
|
+
options,
|
|
38
|
+
_nullishCoalesce(relativeSetupImportForMain, () => ( undefined))
|
|
39
|
+
);
|
|
22
40
|
fileContents += _genTypes2.default.call(void 0, spec, options);
|
|
23
41
|
}
|
|
24
42
|
|
|
@@ -29,12 +47,52 @@ var _utils = require('../utils');
|
|
|
29
47
|
}
|
|
30
48
|
}
|
|
31
49
|
|
|
50
|
+
// Generate the write-once setup scaffold when --clientSetup is set
|
|
51
|
+
if (options.clientSetup && options.out && resolvedSetupPath) {
|
|
52
|
+
const resolvedOutPath = _utils.prepareOutputFilename.call(void 0, options.out);
|
|
53
|
+
if (resolvedOutPath) {
|
|
54
|
+
// From setup file → api.ts (used inside the scaffold's import of `http`)
|
|
55
|
+
const relativeApiImport = _utils.deriveRelativeImport.call(void 0, resolvedSetupPath, resolvedOutPath);
|
|
56
|
+
const setupContents = _genOperations.generateClientSetup.call(void 0,
|
|
57
|
+
options,
|
|
58
|
+
relativeApiImport,
|
|
59
|
+
_nullishCoalesce(relativeSetupImportForMain, () => ( './api'))
|
|
60
|
+
);
|
|
61
|
+
if (setupContents) {
|
|
62
|
+
const result = await _utils.saveFileIfMissing.call(void 0,
|
|
63
|
+
resolvedSetupPath,
|
|
64
|
+
setupContents,
|
|
65
|
+
_nullishCoalesce(options.forceSetup, () => ( false))
|
|
66
|
+
);
|
|
67
|
+
// result === 'skipped' when the file already exists and --forceSetup was not set
|
|
68
|
+
void result;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Generate the hooks file when --hooksOut is set (L2 templates only)
|
|
74
|
+
if (options.hooksOut && options.out) {
|
|
75
|
+
const resolvedHooksPath = _utils.prepareOutputFilename.call(void 0, options.hooksOut);
|
|
76
|
+
const resolvedOutPath = _utils.prepareOutputFilename.call(void 0, options.out);
|
|
77
|
+
if (resolvedHooksPath && resolvedOutPath) {
|
|
78
|
+
const relativeMainImport = _utils.deriveRelativeImport.call(void 0, resolvedHooksPath, resolvedOutPath);
|
|
79
|
+
const hooksContents = await _genOperations.generateHooks.call(void 0, spec, options, relativeMainImport);
|
|
80
|
+
await _utils.saveFile.call(void 0, resolvedHooksPath, hooksContents);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
32
84
|
if (options.mocks && options.testingFramework && options.out) {
|
|
33
85
|
const resolvedMocksPath = _utils.prepareOutputFilename.call(void 0, options.mocks);
|
|
34
86
|
const resolvedOutPath = _utils.prepareOutputFilename.call(void 0, options.out);
|
|
87
|
+
const resolvedHooksPath = options.hooksOut
|
|
88
|
+
? _utils.prepareOutputFilename.call(void 0, options.hooksOut)
|
|
89
|
+
: null;
|
|
35
90
|
if (resolvedMocksPath && resolvedOutPath) {
|
|
36
91
|
const relativeApiImport = _utils.deriveRelativeImport.call(void 0, resolvedMocksPath, resolvedOutPath);
|
|
37
|
-
const
|
|
92
|
+
const relativeHooksImport = resolvedHooksPath
|
|
93
|
+
? _utils.deriveRelativeImport.call(void 0, resolvedMocksPath, resolvedHooksPath)
|
|
94
|
+
: undefined;
|
|
95
|
+
const mockContents = _genMocks2.default.call(void 0, spec, options, relativeApiImport, relativeHooksImport);
|
|
38
96
|
await _utils.saveFile.call(void 0, resolvedMocksPath, mockContents);
|
|
39
97
|
}
|
|
40
98
|
}
|