@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.
- package/CHANGELOG.md +1162 -0
- package/CONTRIBUTING.md +149 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/bin/cli.js +297 -0
- package/changes.json +14 -0
- package/dist/index.cjs +152 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +517 -0
- package/dist/index.d.mts +515 -0
- package/dist/index.d.ts +517 -0
- package/dist/index.mjs +85 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/redoc.cjs +129 -0
- package/dist/lib/redoc.cjs.map +1 -0
- package/dist/lib/redoc.mjs +122 -0
- package/dist/lib/redoc.mjs.map +1 -0
- package/dist/lib/ts.cjs +470 -0
- package/dist/lib/ts.cjs.map +1 -0
- package/dist/lib/ts.mjs +431 -0
- package/dist/lib/ts.mjs.map +1 -0
- package/dist/lib/utils.cjs +295 -0
- package/dist/lib/utils.cjs.map +1 -0
- package/dist/lib/utils.mjs +278 -0
- package/dist/lib/utils.mjs.map +1 -0
- package/dist/transform/components-object.cjs +150 -0
- package/dist/transform/components-object.cjs.map +1 -0
- package/dist/transform/components-object.mjs +127 -0
- package/dist/transform/components-object.mjs.map +1 -0
- package/dist/transform/header-object.cjs +48 -0
- package/dist/transform/header-object.cjs.map +1 -0
- package/dist/transform/header-object.mjs +42 -0
- package/dist/transform/header-object.mjs.map +1 -0
- package/dist/transform/index.cjs +124 -0
- package/dist/transform/index.cjs.map +1 -0
- package/dist/transform/index.mjs +118 -0
- package/dist/transform/index.mjs.map +1 -0
- package/dist/transform/media-type-object.cjs +14 -0
- package/dist/transform/media-type-object.cjs.map +1 -0
- package/dist/transform/media-type-object.mjs +12 -0
- package/dist/transform/media-type-object.mjs.map +1 -0
- package/dist/transform/operation-object.cjs +105 -0
- package/dist/transform/operation-object.cjs.map +1 -0
- package/dist/transform/operation-object.mjs +96 -0
- package/dist/transform/operation-object.mjs.map +1 -0
- package/dist/transform/parameter-object.cjs +11 -0
- package/dist/transform/parameter-object.cjs.map +1 -0
- package/dist/transform/parameter-object.mjs +9 -0
- package/dist/transform/parameter-object.mjs.map +1 -0
- package/dist/transform/parameters-array.cjs +120 -0
- package/dist/transform/parameters-array.cjs.map +1 -0
- package/dist/transform/parameters-array.mjs +114 -0
- package/dist/transform/parameters-array.mjs.map +1 -0
- package/dist/transform/path-item-object.cjs +84 -0
- package/dist/transform/path-item-object.cjs.map +1 -0
- package/dist/transform/path-item-object.mjs +78 -0
- package/dist/transform/path-item-object.mjs.map +1 -0
- package/dist/transform/paths-enum.cjs +35 -0
- package/dist/transform/paths-enum.cjs.map +1 -0
- package/dist/transform/paths-enum.mjs +33 -0
- package/dist/transform/paths-enum.mjs.map +1 -0
- package/dist/transform/paths-object.cjs +134 -0
- package/dist/transform/paths-object.cjs.map +1 -0
- package/dist/transform/paths-object.mjs +128 -0
- package/dist/transform/paths-object.mjs.map +1 -0
- package/dist/transform/request-body-object.cjs +68 -0
- package/dist/transform/request-body-object.cjs.map +1 -0
- package/dist/transform/request-body-object.mjs +62 -0
- package/dist/transform/request-body-object.mjs.map +1 -0
- package/dist/transform/response-object.cjs +123 -0
- package/dist/transform/response-object.cjs.map +1 -0
- package/dist/transform/response-object.mjs +117 -0
- package/dist/transform/response-object.mjs.map +1 -0
- package/dist/transform/responses-object.cjs +36 -0
- package/dist/transform/responses-object.cjs.map +1 -0
- package/dist/transform/responses-object.mjs +30 -0
- package/dist/transform/responses-object.mjs.map +1 -0
- package/dist/transform/schema-object.cjs +502 -0
- package/dist/transform/schema-object.cjs.map +1 -0
- package/dist/transform/schema-object.mjs +493 -0
- package/dist/transform/schema-object.mjs.map +1 -0
- package/dist/transform/webhooks-object.cjs +37 -0
- package/dist/transform/webhooks-object.cjs.map +1 -0
- package/dist/transform/webhooks-object.mjs +31 -0
- package/dist/transform/webhooks-object.mjs.map +1 -0
- package/package.json +102 -0
- package/src/index.ts +108 -0
- package/src/lib/redoc.ts +164 -0
- package/src/lib/ts.ts +616 -0
- package/src/lib/utils.ts +394 -0
- package/src/transform/components-object.ts +161 -0
- package/src/transform/header-object.ts +45 -0
- package/src/transform/index.ts +116 -0
- package/src/transform/media-type-object.ts +18 -0
- package/src/transform/operation-object.ts +104 -0
- package/src/transform/parameter-object.ts +15 -0
- package/src/transform/parameters-array.ts +129 -0
- package/src/transform/path-item-object.ts +102 -0
- package/src/transform/paths-enum.ts +43 -0
- package/src/transform/paths-object.ts +134 -0
- package/src/transform/request-body-object.ts +59 -0
- package/src/transform/response-object.ts +112 -0
- package/src/transform/responses-object.ts +36 -0
- package/src/transform/schema-object.ts +737 -0
- package/src/transform/webhooks-object.ts +27 -0
- 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
|
+
}
|