@travetto/transformer 8.0.0-alpha.1 → 8.0.0-alpha.11

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.1",
3
+ "version": "8.0.0-alpha.11",
4
4
  "type": "module",
5
5
  "description": "Functionality for AST transformations, with transformer registration, and general utils",
6
6
  "keywords": [
@@ -25,9 +25,9 @@
25
25
  "directory": "module/transformer"
26
26
  },
27
27
  "dependencies": {
28
- "@travetto/manifest": "^8.0.0-alpha.1",
28
+ "@travetto/manifest": "^8.0.0-alpha.8",
29
29
  "tslib": "^2.8.1",
30
- "typescript": "^6.0.1-rc"
30
+ "typescript": "^6.0.3"
31
31
  },
32
32
  "travetto": {
33
33
  "displayName": "Transformation",
@@ -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
- result.comment = DocUtil.describeDocs(type).description;
151
+ result.description = 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
  */
@@ -16,7 +18,11 @@ export interface Type<K extends string> {
16
18
  /**
17
19
  * JS Doc comment
18
20
  */
19
- comment?: string;
21
+ description?: string;
22
+ /**
23
+ * JS Doc examples
24
+ */
25
+ examples?: string[];
20
26
  /**
21
27
  * Can be undefined
22
28
  */
@@ -29,6 +35,10 @@ export interface Type<K extends string> {
29
35
  * Original type
30
36
  */
31
37
  original?: ts.Type;
38
+ /**
39
+ * Template name if this type is derived from a template
40
+ */
41
+ templateTypeName?: string;
32
42
  }
33
43
 
34
44
  /**
@@ -46,7 +56,11 @@ export interface ManagedType extends Type<'managed'> {
46
56
  /**
47
57
  * Type Info
48
58
  */
49
- tsTypeArguments?: ts.Type[];
59
+ tsTypeArguments?: TemplateArgument[];
60
+ /**
61
+ * Inner return property
62
+ */
63
+ innerTypeProperty?: string;
50
64
  }
51
65
 
52
66
  /**
@@ -72,7 +86,15 @@ export interface ShapeType extends Type<'shape'> {
72
86
  /**
73
87
  * Type Arguments
74
88
  */
75
- tsTypeArguments?: ts.Type[];
89
+ tsTypeArguments?: TemplateArgument[];
90
+ /**
91
+ * Can we template this type
92
+ */
93
+ canTemplate?: boolean;
94
+ /**
95
+ * Extends from another type, used for shapes that are concrete types, but we want to preserve the shape information as well
96
+ */
97
+ extendsFrom?: ManagedType;
76
98
  }
77
99
 
78
100
  /**
@@ -116,7 +138,7 @@ export interface LiteralType extends Type<'literal'> {
116
138
  /**
117
139
  * Type Info
118
140
  */
119
- tsTypeArguments?: ts.Type[];
141
+ tsTypeArguments?: TemplateArgument[];
120
142
  }
121
143
 
122
144
  /**
@@ -213,8 +235,17 @@ export interface TransformResolver {
213
235
  isKnownFile(file: string): boolean;
214
236
  getFileImportName(file: string, removeExt?: boolean): string;
215
237
  getTypeImportName(type: ts.Type, removeExt?: boolean): string | undefined;
216
- getAllTypeArguments(type: ts.Type): ts.Type[];
238
+ getAllTypeArguments(type: ts.Type): TemplateArgument[];
217
239
  getPropertiesOfType(type: ts.Type): ts.Symbol[];
218
240
  getTypeAsString(type: ts.Type): string | undefined;
219
241
  getType(node: ts.Node): ts.Type;
220
- }
242
+ }
243
+
244
+ export type ResolverContext = {
245
+ alias?: ts.Symbol;
246
+ node?: ts.Node;
247
+ importName: string;
248
+ depth?: number;
249
+ templateTypeName?: string;
250
+ extendsFrom?: ManagedType;
251
+ };
@@ -14,6 +14,7 @@ export interface ParamDocumentation {
14
14
  export interface DeclDocumentation {
15
15
  return?: string;
16
16
  description?: string;
17
+ examples?: string[];
17
18
  params?: ParamDocumentation[];
18
19
  }
19
20
 
package/src/util/doc.ts CHANGED
@@ -32,15 +32,17 @@ export class DocUtil {
32
32
  const out: DeclDocumentation = {
33
33
  description: undefined,
34
34
  return: undefined,
35
+ examples: undefined,
35
36
  params: []
36
37
  };
37
38
 
38
39
  if (toDescribe) {
39
- const tags = ts.getJSDocTags(toDescribe);
40
40
  while (!this.hasJSDoc(toDescribe) && CoreUtil.hasOriginal(toDescribe)) {
41
41
  toDescribe = transformCast<ts.Declaration>(toDescribe.original);
42
42
  }
43
43
 
44
+ const tags = ts.getJSDocTags(toDescribe);
45
+
44
46
  const docs = this.hasJSDoc(toDescribe) ? toDescribe.jsDoc : undefined;
45
47
 
46
48
  if (docs) {
@@ -59,6 +61,8 @@ export class DocUtil {
59
61
  name: tag.name && tag.name.getText(),
60
62
  description: this.getDocComment(tag, '')!
61
63
  });
64
+ } else if (tag.tagName.getText() === 'example') {
65
+ (out.examples ??= []).push(this.getDocComment(tag, '')!);
62
66
  }
63
67
  }
64
68
  }