@travetto/transformer 8.0.0-alpha.6 → 8.0.0-alpha.7
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/package.json +1 -1
- package/src/resolver/builder.ts +57 -16
- package/src/resolver/service.ts +33 -13
- package/src/resolver/types.ts +32 -5
package/package.json
CHANGED
package/src/resolver/builder.ts
CHANGED
|
@@ -9,12 +9,13 @@ import { DeclarationUtil } from '../util/declaration.ts';
|
|
|
9
9
|
import { LiteralUtil } from '../util/literal.ts';
|
|
10
10
|
import { transformCast, type TemplateLiteralPart } from '../types/shared.ts';
|
|
11
11
|
|
|
12
|
-
import type { Type, AnyType, CompositionType, TransformResolver, TemplateType, MappedType } from './types.ts';
|
|
12
|
+
import type { Type, AnyType, CompositionType, TransformResolver, TemplateType, MappedType, ShapeType, ResolverContext, ManagedType } from './types.ts';
|
|
13
13
|
import { CoerceUtil } from './coerce.ts';
|
|
14
14
|
|
|
15
15
|
const UNDEFINED = Symbol();
|
|
16
16
|
|
|
17
17
|
const MAPPED_TYPE_SET = new Set(['Omit', 'Pick', 'Required', 'Partial']);
|
|
18
|
+
const allowsVirtualTemplate = (type: ts.Type): boolean => DocUtil.readDocTag(type, 'virtual')?.[0]?.trim() === 'true';
|
|
18
19
|
const isMappedType = (type: string | undefined): type is MappedType['operation'] => MAPPED_TYPE_SET.has(type!);
|
|
19
20
|
const getMappedFields = (type: ts.Type): string[] | undefined => {
|
|
20
21
|
if (type.isStringLiteral()) {
|
|
@@ -52,9 +53,7 @@ const GLOBAL_SIMPLE: Record<string, Function> = {
|
|
|
52
53
|
PromiseConstructor: Promise.constructor
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
type Category =
|
|
56
|
-
'tuple' | 'shape' | 'literal' | 'template' | 'managed' |
|
|
57
|
-
'composition' | 'foreign' | 'concrete' | 'unknown' | 'mapped';
|
|
56
|
+
type Category = Exclude<AnyType['key'], 'pointer'> | 'concrete';
|
|
58
57
|
|
|
59
58
|
/**
|
|
60
59
|
* Type categorizer, input for builder
|
|
@@ -137,7 +136,7 @@ export function TypeCategorize(resolver: TransformResolver, type: ts.Type): { ca
|
|
|
137
136
|
*/
|
|
138
137
|
export const TypeBuilder: {
|
|
139
138
|
[K in Category]: {
|
|
140
|
-
build(resolver: TransformResolver, type: ts.Type, context:
|
|
139
|
+
build(resolver: TransformResolver, type: ts.Type, context: ResolverContext): AnyType | undefined;
|
|
141
140
|
finalize?(type: Type<K>): AnyType;
|
|
142
141
|
}
|
|
143
142
|
} = {
|
|
@@ -223,12 +222,31 @@ export const TypeBuilder: {
|
|
|
223
222
|
}
|
|
224
223
|
},
|
|
225
224
|
managed: {
|
|
226
|
-
build: (resolver, type) => {
|
|
225
|
+
build: (resolver, type, context) => {
|
|
227
226
|
const name = CoreUtil.getSymbol(type)?.getName();
|
|
228
227
|
const importName = resolver.getTypeImportName(type)!;
|
|
229
228
|
const tsTypeArguments = resolver.getAllTypeArguments(type);
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
const docs = DocUtil.readDocTag(type, 'see');
|
|
230
|
+
const innerTypeProperty = docs.find(doc => doc.includes('#target'))?.split(' ')?.[0];
|
|
231
|
+
|
|
232
|
+
const managedType: ManagedType = { key: 'managed', name, importName, tsTypeArguments, innerTypeProperty };
|
|
233
|
+
|
|
234
|
+
// Detect managed, but shapeable
|
|
235
|
+
if (tsTypeArguments.length > 0) {
|
|
236
|
+
const hasEmptyConstructor = type.getConstructSignatures().some(signature => signature.parameters.length === 0)
|
|
237
|
+
|| type.getConstructSignatures().length === 0; // If no constructor, we can assume it's shapeable
|
|
238
|
+
const hasMethods = type.getProperties().some(property => property.getFlags() & ts.SymbolFlags.Method);
|
|
239
|
+
if (hasEmptyConstructor && !hasMethods && allowsVirtualTemplate(type)) {
|
|
240
|
+
return TypeBuilder.shape.build(resolver, type, {
|
|
241
|
+
...context,
|
|
242
|
+
extendsFrom: managedType,
|
|
243
|
+
alias: type.aliasSymbol
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return managedType;
|
|
249
|
+
},
|
|
232
250
|
},
|
|
233
251
|
composition: {
|
|
234
252
|
build: (resolver, uType: ts.UnionOrIntersectionType) => {
|
|
@@ -311,31 +329,54 @@ export const TypeBuilder: {
|
|
|
311
329
|
},
|
|
312
330
|
shape: {
|
|
313
331
|
build: (resolver, type, context) => {
|
|
314
|
-
const tsFieldTypes: Record<string, ts.Type> = {};
|
|
315
|
-
const name = CoreUtil.getSymbol(context?.alias ?? type)?.getName();
|
|
316
332
|
const importName = resolver.getTypeImportName(type) ?? '<unknown>';
|
|
317
|
-
const tsTypeArguments = resolver.getAllTypeArguments(type);
|
|
318
333
|
const properties = resolver.getPropertiesOfType(type);
|
|
319
334
|
if (properties.length === 0) {
|
|
320
335
|
return { key: 'literal', name: 'Object', ctor: Object, importName };
|
|
321
336
|
}
|
|
322
337
|
|
|
338
|
+
const tsTypeArguments = resolver.getAllTypeArguments(type);
|
|
339
|
+
const name = CoreUtil.getSymbol(context?.alias ?? type)?.getName();
|
|
340
|
+
const tsFieldTypes: Record<string, ts.Type> = {};
|
|
341
|
+
|
|
323
342
|
for (const member of properties) {
|
|
324
|
-
const
|
|
325
|
-
if (DeclarationUtil.isPublic(
|
|
326
|
-
const memberType = resolver.getType(
|
|
343
|
+
const declaration = DeclarationUtil.getPrimaryDeclarationNode(member);
|
|
344
|
+
if (DeclarationUtil.isPublic(declaration)) { // IF public
|
|
345
|
+
const memberType = resolver.getType(declaration);
|
|
327
346
|
if (
|
|
328
347
|
!member.getName().includes('@') && // if not a symbol
|
|
329
348
|
!memberType.getCallSignatures().length // if not a function
|
|
330
349
|
) {
|
|
331
|
-
if ((ts.isPropertySignature(
|
|
350
|
+
if ((ts.isPropertySignature(declaration) || ts.isPropertyDeclaration(declaration)) && !!declaration.questionToken) {
|
|
332
351
|
Object.defineProperty(memberType, UNDEFINED, { value: true });
|
|
333
352
|
}
|
|
334
353
|
tsFieldTypes[member.getName()] = memberType;
|
|
335
354
|
}
|
|
336
355
|
}
|
|
337
356
|
}
|
|
338
|
-
return {
|
|
357
|
+
return {
|
|
358
|
+
key: 'shape', name, importName, tsFieldTypes, tsTypeArguments,
|
|
359
|
+
fieldTypes: {}, extendsFrom: context.extendsFrom, canTemplate: allowsVirtualTemplate(type)
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
finalize: (type: ShapeType) => {
|
|
363
|
+
if ((type.typeArguments?.length ?? 0) > 0 && type.canTemplate !== false) {
|
|
364
|
+
let suffix = '';
|
|
365
|
+
const typeTemplateMap = Object.fromEntries(type.typeArguments!.map(node => [node.templateTypeName, node]));
|
|
366
|
+
|
|
367
|
+
for (const field of Object.values(type.fieldTypes)) {
|
|
368
|
+
if (field.key === 'literal' || field.key === 'shape' || field.key === 'managed') {
|
|
369
|
+
if (field.typeArguments?.every(item => item.templateTypeName && item.templateTypeName in typeTemplateMap)) {
|
|
370
|
+
field.typeArguments = field.typeArguments.map(item => typeTemplateMap[item.templateTypeName!]);
|
|
371
|
+
suffix = `${suffix}${field.name}${field.typeArguments.map(arg => arg.name).join('$')}`;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (suffix) {
|
|
376
|
+
type.name = `${type.name}$${suffix}`;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return type;
|
|
339
380
|
}
|
|
340
381
|
},
|
|
341
382
|
concrete: {
|
package/src/resolver/service.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ts from 'typescript';
|
|
2
2
|
|
|
3
3
|
import { path, type ManifestIndex, ManifestModuleUtil, type IndexedFile } from '@travetto/manifest';
|
|
4
4
|
|
|
5
|
-
import type { AnyType, TransformResolver } from './types.ts';
|
|
5
|
+
import type { AnyType, ResolverContext, TransformResolver } from './types.ts';
|
|
6
6
|
import { TypeCategorize, TypeBuilder } from './builder.ts';
|
|
7
7
|
import { VisitCache } from './cache.ts';
|
|
8
8
|
import { DocUtil } from '../util/doc.ts';
|
|
9
9
|
import { DeclarationUtil } from '../util/declaration.ts';
|
|
10
10
|
import { transformCast } from '../types/shared.ts';
|
|
11
11
|
|
|
12
|
+
const isFinalizeType = (key: string): key is keyof typeof TypeBuilder => key in TypeBuilder;
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Implementation of TransformResolver
|
|
14
16
|
*/
|
|
@@ -82,8 +84,16 @@ export class SimpleResolver implements TransformResolver {
|
|
|
82
84
|
/**
|
|
83
85
|
* Fetch all type arguments for a give type
|
|
84
86
|
*/
|
|
85
|
-
getAllTypeArguments(ref: ts.Type): ts.Type[] {
|
|
86
|
-
|
|
87
|
+
getAllTypeArguments(ref: ts.Type): [templateName: string, type: ts.Type][] {
|
|
88
|
+
const types = this.#tsChecker.getTypeArguments(transformCast(ref));
|
|
89
|
+
let names: string[] | undefined;
|
|
90
|
+
if (ref.symbol.declarations?.[0]) {
|
|
91
|
+
const first = ref.symbol.declarations[0];
|
|
92
|
+
if (ts.isClassLike(first) || ts.isInterfaceDeclaration(first) || ts.isTypeAliasDeclaration(first)) {
|
|
93
|
+
names = first.typeParameters?.map(tp => tp.name.getText()) ?? [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return transformCast(types.map((type, i) => [names?.[i] ?? '$', type]));
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
/**
|
|
@@ -113,18 +123,21 @@ export class SimpleResolver implements TransformResolver {
|
|
|
113
123
|
/**
|
|
114
124
|
* Resolve an `AnyType` from a `ts.Type` or a `ts.Node`
|
|
115
125
|
*/
|
|
116
|
-
resolveType(node: ts.Type | ts.Node, importName: string): AnyType {
|
|
126
|
+
resolveType(node: ts.Type | ts.Node | ts.TypeReference, importName: string): AnyType {
|
|
117
127
|
const visited = new VisitCache();
|
|
118
|
-
const resolve = (resType: ts.Type,
|
|
128
|
+
const resolve = (resType: ts.Type, context?: Omit<ResolverContext, 'node' | 'importName'>): AnyType => {
|
|
129
|
+
const { depth = 0 } = context ?? {};
|
|
119
130
|
|
|
120
131
|
if (depth > 20) { // Max depth is 20
|
|
121
132
|
throw new Error(`Object structure too nested: ${'getText' in node ? node.getText() : ''}`);
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
const { category, type } = TypeCategorize(this, resType);
|
|
125
|
-
|
|
136
|
+
// TODO: Figure out how to get this legitimately
|
|
137
|
+
const typeArguments: ts.Type[] =
|
|
138
|
+
'resolvedTypeArguments' in resType && resType.resolvedTypeArguments ? transformCast(resType.resolvedTypeArguments) : [];
|
|
126
139
|
|
|
127
|
-
let result = build(this, type, {
|
|
140
|
+
let result = TypeBuilder[category].build(this, type, { ...context, node: (node && 'kind' in node) ? node : undefined, importName });
|
|
128
141
|
|
|
129
142
|
// Convert via cache if needed
|
|
130
143
|
result = visited.getOrSet(type, result);
|
|
@@ -132,28 +145,35 @@ export class SimpleResolver implements TransformResolver {
|
|
|
132
145
|
// Recurse
|
|
133
146
|
if (result) {
|
|
134
147
|
result.original = resType;
|
|
148
|
+
result.templateTypeName = context?.templateTypeName;
|
|
149
|
+
|
|
135
150
|
try {
|
|
136
151
|
result.comment = DocUtil.describeDocs(type).description;
|
|
137
152
|
} catch { }
|
|
138
153
|
|
|
139
154
|
if ('tsTypeArguments' in result) {
|
|
140
|
-
|
|
155
|
+
const tsTypeArguments = result.tsTypeArguments!;
|
|
156
|
+
if (typeArguments.length) {
|
|
157
|
+
result.typeArguments = typeArguments.map((item, i) => resolve(item, { alias: type.aliasSymbol, templateTypeName: tsTypeArguments[i][0], depth: depth + 1 }));
|
|
158
|
+
} else {
|
|
159
|
+
result.typeArguments = tsTypeArguments.map((item) => resolve(item[1], { alias: type.aliasSymbol, templateTypeName: item[0], depth: depth + 1 }));
|
|
160
|
+
}
|
|
141
161
|
delete result.tsTypeArguments;
|
|
142
162
|
}
|
|
143
163
|
if ('tsFieldTypes' in result) {
|
|
144
164
|
const fields: Record<string, AnyType> = {};
|
|
145
165
|
for (const [name, fieldType] of Object.entries(result.tsFieldTypes ?? [])) {
|
|
146
|
-
fields[name] = resolve(fieldType,
|
|
166
|
+
fields[name] = resolve(fieldType, { depth: depth + 1 });
|
|
147
167
|
}
|
|
148
168
|
result.fieldTypes = fields;
|
|
149
169
|
delete result.tsFieldTypes;
|
|
150
170
|
}
|
|
151
171
|
if ('tsSubTypes' in result) {
|
|
152
|
-
result.subTypes = result.tsSubTypes!.map((
|
|
172
|
+
result.subTypes = result.tsSubTypes!.map((item) => resolve(item, { alias: type.aliasSymbol, depth: depth + 1 }));
|
|
153
173
|
delete result.tsSubTypes;
|
|
154
174
|
}
|
|
155
|
-
if (
|
|
156
|
-
result = finalize(transformCast(result));
|
|
175
|
+
if (isFinalizeType(result.key)) {
|
|
176
|
+
result = TypeBuilder[result.key].finalize?.(transformCast(result)) ?? result;
|
|
157
177
|
}
|
|
158
178
|
}
|
|
159
179
|
|
package/src/resolver/types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type ts from 'typescript';
|
|
2
2
|
import type { TemplateLiteral } from '../types/shared.ts';
|
|
3
3
|
|
|
4
|
+
type TemplateArgument = [templateName: string, type: ts.Type];
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Base type for a simplistic type structure
|
|
6
8
|
*/
|
|
@@ -29,6 +31,10 @@ export interface Type<K extends string> {
|
|
|
29
31
|
* Original type
|
|
30
32
|
*/
|
|
31
33
|
original?: ts.Type;
|
|
34
|
+
/**
|
|
35
|
+
* Template name if this type is derived from a template
|
|
36
|
+
*/
|
|
37
|
+
templateTypeName?: string;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
/**
|
|
@@ -46,7 +52,11 @@ export interface ManagedType extends Type<'managed'> {
|
|
|
46
52
|
/**
|
|
47
53
|
* Type Info
|
|
48
54
|
*/
|
|
49
|
-
tsTypeArguments?:
|
|
55
|
+
tsTypeArguments?: TemplateArgument[];
|
|
56
|
+
/**
|
|
57
|
+
* Inner return property
|
|
58
|
+
*/
|
|
59
|
+
innerTypeProperty?: string;
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
/**
|
|
@@ -72,7 +82,15 @@ export interface ShapeType extends Type<'shape'> {
|
|
|
72
82
|
/**
|
|
73
83
|
* Type Arguments
|
|
74
84
|
*/
|
|
75
|
-
tsTypeArguments?:
|
|
85
|
+
tsTypeArguments?: TemplateArgument[];
|
|
86
|
+
/**
|
|
87
|
+
* Can we template this type
|
|
88
|
+
*/
|
|
89
|
+
canTemplate?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Extends from another type, used for shapes that are concrete types, but we want to preserve the shape information as well
|
|
92
|
+
*/
|
|
93
|
+
extendsFrom?: ManagedType;
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
/**
|
|
@@ -116,7 +134,7 @@ export interface LiteralType extends Type<'literal'> {
|
|
|
116
134
|
/**
|
|
117
135
|
* Type Info
|
|
118
136
|
*/
|
|
119
|
-
tsTypeArguments?:
|
|
137
|
+
tsTypeArguments?: TemplateArgument[];
|
|
120
138
|
}
|
|
121
139
|
|
|
122
140
|
/**
|
|
@@ -213,8 +231,17 @@ export interface TransformResolver {
|
|
|
213
231
|
isKnownFile(file: string): boolean;
|
|
214
232
|
getFileImportName(file: string, removeExt?: boolean): string;
|
|
215
233
|
getTypeImportName(type: ts.Type, removeExt?: boolean): string | undefined;
|
|
216
|
-
getAllTypeArguments(type: ts.Type):
|
|
234
|
+
getAllTypeArguments(type: ts.Type): TemplateArgument[];
|
|
217
235
|
getPropertiesOfType(type: ts.Type): ts.Symbol[];
|
|
218
236
|
getTypeAsString(type: ts.Type): string | undefined;
|
|
219
237
|
getType(node: ts.Node): ts.Type;
|
|
220
|
-
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export type ResolverContext = {
|
|
241
|
+
alias?: ts.Symbol;
|
|
242
|
+
node?: ts.Node;
|
|
243
|
+
importName: string;
|
|
244
|
+
depth?: number;
|
|
245
|
+
templateTypeName?: string;
|
|
246
|
+
extendsFrom?: ManagedType;
|
|
247
|
+
};
|