swaggie 2.0.1 → 2.1.0-beta.1
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 +54 -4
- package/dist/cli.js +19 -1
- package/dist/gen/genMocks.js +25 -13
- 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
|
@@ -21,7 +21,7 @@ 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`, `Angular 1`, `Angular 2+`; with optional reactive layers (`swr`, `tsq`) that compose with any compatible HTTP client
|
|
24
|
+
- Supports multiple HTTP client libraries out of the box: `fetch`, `axios`, `xior`, `ky`, `Angular 1`, `Angular 2+`; with optional reactive layers (`swr`, `tsq`) that compose with any compatible HTTP client
|
|
25
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()`
|
|
26
26
|
- Custom templates — bring your own to fit your existing codebase
|
|
27
27
|
- Supports `allOf`, `oneOf`, `anyOf`, `$ref`, nullable types, and various enum definitions
|
|
@@ -79,7 +79,7 @@ swaggie -s https://petstore3.swagger.io/api/v3/openapi.json -o ./client/petstore
|
|
|
79
79
|
-s, --src <url|path> URL or file path to the OpenAPI spec
|
|
80
80
|
-o, --out <filePath> Output file path (omit to print to stdout)
|
|
81
81
|
-b, --baseUrl <string> Base URL that will be used as a default value in the clients
|
|
82
|
-
-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
|
+
-t, --template <string> Template to use. Single name: "axios", "fetch", "xior", "ky", "ng1", "ng2", "swr", "tsq". Reactive pair: "swr,axios" / "tsq,xior" / etc. (default: "axios")
|
|
83
83
|
-m, --mode <mode> Generation mode: "full" or "schemas" (default: "full")
|
|
84
84
|
-d, --schemaStyle <style> Schema object style: "interface" or "type" (default: "interface")
|
|
85
85
|
--enumStyle <style> Enum style for plain string enums: "union" or "enum" (default: "union")
|
|
@@ -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
|
}
|
|
@@ -383,7 +393,8 @@ function buildCreateClientMocks(
|
|
|
383
393
|
|
|
384
394
|
function buildCreateSwrHookMocks(
|
|
385
395
|
groups,
|
|
386
|
-
fw
|
|
396
|
+
fw,
|
|
397
|
+
hooksAlias
|
|
387
398
|
) {
|
|
388
399
|
const spy = spyFn(fw);
|
|
389
400
|
const lines = ['export function createApiHookMocks() {', ' return {'];
|
|
@@ -397,7 +408,7 @@ function buildCreateSwrHookMocks(
|
|
|
397
408
|
for (const op of getOps) {
|
|
398
409
|
const hookName = toHookName(op.name, 'use');
|
|
399
410
|
lines.push(
|
|
400
|
-
` ${hookName}: withMockSWR(${spy}(
|
|
411
|
+
` ${hookName}: withMockSWR(${spy}(${hooksAlias}.${camelName}.queries, '${hookName}').mockReturnValue(defaultSWRReturn)),`
|
|
401
412
|
);
|
|
402
413
|
}
|
|
403
414
|
lines.push(' },');
|
|
@@ -405,7 +416,7 @@ function buildCreateSwrHookMocks(
|
|
|
405
416
|
for (const op of mutOps) {
|
|
406
417
|
const hookName = 'use' + _case.pascal.call(void 0, op.name);
|
|
407
418
|
lines.push(
|
|
408
|
-
` ${hookName}: withMockSWRMutation(${spy}(
|
|
419
|
+
` ${hookName}: withMockSWRMutation(${spy}(${hooksAlias}.${camelName}.mutations, '${hookName}').mockReturnValue(defaultSWRMutationReturn as any)),`
|
|
409
420
|
);
|
|
410
421
|
}
|
|
411
422
|
lines.push(' },');
|
|
@@ -419,7 +430,8 @@ function buildCreateSwrHookMocks(
|
|
|
419
430
|
|
|
420
431
|
function buildCreateTsqHookMocks(
|
|
421
432
|
groups,
|
|
422
|
-
fw
|
|
433
|
+
fw,
|
|
434
|
+
hooksAlias
|
|
423
435
|
) {
|
|
424
436
|
const spy = spyFn(fw);
|
|
425
437
|
const lines = ['export function createApiHookMocks() {', ' return {'];
|
|
@@ -433,7 +445,7 @@ function buildCreateTsqHookMocks(
|
|
|
433
445
|
for (const op of getOps) {
|
|
434
446
|
const hookName = toHookName(op.name, 'use');
|
|
435
447
|
lines.push(
|
|
436
|
-
` ${hookName}: withMockQuery(${spy}(
|
|
448
|
+
` ${hookName}: withMockQuery(${spy}(${hooksAlias}.${camelName}.queries, '${hookName}').mockReturnValue(defaultQueryReturn as any)),`
|
|
437
449
|
);
|
|
438
450
|
}
|
|
439
451
|
lines.push(' },');
|
|
@@ -441,7 +453,7 @@ function buildCreateTsqHookMocks(
|
|
|
441
453
|
for (const op of mutOps) {
|
|
442
454
|
const hookName = 'use' + _case.pascal.call(void 0, op.name);
|
|
443
455
|
lines.push(
|
|
444
|
-
` ${hookName}: withMockMutation(${spy}(
|
|
456
|
+
` ${hookName}: withMockMutation(${spy}(${hooksAlias}.${camelName}.mutations, '${hookName}').mockReturnValue(defaultMutationReturn as any)),`
|
|
445
457
|
);
|
|
446
458
|
}
|
|
447
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
|
}
|