genoc 0.1.3 → 0.2.0

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
@@ -1,7 +1,7 @@
1
1
  # genoc
2
2
 
3
3
  Generate TypeScript HTTP clients from OpenAPI 3.0 / 3.1 specifications.
4
- The generated code has zero runtime dependencies. Full type safety. Bring your own HTTP client.
4
+ Generated code depends only on the tiny `genoc/runtime` module. Full type safety. Bring your own HTTP client.
5
5
 
6
6
  [![npm version](https://img.shields.io/npm/v/genoc)](https://www.npmjs.com/package/genoc)
7
7
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-green)](https://nodejs.org/)
@@ -13,6 +13,7 @@ The generated code has zero runtime dependencies. Full type safety. Bring your o
13
13
  - Full OpenAPI 3.0 and 3.1 specification support with automatic version detection
14
14
  - End-to-end type safety — requests, responses, and errors are fully typed
15
15
  - HTTP-client agnostic — adapter pattern lets you plug in fetch, axios, or anything else
16
+ - Shared runtime contract — `genoc/runtime` exports the `Requester` type and response/error classes, so one requester implementation works with every generated client
16
17
  - Error types with per-status-code narrowing and type guards
17
18
  - File and binary upload/download with stream handling
18
19
  - Flexible method naming strategies (path-based, operationId, operationId-with-fallback)
@@ -23,9 +24,12 @@ The generated code has zero runtime dependencies. Full type safety. Bring your o
23
24
  Install:
24
25
 
25
26
  ```bash
26
- npm install -D genoc
27
+ npm install genoc
27
28
  ```
28
29
 
30
+ Generated code imports from `genoc/runtime`, so `genoc` is a runtime
31
+ dependency (not just `devDependencies`).
32
+
29
33
  Generate:
30
34
 
31
35
  ```bash
@@ -40,8 +44,13 @@ This creates two files in `./src/api`:
40
44
  ## Usage
41
45
 
42
46
  The generated client requires a `Requester` implementation — a function that
43
- performs the actual HTTP call and returns the result. This is the type your
44
- implementation must satisfy:
47
+ performs the actual HTTP call and returns the result. The type lives in
48
+ `genoc/runtime`, so you can write and compile a requester before generating
49
+ anything:
50
+
51
+ ```typescript
52
+ import type { Requester } from 'genoc/runtime';
53
+ ```
45
54
 
46
55
  ```typescript
47
56
  type Requester = <TResponse>(
@@ -60,7 +69,8 @@ type Requester = <TResponse>(
60
69
 
61
70
  ```typescript
62
71
  import { createClient } from './client.js';
63
- import { ApiError, RequesterFailError, ErrorResponse } from './contracts.js';
72
+ import type { Requester } from 'genoc/runtime';
73
+ import { RequesterFailError, ErrorResponse } from 'genoc/runtime';
64
74
 
65
75
  const baseUrl = 'https://api.example.com';
66
76
 
@@ -85,7 +95,7 @@ const requester: Requester = async (method, path, options) => {
85
95
  return new ErrorResponse(
86
96
  response.status,
87
97
  await response.json(),
88
- response.headers,
98
+ Object.fromEntries(response.headers.entries()),
89
99
  response.statusText
90
100
  );
91
101
  }
@@ -101,6 +111,27 @@ const pets = await client.getPets({ limit: 10 });
101
111
 
102
112
  See [Binary / File Responses](#binary--file-responses) for handling `expectStream: true`.
103
113
 
114
+ ## Shared Runtime (`genoc/runtime`)
115
+
116
+ Generated clients import their response and error classes from `genoc/runtime`
117
+ instead of declaring inline copies (the generated `contracts.ts` re-exports
118
+ them, so existing imports keep working). This gives every generated client the
119
+ same class identity — `instanceof` checks work across clients, and you can
120
+ implement **one shared requester** typed against the package:
121
+
122
+ ```typescript
123
+ // common-requester.ts — reusable across all generated clients,
124
+ // no generated imports needed
125
+ import type { Requester } from 'genoc/runtime';
126
+
127
+ export const requester: Requester = async (method, path, options) => {
128
+ // ...your fetch/axios/etc. implementation
129
+ };
130
+ ```
131
+
132
+ To pin a specific version or point at a mirror, override the import specifier
133
+ via `--runtime-import-path` (CLI) or `runtimeImportPath` (programmatic config).
134
+
104
135
  ## Binary / File Responses
105
136
 
106
137
  When your spec defines binary responses (e.g. `format: binary`,
@@ -109,14 +140,14 @@ When your spec defines binary responses (e.g. `format: binary`,
109
140
  `StreamResponse` in that case:
110
141
 
111
142
  ```typescript
112
- import { StreamResponse } from './contracts.js';
143
+ import { StreamResponse } from 'genoc/runtime';
113
144
 
114
145
  // Inside your Requester implementation:
115
146
  if (options.expectStream === true) {
116
147
  return new StreamResponse(
117
148
  response.body as ReadableStream<Uint8Array>,
118
149
  getFilename(response.headers), // extract from Content-Disposition
119
- response.headers
150
+ Object.fromEntries(response.headers.entries())
120
151
  );
121
152
  }
122
153
  ```
@@ -131,17 +162,6 @@ class StreamResponse {
131
162
  }
132
163
  ```
133
164
 
134
- ### Response Helpers
135
-
136
- The generated `contracts.ts` includes helper functions for constructing
137
- responses in your `Requester` implementation:
138
-
139
- - `streamResponse(data, filename?, headers?)` — Creates a `StreamResponse` instance
140
- - `errorResponse(status, data, headers?, message?)` — Creates an `ErrorResponse` instance
141
-
142
- These are convenience wrappers around the `StreamResponse` and `ErrorResponse`
143
- constructors.
144
-
145
165
  ## CLI Reference
146
166
 
147
167
  ```bash
@@ -150,12 +170,13 @@ genoc <spec> [flags]
150
170
 
151
171
  `<spec>` — Path or URL to an OpenAPI 3.0 / 3.1 spec (JSON or YAML).
152
172
 
153
- | Flag | Default | Description |
154
- | ------------------------ | ------------ | ---------------------------------------------------- |
155
- | `--output-dir` | (required) | Output directory for generated files |
156
- | `--method-name-strategy` | `path-based` | Method naming strategy |
157
- | `--spec-version` | auto-detect | Override version detection (`"3.0"` or `"3.1"`) |
158
- | `--strict-version` | `true` | Warn if `--spec-version` mismatches detected version |
173
+ | Flag | Default | Description |
174
+ | ------------------------ | --------------- | ------------------------------------------------------------ |
175
+ | `--output-dir` | (required) | Output directory for generated files |
176
+ | `--method-name-strategy` | `path-based` | Method naming strategy |
177
+ | `--spec-version` | auto-detect | Override version detection (`"3.0"` or `"3.1"`) |
178
+ | `--strict-version` | `true` | Warn if `--spec-version` mismatches detected version |
179
+ | `--runtime-import-path` | `genoc/runtime` | Module specifier generated code imports runtime classes from |
159
180
 
160
181
  ## Method Naming Strategies
161
182
 
@@ -168,6 +189,15 @@ genoc <spec> [flags]
168
189
  - **`operationId-with-fallback`** — Use `operationId` if present, otherwise
169
190
  fall back to path-based naming.
170
191
 
192
+ ## JSDoc Documentation Output
193
+
194
+ `description`, `deprecated`, `default`, `example`/`examples`, and `title`
195
+ fields from your spec become JSDoc comments in the generated code — on object
196
+ type properties, named schemas, and client methods. Object types are always
197
+ emitted multi-line, so per-property JSDoc stays aligned at each nesting depth.
198
+ `@deprecated` tags give you editor strikethrough on deprecated fields and
199
+ methods for free.
200
+
171
201
  ## Programmatic API
172
202
 
173
203
  ```typescript
@@ -194,7 +224,7 @@ union, and `isDefinedError` narrows a caught error to that union:
194
224
 
195
225
  ```typescript
196
226
  import { UnspecifiedApiError, RequesterFailError } from './contracts.js';
197
- import { isDefinedError } from './client.js';
227
+ import { isDefinedError } from 'genoc/runtime';
198
228
 
199
229
  try {
200
230
  const result = await client.getPets();
@@ -9,6 +9,7 @@ export interface AnalyzedParameter {
9
9
  tsType: string;
10
10
  description?: string;
11
11
  deprecated?: boolean;
12
+ example?: unknown;
12
13
  }
13
14
  export interface AnalyzedRequestBody {
14
15
  required: boolean;
@@ -16,6 +17,7 @@ export interface AnalyzedRequestBody {
16
17
  schema: SchemaObject | ReferenceObject | undefined;
17
18
  tsType: string;
18
19
  isMultipart: boolean;
20
+ description?: string;
19
21
  }
20
22
  export interface AnalyzedResponse {
21
23
  statusCode: string;
@@ -78,6 +78,7 @@ function analyzeParameter(param, resolver) {
78
78
  tsType: schemaToTsType(param.schema, resolver),
79
79
  description: param.description,
80
80
  deprecated: param.deprecated,
81
+ example: param.example,
81
82
  };
82
83
  }
83
84
  function mergeParameters(pathItemParams, operationParams, resolver) {
@@ -120,6 +121,7 @@ function analyzeRequestBody(body, resolver) {
120
121
  schema,
121
122
  tsType,
122
123
  isMultipart,
124
+ description: resolved.description,
123
125
  };
124
126
  }
125
127
  function analyzeResponses(responses, resolver) {
@@ -44,5 +44,13 @@ export declare class SchemaMapper {
44
44
  private mapDiscriminatedUnion;
45
45
  private resolveDiscriminantValue;
46
46
  private mapArray;
47
+ /**
48
+ * Render the per-property JSDoc comment lines for an object property.
49
+ *
50
+ * Metadata comes from the property's own schema node; for a `$ref` property
51
+ * the resolved target's own node metadata is used (one level only — never
52
+ * recursed), so self-referential schemas cannot loop here.
53
+ */
54
+ private renderPropertyJsDoc;
47
55
  private mapObject;
48
56
  }
@@ -1,7 +1,12 @@
1
1
  import { RefResolver } from '../parser/ref-resolver.js';
2
2
  import { formatToBrandTypeName } from '../utils/case.js';
3
- import { sanitizeTypeName } from '../utils/generator-helpers.js';
3
+ import { buildTypeJsDoc, sanitizeTypeName } from '../utils/generator-helpers.js';
4
4
  import { quoteKey } from '../utils/string.js';
5
+ /** Indentation contract for multi-line types — 2-space unit pinned from the generated ServerParams interface; binding for downstream generators. */
6
+ const INDENT_UNIT = ' ';
7
+ function indentBy(level) {
8
+ return INDENT_UNIT.repeat(level);
9
+ }
5
10
  /**
6
11
  * Default: extracts the last segment of the JSON pointer.
7
12
  * "#/components/schemas/User" -> "User"
@@ -58,7 +63,7 @@ export class SchemaMapper {
58
63
  return { tsType: schema ? 'unknown' : 'never', imports: [] };
59
64
  }
60
65
  const visited = new Set();
61
- const result = this.mapInternal(schema, name, context, visited);
66
+ const result = this.mapInternal(schema, name, context, visited, 0);
62
67
  if (name && this.discriminatorTargets.has(name)) {
63
68
  const target = this.discriminatorTargets.get(name);
64
69
  result.tsType += ` & { ${quoteKey(target.propertyName)}: '${target.literalValue}' }`;
@@ -75,7 +80,7 @@ export class SchemaMapper {
75
80
  return null;
76
81
  return brandName;
77
82
  }
78
- mapInternal(schema, name, context, visited) {
83
+ mapInternal(schema, name, context, visited, indent) {
79
84
  if (isRefObject(schema)) {
80
85
  const refStr = schema.$ref;
81
86
  const refName = this.typeNameGenerator(refStr);
@@ -89,7 +94,7 @@ export class SchemaMapper {
89
94
  if (resolved) {
90
95
  const discInfo = this.resolveDiscriminatorInfo(resolved, refStr);
91
96
  if (discInfo) {
92
- const expanded = this.mapInternal(resolved, undefined, context, visited);
97
+ const expanded = this.mapInternal(resolved, undefined, context, visited, indent);
93
98
  expanded.tsType += ` & { ${quoteKey(discInfo.propertyName)}: '${discInfo.literalValue}' }`;
94
99
  return expanded;
95
100
  }
@@ -121,7 +126,7 @@ export class SchemaMapper {
121
126
  return { tsType, imports: [] };
122
127
  }
123
128
  if (s.allOf !== undefined && s.allOf.length > 0) {
124
- const result = this.mapCombinator(s.allOf, '&', context, visited);
129
+ const result = this.mapCombinator(s.allOf, '&', context, visited, indent);
125
130
  if (s.nullable === true) {
126
131
  return {
127
132
  tsType: needsParens(result.tsType)
@@ -134,8 +139,8 @@ export class SchemaMapper {
134
139
  }
135
140
  if (s.oneOf !== undefined && s.oneOf.length > 0) {
136
141
  const result = s.discriminator
137
- ? this.mapDiscriminatedUnion(s.oneOf, s.discriminator, context, visited)
138
- : this.mapCombinator(s.oneOf, '|', context, visited);
142
+ ? this.mapDiscriminatedUnion(s.oneOf, s.discriminator, context, visited, indent)
143
+ : this.mapCombinator(s.oneOf, '|', context, visited, indent);
139
144
  if (s.nullable === true) {
140
145
  return {
141
146
  tsType: needsParens(result.tsType)
@@ -148,8 +153,8 @@ export class SchemaMapper {
148
153
  }
149
154
  if (s.anyOf !== undefined && s.anyOf.length > 0) {
150
155
  const result = s.discriminator
151
- ? this.mapDiscriminatedUnion(s.anyOf, s.discriminator, context, visited)
152
- : this.mapCombinator(s.anyOf, '|', context, visited);
156
+ ? this.mapDiscriminatedUnion(s.anyOf, s.discriminator, context, visited, indent)
157
+ : this.mapCombinator(s.anyOf, '|', context, visited, indent);
153
158
  if (s.nullable === true) {
154
159
  return {
155
160
  tsType: needsParens(result.tsType)
@@ -169,7 +174,7 @@ export class SchemaMapper {
169
174
  if (nonNull.length === 0) {
170
175
  return { tsType: 'null', imports: [] };
171
176
  }
172
- const baseResult = this.mapInternal({ ...s, type: nonNull[0] }, name, context, new Set(visited));
177
+ const baseResult = this.mapInternal({ ...s, type: nonNull[0] }, name, context, new Set(visited), indent);
173
178
  if (hasNull) {
174
179
  return {
175
180
  tsType: `${baseResult.tsType} | null`,
@@ -224,9 +229,9 @@ export class SchemaMapper {
224
229
  case 'null':
225
230
  return { tsType: 'null', imports: [] };
226
231
  case 'array':
227
- return this.mapArray(s, context, visited);
232
+ return this.mapArray(s, context, visited, indent);
228
233
  case 'object':
229
- return this.mapObject(s, name, context, visited);
234
+ return this.mapObject(s, name, context, visited, indent);
230
235
  default:
231
236
  return { tsType: 'unknown', imports: [] };
232
237
  }
@@ -270,8 +275,8 @@ export class SchemaMapper {
270
275
  })
271
276
  .join(' | ');
272
277
  }
273
- mapCombinator(schemas, kind, context, visited) {
274
- const results = schemas.map((s) => this.mapInternal(s, undefined, context, visited));
278
+ mapCombinator(schemas, kind, context, visited, indent) {
279
+ const results = schemas.map((s) => this.mapInternal(s, undefined, context, visited, indent));
275
280
  const allImports = [];
276
281
  for (const r of results) {
277
282
  allImports.push(...r.imports);
@@ -285,14 +290,14 @@ export class SchemaMapper {
285
290
  });
286
291
  return { tsType: parts.join(separator), imports: allImports };
287
292
  }
288
- mapDiscriminatedUnion(schemas, discriminator, context, visited) {
293
+ mapDiscriminatedUnion(schemas, discriminator, context, visited, indent) {
289
294
  const propertyName = discriminator.propertyName;
290
295
  const mapping = discriminator.mapping;
291
296
  const allImports = [];
292
297
  const parts = [];
293
298
  for (const schema of schemas) {
294
299
  const discriminantValue = this.resolveDiscriminantValue(schema, propertyName, mapping);
295
- const variantResult = this.mapInternal(schema, undefined, context, visited);
300
+ const variantResult = this.mapInternal(schema, undefined, context, visited, indent);
296
301
  allImports.push(...variantResult.imports);
297
302
  const quotedProp = quoteKey(propertyName);
298
303
  parts.push(`({ ${quotedProp}: '${discriminantValue}' } & ${variantResult.tsType})`);
@@ -323,14 +328,14 @@ export class SchemaMapper {
323
328
  }
324
329
  return 'unknown';
325
330
  }
326
- mapArray(schema, context, visited) {
331
+ mapArray(schema, context, visited, indent) {
327
332
  if (!schema.items) {
328
333
  return {
329
334
  tsType: schema.nullable === true ? 'unknown[] | null' : 'unknown[]',
330
335
  imports: [],
331
336
  };
332
337
  }
333
- const itemResult = this.mapInternal(schema.items, undefined, context, visited);
338
+ const itemResult = this.mapInternal(schema.items, undefined, context, visited, indent);
334
339
  const tsType = isComplexType(itemResult.tsType)
335
340
  ? `Array<${itemResult.tsType}>`
336
341
  : `${itemResult.tsType}[]`;
@@ -339,7 +344,33 @@ export class SchemaMapper {
339
344
  imports: itemResult.imports,
340
345
  };
341
346
  }
342
- mapObject(schema, name, context, visited) {
347
+ /**
348
+ * Render the per-property JSDoc comment lines for an object property.
349
+ *
350
+ * Metadata comes from the property's own schema node; for a `$ref` property
351
+ * the resolved target's own node metadata is used (one level only — never
352
+ * recursed), so self-referential schemas cannot loop here.
353
+ */
354
+ renderPropertyJsDoc(propSchema, indentLevel) {
355
+ let metaSchema;
356
+ if (isRefObject(propSchema)) {
357
+ try {
358
+ metaSchema = this.resolver.resolveSchema(propSchema);
359
+ }
360
+ catch {
361
+ return [];
362
+ }
363
+ }
364
+ else {
365
+ metaSchema = propSchema;
366
+ }
367
+ const block = buildTypeJsDoc(metaSchema);
368
+ if (block === '')
369
+ return [];
370
+ const ind = indentBy(indentLevel);
371
+ return block.split('\n').map((line) => `${ind}${line}`);
372
+ }
373
+ mapObject(schema, name, context, visited, indent) {
343
374
  const properties = schema.properties ?? {};
344
375
  const requiredSet = new Set(schema.required ?? []);
345
376
  const propNames = Object.keys(properties);
@@ -354,7 +385,9 @@ export class SchemaMapper {
354
385
  return false;
355
386
  return true;
356
387
  });
357
- const propEntries = [];
388
+ const ownIndent = indentBy(indent);
389
+ const memberIndent = indentBy(indent + 1);
390
+ const memberLines = [];
358
391
  const allImports = [];
359
392
  if (schema.discriminator) {
360
393
  const discPropName = schema.discriminator.propertyName;
@@ -362,15 +395,16 @@ export class SchemaMapper {
362
395
  if (discIndex !== -1) {
363
396
  filteredPropNames.splice(discIndex, 1);
364
397
  }
365
- propEntries.push(`"${discPropName}": string`);
398
+ memberLines.push(`${memberIndent}"${discPropName}": string;`);
366
399
  }
367
400
  for (const propName of filteredPropNames) {
368
401
  const propSchema = properties[propName];
369
- const propResult = this.mapInternal(propSchema, undefined, context, visited);
402
+ const propResult = this.mapInternal(propSchema, undefined, context, visited, indent + 1);
370
403
  allImports.push(...propResult.imports);
404
+ memberLines.push(...this.renderPropertyJsDoc(propSchema, indent + 1));
371
405
  const optional = requiredSet.has(propName) ? '' : '?';
372
406
  const quotedName = quoteKey(propName);
373
- propEntries.push(`${quotedName}${optional}: ${propResult.tsType}`);
407
+ memberLines.push(`${memberIndent}${quotedName}${optional}: ${propResult.tsType};`);
374
408
  }
375
409
  const additionalProps = schema.additionalProperties;
376
410
  let indexSignature = null;
@@ -381,11 +415,11 @@ export class SchemaMapper {
381
415
  indexSignature = '[key: string]: unknown';
382
416
  }
383
417
  else if (additionalProps !== undefined && typeof additionalProps === 'object') {
384
- const addPropResult = this.mapInternal(additionalProps, undefined, context, visited);
418
+ const addPropResult = this.mapInternal(additionalProps, undefined, context, visited, indent + 1);
385
419
  allImports.push(...addPropResult.imports);
386
420
  indexSignature = `[key: string]: ${addPropResult.tsType}`;
387
421
  }
388
- const hasProps = propEntries.length > 0;
422
+ const hasProps = memberLines.length > 0;
389
423
  const hasIndex = indexSignature !== null;
390
424
  let result;
391
425
  if (!hasProps && !hasIndex) {
@@ -400,7 +434,7 @@ export class SchemaMapper {
400
434
  const valueType = indexSignature.replace('[key: string]: ', '');
401
435
  if (name) {
402
436
  result = {
403
- tsType: `{ ${indexSignature}; }`,
437
+ tsType: `{\n${memberIndent}${indexSignature};\n${ownIndent}}`,
404
438
  imports: allImports,
405
439
  };
406
440
  }
@@ -412,16 +446,17 @@ export class SchemaMapper {
412
446
  }
413
447
  }
414
448
  else {
415
- const propsBody = propEntries.join('; ') + ';';
449
+ const propsBlock = `{\n${memberLines.join('\n')}\n${ownIndent}}`;
416
450
  if (hasIndex) {
451
+ const indexBlock = `{\n${memberIndent}${indexSignature};\n${ownIndent}}`;
417
452
  result = {
418
- tsType: `{ ${propsBody} } & { ${indexSignature}; }`,
453
+ tsType: `${propsBlock} & ${indexBlock}`,
419
454
  imports: allImports,
420
455
  };
421
456
  }
422
457
  else {
423
458
  result = {
424
- tsType: `{ ${propsBody} }`,
459
+ tsType: propsBlock,
425
460
  imports: allImports,
426
461
  };
427
462
  }
package/dist/cli/app.d.ts CHANGED
@@ -3,6 +3,7 @@ interface Flags {
3
3
  methodNameStrategy: 'path-based' | 'operationId' | 'operationId-with-fallback';
4
4
  specVersion?: string;
5
5
  strictVersion: boolean;
6
+ runtimeImportPath?: string;
6
7
  }
7
8
  export type AppFlags = Flags;
8
9
  export declare const app: import("@stricli/core").Application<import("@stricli/core").CommandContext>;
package/dist/cli/app.js CHANGED
@@ -29,6 +29,13 @@ const command = buildCommand({
29
29
  default: true,
30
30
  brief: 'Enable strict version checking',
31
31
  },
32
+ runtimeImportPath: {
33
+ kind: 'parsed',
34
+ parse: String,
35
+ brief: 'Module specifier for runtime imports (default: genoc/runtime)',
36
+ optional: true,
37
+ placeholder: 'module',
38
+ },
32
39
  },
33
40
  positional: {
34
41
  kind: 'tuple',
package/dist/cli/impl.js CHANGED
@@ -27,6 +27,7 @@ export default async function (flags, spec) {
27
27
  methodNameStrategy: flags.methodNameStrategy || 'path-based',
28
28
  specVersion: flags.specVersion,
29
29
  strictVersion: flags.strictVersion,
30
+ runtimeImportPath: flags.runtimeImportPath,
30
31
  };
31
32
  const preserveRefSiblings = strategy.version() === '3.1';
32
33
  this.process.stdout.write('Generating client...\n');
@@ -2,7 +2,7 @@ import { mkdir, writeFile } from 'fs/promises';
2
2
  import { join } from 'path';
3
3
  import { analyzePaths } from '../analyzer/path-analyzer.js';
4
4
  import { RefResolver } from '../parser/ref-resolver.js';
5
- import { getOperationTypePrefix, getSuccessType, getErrorType, makeHeader, } from '../utils/generator-helpers.js';
5
+ import { DEFAULT_RUNTIME_IMPORT_PATH, getOperationTypePrefix, getSuccessType, getErrorType, makeHeader, } from '../utils/generator-helpers.js';
6
6
  import { generateContracts } from './contracts-generator.js';
7
7
  import { generateMethod } from './method-generator.js';
8
8
  function collectImportTypes(operations) {
@@ -156,7 +156,7 @@ function buildClientMethodBody(op) {
156
156
  }
157
157
  return tryCatchLines.join('\n');
158
158
  }
159
- function buildClientFile(operations, version) {
159
+ function buildClientFile(operations, version, runtimeImportPath = DEFAULT_RUNTIME_IMPORT_PATH) {
160
160
  const lines = [];
161
161
  lines.push(makeHeader(version));
162
162
  const importTypes = collectImportTypes(operations);
@@ -180,47 +180,11 @@ function buildClientFile(operations, version) {
180
180
  if (hasMultipart) {
181
181
  lines.push('/* global FormData */');
182
182
  }
183
+ lines.push(`import { decorateWithErrors } from '${runtimeImportPath}';`);
184
+ lines.push(`import type { Requester } from '${runtimeImportPath}';`);
183
185
  lines.push('');
184
- lines.push('function decorateWithErrors<T, E>(');
185
- lines.push(' item: T,');
186
- lines.push(' runtimeErrors: unknown,');
187
- lines.push('): T & { __definedErrors: E } {');
188
- lines.push(' Object.defineProperty(item, "__definedErrors", {');
189
- lines.push(' value: runtimeErrors,');
190
- lines.push(' enumerable: false,');
191
- lines.push(' configurable: true,');
192
- lines.push(' writable: false,');
193
- lines.push(' });');
194
- lines.push(' return item as T & { __definedErrors: E };');
195
- lines.push('}');
196
- lines.push('');
197
- lines.push('export function isDefinedError<E extends ApiError<number, unknown>>(');
198
- lines.push(' err: unknown,');
199
- lines.push(' fn: { __definedErrors: E },');
200
- lines.push('): err is E {');
201
- lines.push(' if (err instanceof UnspecifiedApiError) return false;');
202
- lines.push(' if (!(err instanceof ApiError)) return false;');
203
- lines.push(' return true;');
204
- lines.push('}');
205
- lines.push('');
206
- lines.push('/**');
207
- lines.push(' * Performs an HTTP request and returns the response.');
208
- lines.push(' *');
209
- lines.push(' * When `expectStream` is true, the implementation should return');
210
- lines.push(' * a `StreamResponse` containing the stream data, filename (from');
211
- lines.push(' * Content-Disposition header), and response headers.');
212
- lines.push(' */');
213
- lines.push('export type Requester = <TResponse>(');
214
- lines.push(' method: string,');
215
- lines.push(' path: string,');
216
- lines.push(' options: {');
217
- lines.push(' query?: Record<string, unknown>;');
218
- lines.push(' body?: unknown;');
219
- lines.push(' headers?: Record<string, string>;');
220
- lines.push(' expectStream?: true;');
221
- lines.push(' },');
222
- lines.push(') => Promise<TResponse | StreamResponse | ErrorResponse>;');
223
- lines.push('');
186
+ lines.push(`export { isDefinedError } from '${runtimeImportPath}';`);
187
+ lines.push(`export type { Requester } from '${runtimeImportPath}';`);
224
188
  lines.push('');
225
189
  lines.push('export function createClient(requester: Requester) {');
226
190
  lines.push(' return {');
@@ -269,9 +233,10 @@ export function generateClient(doc, config, options) {
269
233
  const resolver = new RefResolver(doc, undefined, {
270
234
  preserveRefSiblings: options?.preserveRefSiblings,
271
235
  });
272
- const contracts = generateContracts(doc, resolver);
236
+ const runtimeImportPath = config.runtimeImportPath ?? DEFAULT_RUNTIME_IMPORT_PATH;
237
+ const contracts = generateContracts(doc, resolver, runtimeImportPath);
273
238
  const operations = analyzePaths(doc, resolver, config.methodNameStrategy ?? 'path-based');
274
- const client = buildClientFile(operations, doc.openapi);
239
+ const client = buildClientFile(operations, doc.openapi, runtimeImportPath);
275
240
  return { contracts, client };
276
241
  }
277
242
  /**
@@ -10,7 +10,6 @@ import type { OpenAPIDocument } from '../types/openapi.js';
10
10
  * 4. Header parameter types per operation
11
11
  * 5. Request body types per operation
12
12
  * 6. Response / error types per operation
13
- * 7. ApiError class
14
- * 7b. UnspecifiedApiError class
13
+ * 7. Runtime re-export block (shared classes from the genoc runtime package)
15
14
  */
16
- export declare function generateContracts(doc: OpenAPIDocument, resolver: RefResolver): string;
15
+ export declare function generateContracts(doc: OpenAPIDocument, resolver: RefResolver, runtimeImportPath?: string): string;