genoc 0.1.3 → 0.3.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.
Files changed (36) hide show
  1. package/README.md +79 -26
  2. package/dist/analyzer/naming.js +3 -2
  3. package/dist/analyzer/path-analyzer.d.ts +4 -0
  4. package/dist/analyzer/path-analyzer.js +31 -1
  5. package/dist/analyzer/schema-mapper.d.ts +8 -0
  6. package/dist/analyzer/schema-mapper.js +69 -30
  7. package/dist/cli/app.d.ts +2 -0
  8. package/dist/cli/app.js +14 -0
  9. package/dist/cli/impl.js +11 -1
  10. package/dist/generator/client-generator.js +9 -44
  11. package/dist/generator/contracts-generator.d.ts +2 -3
  12. package/dist/generator/contracts-generator.js +141 -140
  13. package/dist/generator/method-generator.js +11 -1
  14. package/dist/index.d.ts +4 -2
  15. package/dist/index.js +5 -2
  16. package/dist/parser/spec-reader.d.ts +3 -2
  17. package/dist/parser/spec-reader.js +5 -4
  18. package/dist/runtime/defined-errors.d.ts +17 -0
  19. package/dist/runtime/defined-errors.js +27 -0
  20. package/dist/runtime/errors.d.ts +28 -0
  21. package/dist/runtime/errors.js +45 -0
  22. package/dist/runtime/index.d.ts +11 -0
  23. package/dist/runtime/index.js +10 -0
  24. package/dist/runtime/requester.d.ts +14 -0
  25. package/dist/runtime/requester.js +1 -0
  26. package/dist/runtime/responses.d.ts +19 -0
  27. package/dist/runtime/responses.js +28 -0
  28. package/dist/types/client.d.ts +2 -0
  29. package/dist/types/openapi.d.ts +4 -0
  30. package/dist/utils/generator-helpers.d.ts +60 -5
  31. package/dist/utils/generator-helpers.js +223 -14
  32. package/dist/utils/proxy.d.ts +6 -0
  33. package/dist/utils/proxy.js +81 -0
  34. package/package.json +7 -2
  35. package/dist/generator/error-types.d.ts +0 -23
  36. package/dist/generator/error-types.js +0 -87
@@ -0,0 +1,27 @@
1
+ import { ApiError, UnspecifiedApiError } from './errors.js';
2
+ /**
3
+ * Attaches the `__definedErrors` metadata property to a client method.
4
+ * The property carries the method's error codes and is used by
5
+ * `isDefinedError` for type narrowing.
6
+ */
7
+ export function decorateWithErrors(item, runtimeErrors) {
8
+ Object.defineProperty(item, '__definedErrors', {
9
+ value: runtimeErrors,
10
+ enumerable: false,
11
+ configurable: true,
12
+ writable: false,
13
+ });
14
+ return item;
15
+ }
16
+ /**
17
+ * Type guard that narrows a caught error to a method's defined error union.
18
+ * Returns false for `UnspecifiedApiError` (status not declared in the spec)
19
+ * and for anything that is not an `ApiError`.
20
+ */
21
+ export function isDefinedError(err, _fn) {
22
+ if (err instanceof UnspecifiedApiError)
23
+ return false;
24
+ if (!(err instanceof ApiError))
25
+ return false;
26
+ return true;
27
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared error classes for generated clients.
3
+ *
4
+ * This module is the single source of truth for error class identity:
5
+ * both generated client code and user-provided `Requester` implementations
6
+ * import these classes, so `instanceof` checks work across module boundaries.
7
+ */
8
+ /** Error thrown for a specific status code declared in the spec. */
9
+ export declare class ApiError<TStatus extends number, TData> extends Error {
10
+ readonly status: TStatus;
11
+ readonly data: TData;
12
+ constructor(status: TStatus, data: TData, message: string);
13
+ }
14
+ /** Error thrown for a status code not declared in the spec. */
15
+ export declare class UnspecifiedApiError extends ApiError<number, unknown> {
16
+ constructor(status: number, data: unknown, message: string);
17
+ }
18
+ /** Error thrown for operations that declare a `default` response. */
19
+ export declare class DefaultApiError<TData> extends Error {
20
+ readonly status: number;
21
+ readonly data: TData;
22
+ constructor(status: number, data: TData, message: string);
23
+ }
24
+ /** Wraps unexpected failures inside a `Requester` implementation. */
25
+ export declare class RequesterFailError extends Error {
26
+ readonly cause: unknown;
27
+ constructor(cause: unknown);
28
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Shared error classes for generated clients.
3
+ *
4
+ * This module is the single source of truth for error class identity:
5
+ * both generated client code and user-provided `Requester` implementations
6
+ * import these classes, so `instanceof` checks work across module boundaries.
7
+ */
8
+ /** Error thrown for a specific status code declared in the spec. */
9
+ export class ApiError extends Error {
10
+ status;
11
+ data;
12
+ constructor(status, data, message) {
13
+ super(message);
14
+ this.status = status;
15
+ this.data = data;
16
+ this.name = 'ApiError';
17
+ }
18
+ }
19
+ /** Error thrown for a status code not declared in the spec. */
20
+ export class UnspecifiedApiError extends ApiError {
21
+ constructor(status, data, message) {
22
+ super(status, data, message);
23
+ this.name = 'UnspecifiedApiError';
24
+ }
25
+ }
26
+ /** Error thrown for operations that declare a `default` response. */
27
+ export class DefaultApiError extends Error {
28
+ status;
29
+ data;
30
+ constructor(status, data, message) {
31
+ super(message);
32
+ this.status = status;
33
+ this.data = data;
34
+ this.name = 'DefaultApiError';
35
+ }
36
+ }
37
+ /** Wraps unexpected failures inside a `Requester` implementation. */
38
+ export class RequesterFailError extends Error {
39
+ cause;
40
+ constructor(cause) {
41
+ super(`Request failed: ${cause instanceof Error ? cause.message : String(cause)}`);
42
+ this.cause = cause;
43
+ this.name = 'RequesterFailError';
44
+ }
45
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Public runtime contract shared by generated clients and user-provided
3
+ * `Requester` implementations.
4
+ *
5
+ * Import from `genoc/runtime` — the main `genoc` entry point is the
6
+ * generator API (dev-time only); this subpath is the tiny runtime surface.
7
+ */
8
+ export { ApiError, UnspecifiedApiError, DefaultApiError, RequesterFailError } from './errors.js';
9
+ export { StreamResponse, ErrorResponse } from './responses.js';
10
+ export { decorateWithErrors, isDefinedError } from './defined-errors.js';
11
+ export type { Requester } from './requester.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Public runtime contract shared by generated clients and user-provided
3
+ * `Requester` implementations.
4
+ *
5
+ * Import from `genoc/runtime` — the main `genoc` entry point is the
6
+ * generator API (dev-time only); this subpath is the tiny runtime surface.
7
+ */
8
+ export { ApiError, UnspecifiedApiError, DefaultApiError, RequesterFailError } from './errors.js';
9
+ export { StreamResponse, ErrorResponse } from './responses.js';
10
+ export { decorateWithErrors, isDefinedError } from './defined-errors.js';
@@ -0,0 +1,14 @@
1
+ import type { ErrorResponse, StreamResponse } from './responses.js';
2
+ /**
3
+ * Performs an HTTP request and returns the response.
4
+ *
5
+ * When `expectStream` is true, the implementation should return
6
+ * a `StreamResponse` containing the stream data, filename (from
7
+ * Content-Disposition header), and response headers.
8
+ */
9
+ export type Requester = <TResponse>(method: string, path: string, options: {
10
+ query?: Record<string, unknown>;
11
+ body?: unknown;
12
+ headers?: Record<string, string>;
13
+ expectStream?: true;
14
+ }) => Promise<TResponse | StreamResponse | ErrorResponse>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared response containers exchanged between generated clients and
3
+ * `Requester` implementations.
4
+ */
5
+ /** Container for binary/stream responses returned by a `Requester`. */
6
+ export declare class StreamResponse {
7
+ readonly data: ReadableStream<Uint8Array>;
8
+ readonly filename?: string;
9
+ readonly headers: Record<string, string>;
10
+ constructor(data: ReadableStream<Uint8Array>, filename?: string, headers?: Record<string, string>);
11
+ }
12
+ /** Container for non-2xx responses returned by a `Requester`. */
13
+ export declare class ErrorResponse {
14
+ readonly status: number;
15
+ readonly data: unknown;
16
+ readonly headers: Record<string, string>;
17
+ readonly message?: string;
18
+ constructor(status: number, data: unknown, headers?: Record<string, string>, message?: string);
19
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared response containers exchanged between generated clients and
3
+ * `Requester` implementations.
4
+ */
5
+ /** Container for binary/stream responses returned by a `Requester`. */
6
+ export class StreamResponse {
7
+ data;
8
+ filename;
9
+ headers;
10
+ constructor(data, filename, headers = {}) {
11
+ this.data = data;
12
+ this.filename = filename;
13
+ this.headers = headers;
14
+ }
15
+ }
16
+ /** Container for non-2xx responses returned by a `Requester`. */
17
+ export class ErrorResponse {
18
+ status;
19
+ data;
20
+ headers;
21
+ message;
22
+ constructor(status, data, headers = {}, message) {
23
+ this.status = status;
24
+ this.data = data;
25
+ this.headers = headers;
26
+ this.message = message;
27
+ }
28
+ }
@@ -6,6 +6,8 @@ export type GeneratorConfig = {
6
6
  requesterModuleName?: string;
7
7
  specVersion?: string;
8
8
  strictVersion?: boolean;
9
+ runtimeImportPath?: string;
10
+ proxy?: string;
9
11
  };
10
12
  export type RequesterFunction = <TResponse>(method: string, path: string, options: {
11
13
  query?: Record<string, unknown>;
@@ -16,6 +16,10 @@ export type SchemaObject = {
16
16
  const?: unknown;
17
17
  default?: unknown;
18
18
  description?: string;
19
+ title?: string;
20
+ deprecated?: boolean;
21
+ example?: unknown;
22
+ examples?: unknown[];
19
23
  nullable?: boolean;
20
24
  readOnly?: boolean;
21
25
  writeOnly?: boolean;
@@ -1,20 +1,40 @@
1
1
  import type { AnalyzedOperation } from '../analyzer/path-analyzer.js';
2
+ import type { SchemaObject } from '../types/openapi.js';
2
3
  /**
3
4
  * Reserved type names used by the generated output's built-in classes,
4
5
  * functions, and types. User-defined schema names that collide with these
5
6
  * are automatically renamed with a suffix to prevent duplicate identifiers.
6
7
  */
7
8
  export declare const RESERVED_TYPE_NAMES: ReadonlySet<string>;
9
+ /** Default module specifier generated code imports shared runtime classes from. */
10
+ export declare const DEFAULT_RUNTIME_IMPORT_PATH = "genoc/runtime";
8
11
  /**
9
- * Sanitize a schema type name by converting dot-separated segments into
10
- * PascalCase. Names without dots pass through unchanged.
12
+ * Sanitize a schema type name into a valid TypeScript identifier.
11
13
  *
12
- * "Api.Error" → "ApiError", "Simple" → "Simple"
14
+ * Any run of non-identifier characters (dots, brackets, backticks, hyphens,
15
+ * spaces, slashes, ...) splits the name into segments that are re-joined in
16
+ * PascalCase:
17
+ *
18
+ * "Api.Error" → "ApiError"
19
+ * "User[Dto]" → "UserDto"
20
+ * "my-schema" → "MySchema"
21
+ * "payment id" → "PaymentId"
22
+ *
23
+ * Names that are already valid identifiers pass through unchanged
24
+ * ("User", "my_schema", "mySchema", "$Foo", "Данные"). Results that would
25
+ * start with a digit ("2FA") get an "_" prefix, as do exact reserved words
26
+ * ("class" → "_class"). Names with no usable characters collapse to "_".
13
27
  */
14
28
  export declare function sanitizeTypeName(name: string): string;
15
29
  /**
16
- * Build a mapping of sanitized schema names → renamed names for any that
17
- * collide with reserved type names.
30
+ * Build a mapping of raw schema names → renamed names for any whose
31
+ * sanitized name collides with a reserved type name or with another
32
+ * schema's sanitized name (e.g. "User-Dto" and "User[Dto]" both → "UserDto").
33
+ *
34
+ * The map is keyed by the raw schema name (component keys are unique by
35
+ * construction), so callers resolve raw $ref segments / schema keys first
36
+ * and fall back to sanitizeTypeName for the common no-collision case.
37
+ * Numbered suffixes are used when the first renamed candidate also collides.
18
38
  */
19
39
  export declare function buildSchemaRenameMap(schemaNames: Iterable<string>, reserved: ReadonlySet<string>, suffix?: string): Map<string, string>;
20
40
  /**
@@ -25,6 +45,10 @@ export declare function toPascalCase(str: string): string;
25
45
  /**
26
46
  * Build a PascalCase type-name prefix from an operation's method + path.
27
47
  * get + /api/v1/products → "GetApiV1Products"
48
+ *
49
+ * Returns the deduped prefix assigned by analyzePaths when available
50
+ * (distinct routes folding to the same identifier get numbered suffixes),
51
+ * otherwise computes it from the operation directly.
28
52
  */
29
53
  export declare function getOperationTypePrefix(op: AnalyzedOperation): string;
30
54
  /**
@@ -39,3 +63,34 @@ export declare function getErrorType(op: AnalyzedOperation): string;
39
63
  * Generate the auto-generated header comment.
40
64
  */
41
65
  export declare function makeHeader(version: string): string;
66
+ /**
67
+ * Sanitize free-form text for safe inclusion inside a JSDoc comment.
68
+ *
69
+ * Normalizes CRLF to LF, flattens remaining newlines into single spaces,
70
+ * escapes the comment-closing sequence (asterisk + slash) as `*\/`, and
71
+ * trims leading/trailing whitespace. No other transformation is applied —
72
+ * no HTML stripping, no markdown processing, no wrapping.
73
+ */
74
+ export declare function sanitizeJsDocText(text: string): string;
75
+ /**
76
+ * Format an arbitrary metadata value for a JSDoc tag using JSON.stringify
77
+ * (no indent). Returns `null` when stringify yields `undefined` (e.g. for
78
+ * `undefined` or function values) — callers should skip the tag entirely.
79
+ */
80
+ export declare function formatJsDocValue(value: unknown): string | null;
81
+ /**
82
+ * Build the ordered JSDoc content segments for a schema-backed property.
83
+ *
84
+ * Order (frozen): description → `@deprecated` → `@default` → `@example`(s)
85
+ * → `@title`. Segments are skipped when their value is absent, empty, or
86
+ * whitespace-only. Sanitization is applied AFTER stringification for
87
+ * `@default` / `@example` values.
88
+ */
89
+ export declare function buildFieldJsDocLines(schema: SchemaObject): string[];
90
+ /**
91
+ * Render a schema's JSDoc metadata as a complete comment block.
92
+ *
93
+ * 0 segments → `''`; exactly 1 segment → single-line `/** text *\/`;
94
+ * ≥2 segments → multiline block with a blank ` *` line between segments.
95
+ */
96
+ export declare function buildTypeJsDoc(schema: SchemaObject): string;
@@ -1,3 +1,87 @@
1
+ /**
2
+ * TypeScript/JavaScript reserved words, matched case-sensitively: a type
3
+ * named "Null" or "Class" is legal, only the exact lowercase keywords are.
4
+ */
5
+ const RESERVED_WORDS = new Set([
6
+ 'abstract',
7
+ 'any',
8
+ 'as',
9
+ 'asserts',
10
+ 'assert',
11
+ 'async',
12
+ 'await',
13
+ 'boolean',
14
+ 'break',
15
+ 'case',
16
+ 'catch',
17
+ 'class',
18
+ 'continue',
19
+ 'const',
20
+ 'constructor',
21
+ 'debugger',
22
+ 'declare',
23
+ 'default',
24
+ 'delete',
25
+ 'do',
26
+ 'else',
27
+ 'enum',
28
+ 'export',
29
+ 'extends',
30
+ 'false',
31
+ 'finally',
32
+ 'for',
33
+ 'from',
34
+ 'function',
35
+ 'get',
36
+ 'if',
37
+ 'implements',
38
+ 'import',
39
+ 'in',
40
+ 'infer',
41
+ 'instanceof',
42
+ 'interface',
43
+ 'is',
44
+ 'keyof',
45
+ 'let',
46
+ 'module',
47
+ 'namespace',
48
+ 'never',
49
+ 'new',
50
+ 'null',
51
+ 'number',
52
+ 'object',
53
+ 'package',
54
+ 'private',
55
+ 'protected',
56
+ 'public',
57
+ 'readonly',
58
+ 'require',
59
+ 'return',
60
+ 'satisfies',
61
+ 'set',
62
+ 'static',
63
+ 'string',
64
+ 'super',
65
+ 'switch',
66
+ 'symbol',
67
+ 'this',
68
+ 'throw',
69
+ 'true',
70
+ 'try',
71
+ 'type',
72
+ 'typeof',
73
+ 'undefined',
74
+ 'unique',
75
+ 'unknown',
76
+ 'var',
77
+ 'void',
78
+ 'while',
79
+ 'with',
80
+ 'yield',
81
+ ]);
82
+ function isReservedTypeName(name) {
83
+ return RESERVED_WORDS.has(name);
84
+ }
1
85
  /**
2
86
  * Reserved type names used by the generated output's built-in classes,
3
87
  * functions, and types. User-defined schema names that collide with these
@@ -11,9 +95,6 @@ export const RESERVED_TYPE_NAMES = new Set([
11
95
  'UnspecifiedApiError',
12
96
  'DefaultApiError',
13
97
  'RequesterFailError',
14
- // Functions in contracts.ts
15
- 'streamResponse',
16
- 'errorResponse',
17
98
  // Types / functions in client.ts
18
99
  'Requester',
19
100
  'isDefinedError',
@@ -22,31 +103,75 @@ export const RESERVED_TYPE_NAMES = new Set([
22
103
  // Interface in contracts.ts (when file uploads present)
23
104
  'FileInput',
24
105
  ]);
106
+ /** Default module specifier generated code imports shared runtime classes from. */
107
+ export const DEFAULT_RUNTIME_IMPORT_PATH = 'genoc/runtime';
25
108
  /**
26
- * Sanitize a schema type name by converting dot-separated segments into
27
- * PascalCase. Names without dots pass through unchanged.
109
+ * Sanitize a schema type name into a valid TypeScript identifier.
110
+ *
111
+ * Any run of non-identifier characters (dots, brackets, backticks, hyphens,
112
+ * spaces, slashes, ...) splits the name into segments that are re-joined in
113
+ * PascalCase:
28
114
  *
29
- * "Api.Error" → "ApiError", "Simple" → "Simple"
115
+ * "Api.Error" → "ApiError"
116
+ * "User[Dto]" → "UserDto"
117
+ * "my-schema" → "MySchema"
118
+ * "payment id" → "PaymentId"
119
+ *
120
+ * Names that are already valid identifiers pass through unchanged
121
+ * ("User", "my_schema", "mySchema", "$Foo", "Данные"). Results that would
122
+ * start with a digit ("2FA") get an "_" prefix, as do exact reserved words
123
+ * ("class" → "_class"). Names with no usable characters collapse to "_".
30
124
  */
31
125
  export function sanitizeTypeName(name) {
32
- if (!name.includes('.'))
126
+ if (!name)
127
+ return '_';
128
+ // Exact reserved words keep their casing and get an "_" prefix
129
+ // ("class" → "_class"), while "Null" / "Class" are legal and pass through.
130
+ if (isReservedTypeName(name))
131
+ return `_${name}`;
132
+ const isValidIdentifier = /^[\p{ID_Start}$_][\p{ID_Continue}$]*$/u.test(name);
133
+ if (isValidIdentifier)
33
134
  return name;
34
- return name
35
- .split('.')
135
+ const segments = name.match(/[\p{ID_Continue}$]+/gu) ?? [];
136
+ const result = segments
36
137
  .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
37
138
  .join('');
139
+ // Covers empty results, ASCII digits, and non-ASCII digits (Nd but not
140
+ // ID_Start, e.g. Arabic-Indic) alike: anything not starting with a valid
141
+ // identifier start character gets the "_" prefix.
142
+ if (!/^[\p{ID_Start}$_]/u.test(result))
143
+ return `_${result}`;
144
+ if (isReservedTypeName(result))
145
+ return `_${result}`;
146
+ return result;
38
147
  }
39
148
  /**
40
- * Build a mapping of sanitized schema names → renamed names for any that
41
- * collide with reserved type names.
149
+ * Build a mapping of raw schema names → renamed names for any whose
150
+ * sanitized name collides with a reserved type name or with another
151
+ * schema's sanitized name (e.g. "User-Dto" and "User[Dto]" both → "UserDto").
152
+ *
153
+ * The map is keyed by the raw schema name (component keys are unique by
154
+ * construction), so callers resolve raw $ref segments / schema keys first
155
+ * and fall back to sanitizeTypeName for the common no-collision case.
156
+ * Numbered suffixes are used when the first renamed candidate also collides.
42
157
  */
43
158
  export function buildSchemaRenameMap(schemaNames, reserved, suffix = 'Model') {
44
159
  const renameMap = new Map();
160
+ const used = new Set(reserved);
45
161
  for (const name of schemaNames) {
46
162
  const sanitized = sanitizeTypeName(name);
47
- if (reserved.has(sanitized)) {
48
- renameMap.set(sanitized, `${sanitized}${suffix}`);
163
+ if (!used.has(sanitized)) {
164
+ used.add(sanitized);
165
+ continue;
49
166
  }
167
+ let candidate = `${sanitized}${suffix}`;
168
+ let counter = 2;
169
+ while (used.has(candidate)) {
170
+ candidate = `${sanitized}${suffix}${counter}`;
171
+ counter += 1;
172
+ }
173
+ used.add(candidate);
174
+ renameMap.set(name, candidate);
50
175
  }
51
176
  return renameMap;
52
177
  }
@@ -65,15 +190,24 @@ export function toPascalCase(str) {
65
190
  /**
66
191
  * Build a PascalCase type-name prefix from an operation's method + path.
67
192
  * get + /api/v1/products → "GetApiV1Products"
193
+ *
194
+ * Returns the deduped prefix assigned by analyzePaths when available
195
+ * (distinct routes folding to the same identifier get numbered suffixes),
196
+ * otherwise computes it from the operation directly.
68
197
  */
69
198
  export function getOperationTypePrefix(op) {
199
+ if (op.typePrefix)
200
+ return op.typePrefix;
201
+ return computeOperationTypePrefix(op);
202
+ }
203
+ function computeOperationTypePrefix(op) {
70
204
  const methodPascal = op.method.charAt(0).toUpperCase() + op.method.slice(1).toLowerCase();
71
205
  const segments = op.path
72
206
  .split('/')
73
207
  .filter((s) => s.length > 0)
74
208
  .map((s) => {
75
209
  const cleaned = s.replace(/[{}]/g, '');
76
- return toPascalCase(cleaned);
210
+ return sanitizeTypeName(toPascalCase(cleaned));
77
211
  });
78
212
  return methodPascal + segments.join('');
79
213
  }
@@ -116,3 +250,78 @@ export function getErrorType(op) {
116
250
  export function makeHeader(version) {
117
251
  return `// Auto-generated by genoc from OpenAPI ${version} spec. DO NOT EDIT.`;
118
252
  }
253
+ /**
254
+ * Sanitize free-form text for safe inclusion inside a JSDoc comment.
255
+ *
256
+ * Normalizes CRLF to LF, flattens remaining newlines into single spaces,
257
+ * escapes the comment-closing sequence (asterisk + slash) as `*\/`, and
258
+ * trims leading/trailing whitespace. No other transformation is applied —
259
+ * no HTML stripping, no markdown processing, no wrapping.
260
+ */
261
+ export function sanitizeJsDocText(text) {
262
+ return text.replace(/\r\n/g, '\n').replace(/\n/g, ' ').replace(/\*\//g, '*\\/').trim();
263
+ }
264
+ /**
265
+ * Format an arbitrary metadata value for a JSDoc tag using JSON.stringify
266
+ * (no indent). Returns `null` when stringify yields `undefined` (e.g. for
267
+ * `undefined` or function values) — callers should skip the tag entirely.
268
+ */
269
+ export function formatJsDocValue(value) {
270
+ const json = JSON.stringify(value);
271
+ return json === undefined ? null : json;
272
+ }
273
+ /**
274
+ * Build the ordered JSDoc content segments for a schema-backed property.
275
+ *
276
+ * Order (frozen): description → `@deprecated` → `@default` → `@example`(s)
277
+ * → `@title`. Segments are skipped when their value is absent, empty, or
278
+ * whitespace-only. Sanitization is applied AFTER stringification for
279
+ * `@default` / `@example` values.
280
+ */
281
+ export function buildFieldJsDocLines(schema) {
282
+ const lines = [];
283
+ if (schema.description !== undefined && schema.description.trim() !== '') {
284
+ lines.push(sanitizeJsDocText(schema.description));
285
+ }
286
+ if (schema.deprecated === true) {
287
+ lines.push('@deprecated');
288
+ }
289
+ if (schema.default !== undefined) {
290
+ const formatted = formatJsDocValue(schema.default);
291
+ if (formatted !== null) {
292
+ lines.push(`@default ${sanitizeJsDocText(formatted)}`);
293
+ }
294
+ }
295
+ if (schema.example !== undefined) {
296
+ const formatted = formatJsDocValue(schema.example);
297
+ if (formatted !== null) {
298
+ lines.push(`@example ${sanitizeJsDocText(formatted)}`);
299
+ }
300
+ }
301
+ if (schema.examples !== undefined) {
302
+ for (const value of schema.examples) {
303
+ const formatted = formatJsDocValue(value);
304
+ if (formatted !== null) {
305
+ lines.push(`@example ${sanitizeJsDocText(formatted)}`);
306
+ }
307
+ }
308
+ }
309
+ if (schema.title) {
310
+ lines.push(`@title ${sanitizeJsDocText(schema.title)}`);
311
+ }
312
+ return lines;
313
+ }
314
+ /**
315
+ * Render a schema's JSDoc metadata as a complete comment block.
316
+ *
317
+ * 0 segments → `''`; exactly 1 segment → single-line `/** text *\/`;
318
+ * ≥2 segments → multiline block with a blank ` *` line between segments.
319
+ */
320
+ export function buildTypeJsDoc(schema) {
321
+ const lines = buildFieldJsDocLines(schema);
322
+ if (lines.length === 0)
323
+ return '';
324
+ if (lines.length === 1)
325
+ return `/** ${lines[0]} */`;
326
+ return `/**\n${lines.map((line) => ` * ${line}`).join('\n *\n')}\n */`;
327
+ }
@@ -0,0 +1,6 @@
1
+ export type LoadOptions = {
2
+ proxy?: string;
3
+ };
4
+ export declare function assertValidProxyUrl(url: string): void;
5
+ export declare function hasProxyEnv(): boolean;
6
+ export declare function fetchSpec(url: string, opts?: LoadOptions): Promise<Response>;