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 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'; directive (Next.js App Router + SWR/TSQ)
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
 
@@ -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 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
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 ReturnType<typeof ${ref}.spyOn>>(spy: T) {
328
+ function withMockQuery<T extends MockInstance>(spy: T) {
311
329
  return Object.assign(spy, {
312
- mockQuery({ data, isLoading, error }: { data?: unknown; isLoading?: boolean; error?: 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 ReturnType<typeof ${ref}.spyOn>>(spy: T) {
355
+ function withMockMutation<T extends MockInstance>(spy: T) {
329
356
  return Object.assign(spy, {
330
- mockMutation({ data, isPending, error }: { data?: unknown; isPending?: boolean; error?: 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}(realApi.${camelName}.queries, '${hookName}').mockReturnValue(defaultSWRReturn)),`
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}(realApi.${camelName}.mutations, '${hookName}').mockReturnValue(defaultSWRMutationReturn as any)),`
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}(realApi.${camelName}.queries, '${hookName}').mockReturnValue(defaultQueryReturn)),`
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}(realApi.${camelName}.mutations, '${hookName}').mockReturnValue(defaultMutationReturn)),`
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
- baseClients += _templateEngine.renderFile.call(void 0, 'baseClient.ejs', baseClientData);
50
-
51
- const clientDirective = options.useClient ? "'use client';\n" : '';
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, spec, options);
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 mockContents = _genMocks2.default.call(void 0, spec, options, relativeApiImport);
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
  }