@xlr-lib/xlr-utils 0.1.1-next.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.
@@ -0,0 +1,410 @@
1
+ import ts from "typescript";
2
+ import type {
3
+ NamedType,
4
+ NodeType,
5
+ ObjectProperty,
6
+ ObjectType,
7
+ OrType,
8
+ RefNode,
9
+ } from "@xlr-lib/xlr";
10
+ import { computeExtends, resolveConditional } from "./validation-helpers";
11
+ import { isGenericNamedType, isGenericNodeType } from "./type-checks";
12
+
13
+ /**
14
+ * Returns the required type or the optionally required type
15
+ */
16
+ export function tsStripOptionalType(node: ts.TypeNode): ts.TypeNode {
17
+ return ts.isOptionalTypeNode(node) ? node.type : node;
18
+ }
19
+
20
+ /**
21
+ * Returns if the top level declaration is exported
22
+ */
23
+ export function isExportedDeclaration(node: ts.Statement): boolean {
24
+ const modifiers = ts.canHaveModifiers(node)
25
+ ? ts.getModifiers(node)
26
+ : undefined;
27
+
28
+ if (modifiers) {
29
+ return modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
30
+ }
31
+ return false;
32
+ }
33
+
34
+ /**
35
+ * Returns if the node is exported from the source file
36
+ */
37
+ export function isNodeExported(node: ts.Node): boolean {
38
+ return (
39
+ (ts.getCombinedModifierFlags(node as ts.Declaration) &
40
+ ts.ModifierFlags.Export) !==
41
+ 0 ||
42
+ (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Returns the actual type and will following import chains if needed
48
+ */
49
+ export function getReferencedType(
50
+ node: ts.TypeReferenceNode,
51
+ typeChecker: ts.TypeChecker,
52
+ ) {
53
+ let symbol = typeChecker.getSymbolAtLocation(node.typeName);
54
+
55
+ if (
56
+ symbol &&
57
+ (symbol.flags & ts.SymbolFlags.Alias) === ts.SymbolFlags.Alias
58
+ ) {
59
+ // follow alias if it is a symbol
60
+ symbol = typeChecker.getAliasedSymbol(symbol);
61
+ }
62
+
63
+ const varDecl = symbol?.declarations?.[0];
64
+ if (
65
+ varDecl &&
66
+ (ts.isInterfaceDeclaration(varDecl) || ts.isTypeAliasDeclaration(varDecl))
67
+ ) {
68
+ return { declaration: varDecl, exported: isNodeExported(varDecl) };
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Returns list of string literals from potential union of strings
74
+ */
75
+ export function getStringLiteralsFromUnion(node: ts.Node): Set<string> {
76
+ if (ts.isUnionTypeNode(node)) {
77
+ return new Set(
78
+ node.types.map((type) => {
79
+ if (ts.isLiteralTypeNode(type) && ts.isStringLiteral(type.literal)) {
80
+ return type.literal.text;
81
+ }
82
+
83
+ return "";
84
+ }),
85
+ );
86
+ }
87
+
88
+ if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) {
89
+ return new Set([node.literal.text]);
90
+ }
91
+
92
+ return new Set();
93
+ }
94
+
95
+ /**
96
+ * Converts a format string into a regex that can be used to validate a given string matches the template
97
+ */
98
+ export function buildTemplateRegex(
99
+ node: ts.TemplateLiteralTypeNode,
100
+ typeChecker: ts.TypeChecker,
101
+ ): string {
102
+ let regex = node.head.text;
103
+ node.templateSpans.forEach((span) => {
104
+ // process template tag
105
+ let type = span.type.kind;
106
+ if (ts.isTypeReferenceNode(span.type)) {
107
+ let symbol = typeChecker.getSymbolAtLocation(
108
+ span.type.typeName,
109
+ ) as ts.Symbol;
110
+
111
+ if (
112
+ symbol &&
113
+ (symbol.flags & ts.SymbolFlags.Alias) === ts.SymbolFlags.Alias
114
+ ) {
115
+ // follow alias if it is a symbol
116
+ symbol = typeChecker.getAliasedSymbol(symbol);
117
+ }
118
+
119
+ type = (symbol?.declarations?.[0] as ts.TypeAliasDeclaration).type.kind;
120
+ }
121
+
122
+ if (type === ts.SyntaxKind.StringKeyword) {
123
+ regex += ".*";
124
+ } else if (type === ts.SyntaxKind.NumberKeyword) {
125
+ regex += "[0-9]*";
126
+ } else if (type === ts.SyntaxKind.BooleanKeyword) {
127
+ regex += "true|false";
128
+ }
129
+
130
+ // add non-tag element
131
+ regex += span.literal.text;
132
+ });
133
+ return regex;
134
+ }
135
+
136
+ /**
137
+ * Walks generics to fill in values from a combination of the default, constraint, and passed in map values
138
+ * TODO convert this to use simpleTransformGenerator
139
+ */
140
+ export function fillInGenerics(
141
+ xlrNode: NodeType,
142
+ generics?: Map<string, NodeType>,
143
+ ): NodeType {
144
+ // Need to make sure not to set generics in passed in map to avoid using generics outside of tree
145
+ let localGenerics: Map<string, NodeType>;
146
+
147
+ if (generics) {
148
+ localGenerics = new Map(generics);
149
+ } else {
150
+ localGenerics = new Map();
151
+ if (isGenericNodeType(xlrNode)) {
152
+ xlrNode.genericTokens?.forEach((token) => {
153
+ const genericValue = (token.default ?? token.constraints) as NodeType;
154
+ localGenerics.set(
155
+ token.symbol,
156
+ fillInGenerics(genericValue, localGenerics),
157
+ );
158
+ });
159
+ }
160
+ }
161
+
162
+ if (xlrNode.type === "ref") {
163
+ if (localGenerics.has(xlrNode.ref)) {
164
+ return {
165
+ ...(localGenerics.get(xlrNode.ref) as NodeType),
166
+ ...(xlrNode.genericArguments
167
+ ? {
168
+ genericArguments: xlrNode.genericArguments.map((ga) =>
169
+ fillInGenerics(ga, localGenerics),
170
+ ),
171
+ }
172
+ : {}),
173
+ ...(xlrNode.title ? { title: xlrNode.title } : {}),
174
+ ...(xlrNode.name ? { name: xlrNode.name } : {}),
175
+ ...(xlrNode.description ? { description: xlrNode.description } : {}),
176
+ ...(xlrNode.comment ? { comment: xlrNode.comment } : {}),
177
+ };
178
+ }
179
+
180
+ return {
181
+ ...xlrNode,
182
+ ...(xlrNode.genericArguments
183
+ ? {
184
+ genericArguments: xlrNode.genericArguments.map((ga) =>
185
+ fillInGenerics(ga, localGenerics),
186
+ ),
187
+ }
188
+ : {}),
189
+ };
190
+ }
191
+
192
+ if (xlrNode.type === "object") {
193
+ const newProperties: { [name: string]: ObjectProperty } = {};
194
+ Object.getOwnPropertyNames(xlrNode.properties).forEach((propName) => {
195
+ const prop = xlrNode.properties[propName];
196
+ newProperties[propName] = {
197
+ required: prop.required,
198
+ node: fillInGenerics(prop.node, localGenerics),
199
+ };
200
+ });
201
+
202
+ return {
203
+ ...xlrNode,
204
+ properties: newProperties,
205
+ ...(isGenericNamedType(xlrNode)
206
+ ? {
207
+ genericTokens: xlrNode.genericTokens.map((token) => {
208
+ return {
209
+ ...token,
210
+ constraints: token.constraints
211
+ ? fillInGenerics(token.constraints, localGenerics)
212
+ : undefined,
213
+ default: token.default
214
+ ? fillInGenerics(token.default, localGenerics)
215
+ : undefined,
216
+ };
217
+ }),
218
+ }
219
+ : {}),
220
+ extends: xlrNode.extends
221
+ ? (fillInGenerics(xlrNode.extends, localGenerics) as RefNode)
222
+ : undefined,
223
+ additionalProperties: xlrNode.additionalProperties
224
+ ? fillInGenerics(xlrNode.additionalProperties, localGenerics)
225
+ : false,
226
+ };
227
+ }
228
+
229
+ if (xlrNode.type === "array") {
230
+ return {
231
+ ...xlrNode,
232
+ elementType: fillInGenerics(xlrNode.elementType, localGenerics),
233
+ };
234
+ } else if (xlrNode.type === "or" || xlrNode.type === "and") {
235
+ let pointer;
236
+ if (xlrNode.type === "or") {
237
+ pointer = xlrNode.or;
238
+ } else {
239
+ pointer = xlrNode.and;
240
+ }
241
+
242
+ return {
243
+ ...xlrNode,
244
+ [xlrNode.type]: pointer.map((prop) => {
245
+ return fillInGenerics(prop, localGenerics);
246
+ }),
247
+ };
248
+ } else if (xlrNode.type === "record") {
249
+ return {
250
+ ...xlrNode,
251
+ keyType: fillInGenerics(xlrNode.keyType, localGenerics),
252
+ valueType: fillInGenerics(xlrNode.valueType, localGenerics),
253
+ };
254
+ } else if (xlrNode.type === "conditional") {
255
+ const filledInConditional = {
256
+ ...xlrNode,
257
+ check: {
258
+ left: fillInGenerics(xlrNode.check.left, localGenerics),
259
+ right: fillInGenerics(xlrNode.check.right, localGenerics),
260
+ },
261
+ value: {
262
+ true: fillInGenerics(xlrNode.value.true, localGenerics),
263
+ false: fillInGenerics(xlrNode.value.false, localGenerics),
264
+ },
265
+ };
266
+
267
+ // Check to see if we have enough information to resolve this conditional
268
+ if (
269
+ filledInConditional.check.left.type !== "ref" &&
270
+ filledInConditional.check.right.type !== "ref"
271
+ ) {
272
+ return {
273
+ name: xlrNode.name,
274
+ title: xlrNode.title,
275
+ ...resolveConditional(filledInConditional),
276
+ } as NamedType;
277
+ }
278
+
279
+ return filledInConditional;
280
+ }
281
+
282
+ return xlrNode;
283
+ }
284
+
285
+ /** Applies the TS `Pick` or `Omit` type to an interface/union/intersection */
286
+ export function applyPickOrOmitToNodeType(
287
+ baseObject: NodeType,
288
+ operation: "Pick" | "Omit",
289
+ properties: Set<string>,
290
+ ): NodeType | undefined {
291
+ if (baseObject.type === "object") {
292
+ const newObject = { ...baseObject };
293
+ Object.keys(baseObject.properties).forEach((key) => {
294
+ if (
295
+ (operation === "Omit" && properties.has(key)) ||
296
+ (operation === "Pick" && !properties.has(key))
297
+ ) {
298
+ delete newObject.properties[key];
299
+ }
300
+ });
301
+
302
+ /**
303
+ * Filter out objects in cases:
304
+ * - A Pick operation and there are no properties left
305
+ * - An Omit operation and there are no properties left and no additional properties allowed
306
+ */
307
+ if (
308
+ Object.keys(newObject.properties).length === 0 &&
309
+ (operation !== "Omit" || newObject.additionalProperties === false)
310
+ ) {
311
+ return undefined;
312
+ }
313
+
314
+ return newObject;
315
+ }
316
+
317
+ let pointer;
318
+ if (baseObject.type === "and") {
319
+ pointer = baseObject.and;
320
+ } else if (baseObject.type === "or") {
321
+ pointer = baseObject.or;
322
+ } else {
323
+ throw new Error(
324
+ `Error: Can not apply ${operation} to type ${baseObject.type}`,
325
+ );
326
+ }
327
+
328
+ const pickedTypes = pointer
329
+ .map((type) => {
330
+ const node = applyPickOrOmitToNodeType(type, operation, properties);
331
+ if (node === undefined) {
332
+ return undefined;
333
+ }
334
+
335
+ return { ...node, additionalProperties: false } as ObjectType;
336
+ })
337
+ .filter((type) => type !== undefined) as NodeType[];
338
+
339
+ if (pickedTypes.length === 0) {
340
+ return undefined;
341
+ }
342
+
343
+ if (pickedTypes.length === 1) {
344
+ return pickedTypes[0];
345
+ }
346
+
347
+ if (baseObject.type === "and") {
348
+ return { ...baseObject, and: pickedTypes };
349
+ }
350
+
351
+ return { ...baseObject, or: pickedTypes };
352
+ }
353
+
354
+ /** Applies the TS `Partial` or `Required` type to an interface/union/intersection */
355
+ export function applyPartialOrRequiredToNodeType(
356
+ baseObject: NodeType,
357
+ modifier: boolean,
358
+ ): NodeType {
359
+ if (baseObject.type === "object") {
360
+ const newObject = { ...baseObject };
361
+ Object.keys(baseObject.properties).forEach((key) => {
362
+ newObject.properties[key].required = modifier;
363
+ });
364
+
365
+ return newObject;
366
+ }
367
+
368
+ if (baseObject.type === "and") {
369
+ const pickedTypes = baseObject.and.map((type) =>
370
+ applyPartialOrRequiredToNodeType(type, modifier),
371
+ );
372
+ return { ...baseObject, and: pickedTypes };
373
+ }
374
+
375
+ if (baseObject.type === "or") {
376
+ const pickedTypes = baseObject.or.map((type) =>
377
+ applyPartialOrRequiredToNodeType(type, modifier),
378
+ );
379
+ return { ...baseObject, or: pickedTypes };
380
+ }
381
+
382
+ throw new Error(
383
+ `Error: Can not apply ${modifier ? "Required" : "Partial"} to type ${
384
+ baseObject.type
385
+ }`,
386
+ );
387
+ }
388
+
389
+ /** Applies the TS `Exclude` type to a union */
390
+ export function applyExcludeToNodeType(
391
+ baseObject: OrType,
392
+ filters: NodeType | OrType,
393
+ ): NodeType {
394
+ const remainingMembers = baseObject.or.filter((type) => {
395
+ if (filters.type === "or") {
396
+ return !filters.or.some((filter) => computeExtends(type, filter));
397
+ }
398
+
399
+ return !computeExtends(type, filters);
400
+ });
401
+
402
+ if (remainingMembers.length === 1) {
403
+ return remainingMembers[0];
404
+ }
405
+
406
+ return {
407
+ ...baseObject,
408
+ or: remainingMembers,
409
+ };
410
+ }
@@ -0,0 +1,121 @@
1
+ import ts from "typescript";
2
+ import type {
3
+ NamedType,
4
+ NamedTypeWithGenerics,
5
+ NodeType,
6
+ NodeTypeWithGenerics,
7
+ PrimitiveTypes,
8
+ } from "@xlr-lib/xlr";
9
+
10
+ /**
11
+ * Returns if the Object Property is optional
12
+ */
13
+ export function isOptionalProperty(node: ts.PropertySignature): boolean {
14
+ return node.questionToken?.kind === ts.SyntaxKind.QuestionToken;
15
+ }
16
+
17
+ /**
18
+ * Returns if the node is an Interface or Type with Generics
19
+ */
20
+ export function isGenericInterfaceDeclaration(
21
+ node: ts.InterfaceDeclaration,
22
+ ): boolean {
23
+ const length = node.typeParameters?.length;
24
+ return length ? length > 0 : false;
25
+ }
26
+
27
+ /**
28
+ * Returns if the node is an Type Declaration with Generics
29
+ */
30
+ export function isGenericTypeDeclaration(
31
+ node: ts.TypeAliasDeclaration,
32
+ ): boolean {
33
+ const length = node.typeParameters?.length;
34
+ return length ? length > 0 : false;
35
+ }
36
+
37
+ /**
38
+ * Returns if the referenced type is a generic
39
+ */
40
+ export function isTypeReferenceGeneric(
41
+ node: ts.TypeReferenceNode,
42
+ typeChecker: ts.TypeChecker,
43
+ ): boolean {
44
+ const symbol = typeChecker.getSymbolAtLocation(node.typeName);
45
+ if (symbol && symbol.declarations) {
46
+ return symbol.declarations[0].kind === ts.SyntaxKind.TypeParameter;
47
+ }
48
+
49
+ return false;
50
+ }
51
+
52
+ export type TopLevelDeclaration =
53
+ | ts.InterfaceDeclaration
54
+ | ts.TypeAliasDeclaration;
55
+
56
+ /**
57
+ * Returns if the node is an interface or a type declaration
58
+ */
59
+ export function isTopLevelDeclaration(
60
+ node: ts.Node,
61
+ ): node is TopLevelDeclaration {
62
+ return (
63
+ node.kind === ts.SyntaxKind.InterfaceDeclaration ||
64
+ node.kind === ts.SyntaxKind.TypeAliasDeclaration
65
+ );
66
+ }
67
+
68
+ export type TopLevelNode = TopLevelDeclaration | ts.VariableStatement;
69
+
70
+ /**
71
+ * Returns if the node is an interface or a type declaration
72
+ */
73
+ export function isTopLevelNode(node: ts.Node): node is TopLevelNode {
74
+ return (
75
+ node.kind === ts.SyntaxKind.InterfaceDeclaration ||
76
+ node.kind === ts.SyntaxKind.TypeAliasDeclaration ||
77
+ node.kind === ts.SyntaxKind.VariableStatement
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Returns if the NodeType has generic tokens
83
+ */
84
+ export function isGenericNodeType<T extends NodeType = NodeType>(
85
+ nt: NodeType,
86
+ ): nt is NodeTypeWithGenerics<T> {
87
+ return (nt as NodeTypeWithGenerics).genericTokens?.length > 0;
88
+ }
89
+
90
+ /**
91
+ * Returns if the named type has generic tokens
92
+ */
93
+ export function isGenericNamedType<T extends NamedType = NamedType>(
94
+ nt: NodeType,
95
+ ): nt is NamedTypeWithGenerics<T> {
96
+ return (nt as NamedTypeWithGenerics).genericTokens?.length > 0;
97
+ }
98
+
99
+ /**
100
+ * Returns if the node is a `PrimitiveTypes`
101
+ */
102
+ export function isPrimitiveTypeNode(node: NodeType): node is PrimitiveTypes {
103
+ return (
104
+ node.type === "string" ||
105
+ node.type === "number" ||
106
+ node.type === "boolean" ||
107
+ node.type === "null" ||
108
+ node.type === "any" ||
109
+ node.type === "never" ||
110
+ node.type === "undefined" ||
111
+ node.type === "unknown" ||
112
+ node.type === "void"
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Type Guard for non-null values
118
+ */
119
+ export function isNonNullable<T>(a: T | null | undefined): a is NonNullable<T> {
120
+ return a !== null || a !== undefined;
121
+ }