@depup/openapi-typescript 7.13.0-depup.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 (106) hide show
  1. package/CHANGELOG.md +1162 -0
  2. package/CONTRIBUTING.md +149 -0
  3. package/LICENSE +21 -0
  4. package/README.md +32 -0
  5. package/bin/cli.js +297 -0
  6. package/changes.json +14 -0
  7. package/dist/index.cjs +152 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.d.cts +517 -0
  10. package/dist/index.d.mts +515 -0
  11. package/dist/index.d.ts +517 -0
  12. package/dist/index.mjs +85 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/lib/redoc.cjs +129 -0
  15. package/dist/lib/redoc.cjs.map +1 -0
  16. package/dist/lib/redoc.mjs +122 -0
  17. package/dist/lib/redoc.mjs.map +1 -0
  18. package/dist/lib/ts.cjs +470 -0
  19. package/dist/lib/ts.cjs.map +1 -0
  20. package/dist/lib/ts.mjs +431 -0
  21. package/dist/lib/ts.mjs.map +1 -0
  22. package/dist/lib/utils.cjs +295 -0
  23. package/dist/lib/utils.cjs.map +1 -0
  24. package/dist/lib/utils.mjs +278 -0
  25. package/dist/lib/utils.mjs.map +1 -0
  26. package/dist/transform/components-object.cjs +150 -0
  27. package/dist/transform/components-object.cjs.map +1 -0
  28. package/dist/transform/components-object.mjs +127 -0
  29. package/dist/transform/components-object.mjs.map +1 -0
  30. package/dist/transform/header-object.cjs +48 -0
  31. package/dist/transform/header-object.cjs.map +1 -0
  32. package/dist/transform/header-object.mjs +42 -0
  33. package/dist/transform/header-object.mjs.map +1 -0
  34. package/dist/transform/index.cjs +124 -0
  35. package/dist/transform/index.cjs.map +1 -0
  36. package/dist/transform/index.mjs +118 -0
  37. package/dist/transform/index.mjs.map +1 -0
  38. package/dist/transform/media-type-object.cjs +14 -0
  39. package/dist/transform/media-type-object.cjs.map +1 -0
  40. package/dist/transform/media-type-object.mjs +12 -0
  41. package/dist/transform/media-type-object.mjs.map +1 -0
  42. package/dist/transform/operation-object.cjs +105 -0
  43. package/dist/transform/operation-object.cjs.map +1 -0
  44. package/dist/transform/operation-object.mjs +96 -0
  45. package/dist/transform/operation-object.mjs.map +1 -0
  46. package/dist/transform/parameter-object.cjs +11 -0
  47. package/dist/transform/parameter-object.cjs.map +1 -0
  48. package/dist/transform/parameter-object.mjs +9 -0
  49. package/dist/transform/parameter-object.mjs.map +1 -0
  50. package/dist/transform/parameters-array.cjs +120 -0
  51. package/dist/transform/parameters-array.cjs.map +1 -0
  52. package/dist/transform/parameters-array.mjs +114 -0
  53. package/dist/transform/parameters-array.mjs.map +1 -0
  54. package/dist/transform/path-item-object.cjs +84 -0
  55. package/dist/transform/path-item-object.cjs.map +1 -0
  56. package/dist/transform/path-item-object.mjs +78 -0
  57. package/dist/transform/path-item-object.mjs.map +1 -0
  58. package/dist/transform/paths-enum.cjs +35 -0
  59. package/dist/transform/paths-enum.cjs.map +1 -0
  60. package/dist/transform/paths-enum.mjs +33 -0
  61. package/dist/transform/paths-enum.mjs.map +1 -0
  62. package/dist/transform/paths-object.cjs +134 -0
  63. package/dist/transform/paths-object.cjs.map +1 -0
  64. package/dist/transform/paths-object.mjs +128 -0
  65. package/dist/transform/paths-object.mjs.map +1 -0
  66. package/dist/transform/request-body-object.cjs +68 -0
  67. package/dist/transform/request-body-object.cjs.map +1 -0
  68. package/dist/transform/request-body-object.mjs +62 -0
  69. package/dist/transform/request-body-object.mjs.map +1 -0
  70. package/dist/transform/response-object.cjs +123 -0
  71. package/dist/transform/response-object.cjs.map +1 -0
  72. package/dist/transform/response-object.mjs +117 -0
  73. package/dist/transform/response-object.mjs.map +1 -0
  74. package/dist/transform/responses-object.cjs +36 -0
  75. package/dist/transform/responses-object.cjs.map +1 -0
  76. package/dist/transform/responses-object.mjs +30 -0
  77. package/dist/transform/responses-object.mjs.map +1 -0
  78. package/dist/transform/schema-object.cjs +502 -0
  79. package/dist/transform/schema-object.cjs.map +1 -0
  80. package/dist/transform/schema-object.mjs +493 -0
  81. package/dist/transform/schema-object.mjs.map +1 -0
  82. package/dist/transform/webhooks-object.cjs +37 -0
  83. package/dist/transform/webhooks-object.cjs.map +1 -0
  84. package/dist/transform/webhooks-object.mjs +31 -0
  85. package/dist/transform/webhooks-object.mjs.map +1 -0
  86. package/package.json +102 -0
  87. package/src/index.ts +108 -0
  88. package/src/lib/redoc.ts +164 -0
  89. package/src/lib/ts.ts +616 -0
  90. package/src/lib/utils.ts +394 -0
  91. package/src/transform/components-object.ts +161 -0
  92. package/src/transform/header-object.ts +45 -0
  93. package/src/transform/index.ts +116 -0
  94. package/src/transform/media-type-object.ts +18 -0
  95. package/src/transform/operation-object.ts +104 -0
  96. package/src/transform/parameter-object.ts +15 -0
  97. package/src/transform/parameters-array.ts +129 -0
  98. package/src/transform/path-item-object.ts +102 -0
  99. package/src/transform/paths-enum.ts +43 -0
  100. package/src/transform/paths-object.ts +134 -0
  101. package/src/transform/request-body-object.ts +59 -0
  102. package/src/transform/response-object.ts +112 -0
  103. package/src/transform/responses-object.ts +36 -0
  104. package/src/transform/schema-object.ts +737 -0
  105. package/src/transform/webhooks-object.ts +27 -0
  106. package/src/types.ts +735 -0
package/src/lib/ts.ts ADDED
@@ -0,0 +1,616 @@
1
+ import type { OasRef, Referenced } from "@redocly/openapi-core";
2
+ import { parseRef } from "@redocly/openapi-core/lib/ref-utils.js";
3
+ import ts, { type LiteralTypeNode, type TypeLiteralNode } from "typescript";
4
+ import type { ParameterObject } from "../types.js";
5
+
6
+ export const JS_PROPERTY_INDEX_RE = /^[A-Za-z_$][A-Za-z_$0-9]*$/;
7
+ export const JS_ENUM_INVALID_CHARS_RE = /[^A-Za-z_$0-9]+(.)?/g;
8
+ export const JS_PROPERTY_INDEX_INVALID_CHARS_RE = /[^A-Za-z_$0-9]+/g;
9
+ export const SPECIAL_CHARACTER_MAP: Record<string, string> = {
10
+ "+": "Plus",
11
+ // Add more mappings as needed
12
+ };
13
+
14
+ export const BOOLEAN = ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
15
+ export const FALSE = ts.factory.createLiteralTypeNode(ts.factory.createFalse());
16
+ export const NEVER = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword);
17
+ export const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull());
18
+ export const NUMBER = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
19
+ export const QUESTION_TOKEN = ts.factory.createToken(ts.SyntaxKind.QuestionToken);
20
+ export const STRING = ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
21
+ export const TRUE = ts.factory.createLiteralTypeNode(ts.factory.createTrue());
22
+ export const UNDEFINED = ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword);
23
+ export const UNKNOWN = ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
24
+
25
+ const LB_RE = /\r?\n/g;
26
+ const COMMENT_RE = /\*\//g;
27
+
28
+ export interface AnnotatedSchemaObject {
29
+ const?: unknown; // jsdoc without value
30
+ default?: unknown; // jsdoc with value
31
+ deprecated?: boolean; // jsdoc without value
32
+ description?: string; // jsdoc with value
33
+ enum?: unknown[]; // jsdoc without value
34
+ example?: string; // jsdoc with value
35
+ examples?: unknown;
36
+ format?: string; // not jsdoc
37
+ nullable?: boolean; // Node information
38
+ summary?: string; // not jsdoc
39
+ title?: string; // not jsdoc
40
+ type?: string | string[]; // Type of node
41
+ }
42
+
43
+ /**
44
+ * Preparing comments from fields
45
+ * @see {comment} for output examples
46
+ * @returns void if not comments or jsdoc format comment string
47
+ */
48
+ export function addJSDocComment(schemaObject: AnnotatedSchemaObject, node: ts.PropertySignature): void {
49
+ if (!schemaObject || typeof schemaObject !== "object" || Array.isArray(schemaObject)) {
50
+ return;
51
+ }
52
+ const output: string[] = [];
53
+
54
+ // Not JSDoc tags: [title, format]
55
+ if (schemaObject.title) {
56
+ output.push(schemaObject.title.trim().replace(LB_RE, "\n * "));
57
+ }
58
+ if (schemaObject.summary) {
59
+ output.push(schemaObject.summary.trim().replace(LB_RE, "\n * "));
60
+ }
61
+ if (schemaObject.format) {
62
+ output.push(`Format: ${schemaObject.format}`);
63
+ }
64
+
65
+ // JSDoc tags without value
66
+ // 'Deprecated' without value
67
+ if (schemaObject.deprecated) {
68
+ output.push("@deprecated");
69
+ }
70
+
71
+ // JSDoc tags with value
72
+ const supportedJsDocTags = ["description", "default", "example"] as const;
73
+ for (const field of supportedJsDocTags) {
74
+ const allowEmptyString = field === "default" || field === "example";
75
+ if (schemaObject[field] === undefined) {
76
+ continue;
77
+ }
78
+ if (schemaObject[field] === "" && !allowEmptyString) {
79
+ continue;
80
+ }
81
+ const serialized =
82
+ typeof schemaObject[field] === "object" ? JSON.stringify(schemaObject[field], null, 2) : schemaObject[field];
83
+ output.push(`@${field} ${String(serialized).trim().replace(LB_RE, "\n * ")}`);
84
+ }
85
+
86
+ if (Array.isArray(schemaObject.examples)) {
87
+ for (const example of schemaObject.examples) {
88
+ const serialized = typeof example === "object" ? JSON.stringify(example, null, 2) : example;
89
+ output.push(`@example ${String(serialized).trim().replace(LB_RE, "\n * ")}`);
90
+ }
91
+ }
92
+
93
+ // JSDoc 'Constant' without value
94
+ if ("const" in schemaObject) {
95
+ output.push("@constant");
96
+ }
97
+
98
+ // JSDoc 'Enum' with type
99
+ if (schemaObject.enum) {
100
+ let type = "unknown";
101
+ if (Array.isArray(schemaObject.type)) {
102
+ type = schemaObject.type.join("|");
103
+ } else if (typeof schemaObject.type === "string") {
104
+ type = schemaObject.type;
105
+ }
106
+ output.push(`@enum {${type}${schemaObject.nullable ? "|null" : ""}}`);
107
+ }
108
+
109
+ // attach comment if it has content
110
+
111
+ if (output.length) {
112
+ // Check if any output item contains multi-line content (has internal line breaks)
113
+ const hasMultiLineContent = output.some((item) => item.includes("\n"));
114
+
115
+ let text =
116
+ output.length === 1 && !hasMultiLineContent ? `* ${output.join("\n")} ` : `*\n * ${output.join("\n * ")}\n `;
117
+ text = text.replace(COMMENT_RE, "*\\/"); // prevent inner comments from leaking
118
+
119
+ ts.addSyntheticLeadingComment(
120
+ /* node */ node,
121
+ /* kind */ ts.SyntaxKind.MultiLineCommentTrivia, // note: MultiLine just refers to a "/* */" comment
122
+ /* text */ text,
123
+ /* hasTrailingNewLine */ true,
124
+ );
125
+ }
126
+ }
127
+
128
+ function isOasRef<T>(obj: Referenced<T>): obj is OasRef {
129
+ return Boolean((obj as OasRef).$ref);
130
+ }
131
+ type OapiRefResolved = Referenced<ParameterObject>;
132
+
133
+ function isParameterObject(obj: OapiRefResolved | undefined): obj is ParameterObject {
134
+ return Boolean(obj && !isOasRef(obj) && obj.in);
135
+ }
136
+
137
+ function addIndexedAccess(node: ts.TypeNode, ...segments: readonly string[]) {
138
+ return segments.reduce<ts.TypeNode>((acc, segment) => {
139
+ return ts.factory.createIndexedAccessTypeNode(
140
+ acc,
141
+ ts.factory.createLiteralTypeNode(
142
+ typeof segment === "number"
143
+ ? ts.factory.createNumericLiteral(segment)
144
+ : ts.factory.createStringLiteral(segment),
145
+ ),
146
+ );
147
+ }, node);
148
+ }
149
+
150
+ /**
151
+ * Wrap a type with Extract<T, { propertyName: unknown }> to narrow a union type
152
+ * before accessing a property that only exists on some variants.
153
+ */
154
+ function wrapWithExtract(type: ts.TypeNode, propertyName: string): ts.TypeNode {
155
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Extract"), [
156
+ type,
157
+ ts.factory.createTypeLiteralNode([
158
+ ts.factory.createPropertySignature(
159
+ /* modifiers */ undefined,
160
+ /* name */ ts.factory.createIdentifier(propertyName),
161
+ /* questionToken */ undefined,
162
+ /* type */ ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
163
+ ),
164
+ ]),
165
+ ]);
166
+ }
167
+
168
+ export interface OapiRefOptions {
169
+ /** Whether to wrap with FlattenedDeepRequired<> (default: false) */
170
+ deep?: boolean;
171
+ /** Array of property names to wrap with Extract<> when accessing */
172
+ extractProperties?: string[];
173
+ }
174
+
175
+ /**
176
+ * Convert OpenAPI ref into TS indexed access node (ex: `components["schemas"]["Foo"]`)
177
+ * `path` is a JSON Pointer to a location within an OpenAPI document.
178
+ * Transform it into a TypeScript type reference into the generated types.
179
+ *
180
+ * In most cases the structures of the openapi-typescript generated types and the
181
+ * JSON Pointer paths into the OpenAPI document are the same. However, in some cases
182
+ * special transformations are necessary to account for the ways they differ.
183
+ * * Object schemas
184
+ * $refs into the `properties` of object schemas are valid, but openapi-typescript
185
+ * flattens these objects, so we omit so the index into the schema skips ["properties"]
186
+ * * Parameters
187
+ * $refs into the `parameters` of paths are valid, but openapi-ts represents
188
+ * them according to their type; path, query, header, etc… so in these cases we
189
+ * must check the parameter definition to determine the how to index into
190
+ * the openapi-typescript type.
191
+ * * Union variant properties (oneOf/anyOf)
192
+ * When accessing properties that may only exist on some variants of a union type,
193
+ * we use Extract<> to narrow the type before each property access.
194
+ **/
195
+ export function oapiRef(path: string, resolved?: OapiRefResolved, options: OapiRefOptions = {}): ts.TypeNode {
196
+ const { pointer } = parseRef(path);
197
+ if (pointer.length === 0) {
198
+ throw new Error(`Error parsing $ref: ${path}. Is this a valid $ref?`);
199
+ }
200
+
201
+ const parametersObject = isParameterObject(resolved);
202
+ const extractSet = new Set(options.extractProperties ?? []);
203
+
204
+ // Initial segments are handled in a fixed , then remaining segments are treated
205
+ // according to heuristics based on the initial segments
206
+ const initialSegment = pointer[0];
207
+ const leadingSegments = pointer.slice(1, 3);
208
+ const restSegments = pointer.slice(3);
209
+
210
+ const leadingType = addIndexedAccess(
211
+ ts.factory.createTypeReferenceNode(
212
+ ts.factory.createIdentifier(
213
+ options.deep ? `FlattenedDeepRequired<${String(initialSegment)}>` : String(initialSegment),
214
+ ),
215
+ ),
216
+ ...leadingSegments,
217
+ );
218
+
219
+ return restSegments.reduce<ts.TypeNode>((acc, segment, index, original) => {
220
+ // Skip `properties` items when in the middle of the pointer
221
+ // See: https://github.com/openapi-ts/openapi-typescript/issues/1742
222
+ if (segment === "properties") {
223
+ return acc;
224
+ }
225
+
226
+ if (parametersObject && index === original.length - 1) {
227
+ return addIndexedAccess(acc, resolved.in, resolved.name);
228
+ }
229
+
230
+ // If this segment is in the extractProperties list,
231
+ // wrap the current type with Extract<T, { segment: unknown }> before accessing.
232
+ // This narrows union types to variants that have this property.
233
+ if (extractSet.has(segment)) {
234
+ const narrowedType = wrapWithExtract(acc, segment);
235
+ return addIndexedAccess(narrowedType, segment);
236
+ }
237
+
238
+ return addIndexedAccess(acc, segment);
239
+ }, leadingType);
240
+ }
241
+
242
+ export interface AstToStringOptions {
243
+ fileName?: string;
244
+ sourceText?: string;
245
+ formatOptions?: ts.PrinterOptions;
246
+ }
247
+
248
+ /** Convert TypeScript AST to string */
249
+ export function astToString(
250
+ ast: ts.Node | ts.Node[] | ts.TypeElement | ts.TypeElement[],
251
+ options?: AstToStringOptions,
252
+ ): string {
253
+ const sourceFile = ts.createSourceFile(
254
+ options?.fileName ?? "openapi-ts.ts",
255
+ options?.sourceText ?? "",
256
+ ts.ScriptTarget.ESNext,
257
+ false,
258
+ ts.ScriptKind.TS,
259
+ );
260
+
261
+ // @ts-expect-error it’s OK to overwrite statements once
262
+ sourceFile.statements = ts.factory.createNodeArray(Array.isArray(ast) ? ast : [ast]);
263
+
264
+ const printer = ts.createPrinter({
265
+ newLine: ts.NewLineKind.LineFeed,
266
+ removeComments: false,
267
+ ...options?.formatOptions,
268
+ });
269
+ return printer.printFile(sourceFile);
270
+ }
271
+
272
+ /** Convert an arbitrary string to TS (assuming it’s valid) */
273
+ export function stringToAST(source: string): unknown[] {
274
+ return ts.createSourceFile(
275
+ /* fileName */ "stringInput",
276
+ /* sourceText */ source,
277
+ /* languageVersion */ ts.ScriptTarget.ESNext,
278
+ /* setParentNodes */ undefined,
279
+ /* scriptKind */ undefined,
280
+ ).statements as any;
281
+ }
282
+
283
+ /**
284
+ * Deduplicate simple primitive types from an array of nodes
285
+ * Note: won’t deduplicate complex types like objects
286
+ */
287
+ export function tsDedupe(types: ts.TypeNode[]): ts.TypeNode[] {
288
+ const encounteredTypes = new Set<number>();
289
+ const filteredTypes: ts.TypeNode[] = [];
290
+ for (const t of types) {
291
+ // only mark for deduplication if this is not a const ("text" means it is a const)
292
+ if (!("text" in ((t as LiteralTypeNode).literal ?? t))) {
293
+ const { kind } = (t as LiteralTypeNode).literal ?? t;
294
+ if (encounteredTypes.has(kind)) {
295
+ continue;
296
+ }
297
+ if (tsIsPrimitive(t)) {
298
+ encounteredTypes.add(kind);
299
+ }
300
+ }
301
+ filteredTypes.push(t);
302
+ }
303
+ return filteredTypes;
304
+ }
305
+
306
+ export const enumCache = new Map<string, ts.EnumDeclaration>();
307
+
308
+ /** Create a TS enum (with sanitized name and members) */
309
+ export function tsEnum(
310
+ name: string,
311
+ members: (string | number)[],
312
+ metadata?: { name?: string; description?: string | null }[],
313
+ options?: { export?: boolean; shouldCache?: boolean },
314
+ ) {
315
+ let enumName = sanitizeMemberName(name);
316
+ enumName = `${enumName[0].toUpperCase()}${enumName.substring(1)}`;
317
+ let key = "";
318
+ if (options?.shouldCache) {
319
+ key = `${members
320
+ .slice(0)
321
+ .sort()
322
+ .map((v, i) => {
323
+ return `${metadata?.[i]?.name ?? String(v)}:${metadata?.[i]?.description || ""}`;
324
+ })
325
+ .join(",")}`;
326
+ if (enumCache.has(key)) {
327
+ return enumCache.get(key) as ts.EnumDeclaration;
328
+ }
329
+ }
330
+ const enumDeclaration = ts.factory.createEnumDeclaration(
331
+ /* modifiers */ options ? tsModifiers({ export: options.export ?? false }) : undefined,
332
+ /* name */ enumName,
333
+ /* members */ members.map((value, i) => tsEnumMember(value, metadata?.[i])),
334
+ );
335
+ options?.shouldCache && enumCache.set(key, enumDeclaration);
336
+ return enumDeclaration;
337
+ }
338
+
339
+ /** Create an exported TS array literal expression */
340
+ export function tsArrayLiteralExpression(
341
+ name: string,
342
+ elementType: ts.TypeNode,
343
+ values: (string | number)[],
344
+ options?: { export?: boolean; readonly?: boolean; injectFooter?: ts.Node[] },
345
+ ) {
346
+ let variableName = sanitizeMemberName(name);
347
+ variableName = `${variableName[0].toLowerCase()}${variableName.substring(1)}`;
348
+
349
+ if (
350
+ options?.injectFooter &&
351
+ !options.injectFooter.some(
352
+ (node) => ts.isTypeAliasDeclaration(node) && node?.name?.escapedText === "FlattenedDeepRequired",
353
+ )
354
+ ) {
355
+ const helper = stringToAST(
356
+ "type FlattenedDeepRequired<T> = { [K in keyof T]-?: FlattenedDeepRequired<T[K] extends unknown[] | undefined | null ? Extract<T[K], unknown[]>[number] : T[K]>; };",
357
+ )[0] as any;
358
+ options.injectFooter.push(helper);
359
+ }
360
+
361
+ const arrayType = options?.readonly
362
+ ? tsReadonlyArray(elementType, options.injectFooter)
363
+ : ts.factory.createArrayTypeNode(elementType);
364
+
365
+ return ts.factory.createVariableStatement(
366
+ options ? tsModifiers({ export: options.export ?? false }) : undefined,
367
+ ts.factory.createVariableDeclarationList(
368
+ [
369
+ ts.factory.createVariableDeclaration(
370
+ variableName,
371
+ undefined,
372
+ arrayType,
373
+ ts.factory.createArrayLiteralExpression(
374
+ values.map((value) => {
375
+ if (typeof value === "number") {
376
+ if (value < 0) {
377
+ return ts.factory.createPrefixUnaryExpression(
378
+ ts.SyntaxKind.MinusToken,
379
+ ts.factory.createNumericLiteral(Math.abs(value)),
380
+ );
381
+ } else {
382
+ return ts.factory.createNumericLiteral(value);
383
+ }
384
+ } else {
385
+ return ts.factory.createStringLiteral(value);
386
+ }
387
+ }),
388
+ ),
389
+ ),
390
+ ],
391
+ ts.NodeFlags.Const,
392
+ ),
393
+ );
394
+ }
395
+
396
+ function sanitizeMemberName(name: string) {
397
+ let sanitizedName = name.replace(JS_ENUM_INVALID_CHARS_RE, (c) => {
398
+ const last = c[c.length - 1];
399
+ return JS_PROPERTY_INDEX_INVALID_CHARS_RE.test(last) ? "" : last.toUpperCase();
400
+ });
401
+ if (Number(name[0]) >= 0) {
402
+ sanitizedName = `Value${name}`;
403
+ }
404
+ return sanitizedName;
405
+ }
406
+
407
+ /** Sanitize TS enum member expression */
408
+ export function tsEnumMember(value: string | number, metadata: { name?: string; description?: string | null } = {}) {
409
+ let name = metadata.name ?? String(value);
410
+ if (!JS_PROPERTY_INDEX_RE.test(name)) {
411
+ if (Number(name[0]) >= 0) {
412
+ name = `Value${name}`.replace(".", "_"); // don't forged decimals;
413
+ } else if (name[0] === "-") {
414
+ name = `ValueMinus${name.slice(1)}`;
415
+ }
416
+
417
+ const invalidCharMatch = name.match(JS_PROPERTY_INDEX_INVALID_CHARS_RE);
418
+ if (invalidCharMatch) {
419
+ if (invalidCharMatch[0] === name) {
420
+ name = `"${name}"`;
421
+ } else {
422
+ name = name.replace(JS_PROPERTY_INDEX_INVALID_CHARS_RE, (s) => {
423
+ return s in SPECIAL_CHARACTER_MAP ? SPECIAL_CHARACTER_MAP[s] : "_";
424
+ });
425
+ }
426
+ }
427
+ }
428
+
429
+ let member: ts.EnumMember;
430
+ if (typeof value === "number") {
431
+ const literal =
432
+ value < 0
433
+ ? ts.factory.createPrefixUnaryExpression(
434
+ ts.SyntaxKind.MinusToken,
435
+ ts.factory.createNumericLiteral(Math.abs(value)),
436
+ )
437
+ : ts.factory.createNumericLiteral(value);
438
+
439
+ member = ts.factory.createEnumMember(name, literal);
440
+ } else {
441
+ member = ts.factory.createEnumMember(name, ts.factory.createStringLiteral(value));
442
+ }
443
+
444
+ const trimmedDescription = metadata.description?.trim();
445
+ if (trimmedDescription === undefined || trimmedDescription === null || trimmedDescription === "") {
446
+ return member;
447
+ }
448
+
449
+ return ts.addSyntheticLeadingComment(member, ts.SyntaxKind.SingleLineCommentTrivia, ` ${trimmedDescription}`, true);
450
+ }
451
+
452
+ /** Create an intersection type */
453
+ export function tsIntersection(types: ts.TypeNode[]): ts.TypeNode {
454
+ if (types.length === 0) {
455
+ return NEVER;
456
+ }
457
+ if (types.length === 1) {
458
+ return types[0];
459
+ }
460
+ return ts.factory.createIntersectionTypeNode(tsDedupe(types));
461
+ }
462
+
463
+ /** Is this a primitive type (string, number, boolean, null, undefined)? */
464
+ export function tsIsPrimitive(type: ts.TypeNode): boolean {
465
+ if (!type) {
466
+ return true;
467
+ }
468
+ return (
469
+ ts.SyntaxKind[type.kind] === "BooleanKeyword" ||
470
+ ts.SyntaxKind[type.kind] === "NeverKeyword" ||
471
+ ts.SyntaxKind[type.kind] === "NullKeyword" ||
472
+ ts.SyntaxKind[type.kind] === "NumberKeyword" ||
473
+ ts.SyntaxKind[type.kind] === "StringKeyword" ||
474
+ ts.SyntaxKind[type.kind] === "UndefinedKeyword" ||
475
+ ("literal" in type && tsIsPrimitive(type.literal as TypeLiteralNode))
476
+ );
477
+ }
478
+
479
+ /** Create a literal type */
480
+ export function tsLiteral(value: unknown): ts.TypeNode {
481
+ if (typeof value === "string") {
482
+ // workaround for UTF-8: https://github.com/microsoft/TypeScript/issues/36174
483
+ return ts.factory.createIdentifier(JSON.stringify(value)) as unknown as ts.TypeNode;
484
+ }
485
+ if (typeof value === "number") {
486
+ const literal =
487
+ value < 0
488
+ ? ts.factory.createPrefixUnaryExpression(
489
+ ts.SyntaxKind.MinusToken,
490
+ ts.factory.createNumericLiteral(Math.abs(value)),
491
+ )
492
+ : ts.factory.createNumericLiteral(value);
493
+ return ts.factory.createLiteralTypeNode(literal);
494
+ }
495
+ if (typeof value === "boolean") {
496
+ return value === true ? TRUE : FALSE;
497
+ }
498
+ if (value === null) {
499
+ return NULL;
500
+ }
501
+ if (Array.isArray(value)) {
502
+ if (value.length === 0) {
503
+ return ts.factory.createArrayTypeNode(NEVER);
504
+ }
505
+ return ts.factory.createTupleTypeNode(value.map((v: unknown) => tsLiteral(v)));
506
+ }
507
+ if (typeof value === "object") {
508
+ const keys: ts.TypeElement[] = [];
509
+ for (const [k, v] of Object.entries(value)) {
510
+ keys.push(
511
+ ts.factory.createPropertySignature(
512
+ /* modifiers */ undefined,
513
+ /* name */ tsPropertyIndex(k),
514
+ /* questionToken */ undefined,
515
+ /* type */ tsLiteral(v),
516
+ ),
517
+ );
518
+ }
519
+ return keys.length ? ts.factory.createTypeLiteralNode(keys) : tsRecord(STRING, NEVER);
520
+ }
521
+ return UNKNOWN;
522
+ }
523
+
524
+ /** Modifiers (readonly) */
525
+ export function tsModifiers(modifiers: { readonly?: boolean; export?: boolean }): ts.Modifier[] {
526
+ const typeMods: ts.Modifier[] = [];
527
+ if (modifiers.export) {
528
+ typeMods.push(ts.factory.createModifier(ts.SyntaxKind.ExportKeyword));
529
+ }
530
+ if (modifiers.readonly) {
531
+ typeMods.push(ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword));
532
+ }
533
+ return typeMods;
534
+ }
535
+
536
+ /** Create a T | null union */
537
+ export function tsNullable(types: ts.TypeNode[]): ts.TypeNode {
538
+ return ts.factory.createUnionTypeNode([...types, NULL]);
539
+ }
540
+
541
+ /** Create a TS Omit<X, Y> type */
542
+ export function tsOmit(type: ts.TypeNode, keys: string[]): ts.TypeNode {
543
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Omit"), [
544
+ type,
545
+ ts.factory.createUnionTypeNode(keys.map((k) => tsLiteral(k))),
546
+ ]);
547
+ }
548
+
549
+ /** Create a TS Record<X, Y> type */
550
+ export function tsRecord(key: ts.TypeNode, value: ts.TypeNode) {
551
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Record"), [key, value]);
552
+ }
553
+
554
+ /** Create a valid property index */
555
+ export function tsPropertyIndex(index: string | number) {
556
+ if (
557
+ (typeof index === "number" && !(index < 0)) ||
558
+ (typeof index === "string" && String(Number(index)) === index && index[0] !== "-")
559
+ ) {
560
+ return ts.factory.createNumericLiteral(index);
561
+ }
562
+ return typeof index === "string" && JS_PROPERTY_INDEX_RE.test(index)
563
+ ? ts.factory.createIdentifier(index)
564
+ : ts.factory.createStringLiteral(String(index));
565
+ }
566
+
567
+ /** Create a union type */
568
+ export function tsUnion(types: ts.TypeNode[]): ts.TypeNode {
569
+ if (types.length === 0) {
570
+ return NEVER;
571
+ }
572
+ if (types.length === 1) {
573
+ return types[0];
574
+ }
575
+ return ts.factory.createUnionTypeNode(tsDedupe(types));
576
+ }
577
+
578
+ /** Create a WithRequired<X, Y> type */
579
+ export function tsWithRequired(
580
+ type: ts.TypeNode,
581
+ keys: string[],
582
+ injectFooter: ts.Node[], // needed to inject type helper if used
583
+ ): ts.TypeNode {
584
+ if (keys.length === 0) {
585
+ return type;
586
+ }
587
+
588
+ // inject helper, if needed
589
+ if (!injectFooter.some((node) => ts.isTypeAliasDeclaration(node) && node?.name?.escapedText === "WithRequired")) {
590
+ const helper = stringToAST("type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };")[0] as any;
591
+ injectFooter.push(helper);
592
+ }
593
+
594
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("WithRequired"), [
595
+ type,
596
+ tsUnion(keys.map((k) => tsLiteral(k))),
597
+ ]);
598
+ }
599
+
600
+ /**
601
+ * Enhanced ReadonlyArray.
602
+ * eg: type Foo = ReadonlyArray<T>; type Bar = ReadonlyArray<T[]>
603
+ * Foo and Bar are both of type `readonly T[]`
604
+ */
605
+ export function tsReadonlyArray(type: ts.TypeNode, injectFooter?: ts.Node[]): ts.TypeNode {
606
+ if (
607
+ injectFooter &&
608
+ !injectFooter.some((node) => ts.isTypeAliasDeclaration(node) && node?.name?.escapedText === "ReadonlyArray")
609
+ ) {
610
+ const helper = stringToAST(
611
+ "type ReadonlyArray<T> = [Exclude<T, undefined>] extends [unknown[]] ? Readonly<Exclude<T, undefined>> : Readonly<Exclude<T, undefined>[]>;",
612
+ )[0] as any;
613
+ injectFooter.push(helper);
614
+ }
615
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("ReadonlyArray"), [type]);
616
+ }