@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/transformer",
3
- "version": "8.0.0-alpha.6",
3
+ "version": "8.0.0-alpha.7",
4
4
  "type": "module",
5
5
  "description": "Functionality for AST transformations, with transformer registration, and general utils",
6
6
  "keywords": [
@@ -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: { alias?: ts.Symbol, node?: ts.Node }): AnyType | undefined;
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
- return { key: 'managed', name, importName, tsTypeArguments };
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 decorator = DeclarationUtil.getPrimaryDeclarationNode(member);
325
- if (DeclarationUtil.isPublic(decorator)) { // If public
326
- const memberType = resolver.getType(decorator);
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(decorator) || ts.isPropertyDeclaration(decorator)) && !!decorator.questionToken) {
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 { key: 'shape', name, importName, tsFieldTypes, tsTypeArguments, fieldTypes: {} };
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: {
@@ -1,14 +1,16 @@
1
- import type ts from 'typescript';
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
- return transformCast(this.#tsChecker.getTypeArguments(transformCast(ref)));
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, alias?: ts.Symbol, depth = 0): AnyType => {
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
- const { build, finalize } = TypeBuilder[category];
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, { alias, node: node && 'kind' in node ? node : undefined });
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
- result.typeArguments = result.tsTypeArguments!.map((elType) => resolve(elType, type.aliasSymbol, depth + 1));
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, undefined, depth + 1);
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((elType) => resolve(elType, type.aliasSymbol, depth + 1));
172
+ result.subTypes = result.tsSubTypes!.map((item) => resolve(item, { alias: type.aliasSymbol, depth: depth + 1 }));
153
173
  delete result.tsSubTypes;
154
174
  }
155
- if (finalize) {
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
 
@@ -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?: ts.Type[];
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?: ts.Type[];
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?: ts.Type[];
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): 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
+ };