@travetto/transformer 6.0.1 → 7.0.0-rc.1

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": "6.0.1",
3
+ "version": "7.0.0-rc.1",
4
4
  "description": "Functionality for AST transformations, with transformer registration, and general utils",
5
5
  "keywords": [
6
6
  "typescript",
@@ -24,7 +24,7 @@
24
24
  "directory": "module/transformer"
25
25
  },
26
26
  "dependencies": {
27
- "@travetto/manifest": "^6.0.1",
27
+ "@travetto/manifest": "^7.0.0-rc.0",
28
28
  "tslib": "^2.8.1",
29
29
  "typescript": "^5.9.3"
30
30
  },
package/src/importer.ts CHANGED
@@ -2,7 +2,7 @@ import ts from 'typescript';
2
2
 
3
3
  import { ManifestModuleUtil, PackageUtil, path } from '@travetto/manifest';
4
4
 
5
- import { AnyType, TransformResolver, ManagedType } from './resolver/types.ts';
5
+ import { AnyType, TransformResolver, ManagedType, MappedType } from './resolver/types.ts';
6
6
  import { ImportUtil } from './util/import.ts';
7
7
  import { CoreUtil } from './util/core.ts';
8
8
  import { Import } from './types/shared.ts';
@@ -246,12 +246,14 @@ export class ImportManager {
246
246
  /**
247
247
  * Get the identifier and import if needed
248
248
  */
249
- getOrImport(factory: ts.NodeFactory, type: ManagedType): ts.Identifier | ts.PropertyAccessExpression {
249
+ getOrImport(factory: ts.NodeFactory, type: ManagedType | MappedType): ts.Identifier | ts.PropertyAccessExpression {
250
+ const targetName = type.key === 'managed' ? type.name! : type.mappedClassName!;
251
+ // In same file already
250
252
  if (type.importName === this.#importName) {
251
- return factory.createIdentifier(type.name!);
253
+ return factory.createIdentifier(targetName);
252
254
  } else {
253
255
  const { ident } = this.#imports.get(type.importName) ?? this.importFile(type.importName);
254
- return factory.createPropertyAccessExpression(ident, type.name!);
256
+ return factory.createPropertyAccessExpression(ident, targetName);
255
257
  }
256
258
  }
257
259
  }
package/src/register.ts CHANGED
@@ -115,6 +115,15 @@ export function OnMethod(...target: string[]) {
115
115
  ): void => storeHandler(inst, d.value!, 'before', 'method', target);
116
116
  }
117
117
 
118
+ /**
119
+ * Listens for a `ts.ConstructorDeclaration`, on descent
120
+ */
121
+ export function OnConstructor(...target: string[]) {
122
+ return <S extends State = State, R extends ts.Node = ts.Node>(
123
+ inst: Transformer, __: unknown, d: TypedPropertyDescriptor<(state: S, node: ts.ConstructorDeclaration, dm?: DecoratorMeta) => R>
124
+ ): void => storeHandler(inst, d.value!, 'before', 'constructor', target);
125
+ }
126
+
118
127
  /**
119
128
  * Listens for a static `ts.MethodDeclaration`, on descent
120
129
  */
@@ -214,6 +223,15 @@ export function AfterMethod(...target: string[]) {
214
223
  ): void => storeHandler(inst, d.value!, 'after', 'method', target);
215
224
  }
216
225
 
226
+ /**
227
+ * Listens for a `ts.ConstructorDeclaration`, on ascent
228
+ */
229
+ export function AfterConstructor(...target: string[]) {
230
+ return <S extends State = State, R extends ts.Node = ts.Node>(
231
+ inst: Transformer, __: unknown, d: TypedPropertyDescriptor<(state: S, node: ts.ConstructorDeclaration, dm?: DecoratorMeta) => R>
232
+ ): void => storeHandler(inst, d.value!, 'after', 'constructor', target);
233
+ }
234
+
217
235
  /**
218
236
  * Listens for a static `ts.MethodDeclaration`, on ascent
219
237
  */
@@ -9,11 +9,21 @@ import { DeclarationUtil } from '../util/declaration.ts';
9
9
  import { LiteralUtil } from '../util/literal.ts';
10
10
  import { transformCast, TemplateLiteralPart } from '../types/shared.ts';
11
11
 
12
- import { Type, AnyType, CompositionType, TransformResolver, TemplateType } from './types.ts';
12
+ import { Type, AnyType, CompositionType, TransformResolver, TemplateType, MappedType } from './types.ts';
13
13
  import { CoerceUtil } from './coerce.ts';
14
14
 
15
15
  const UNDEFINED = Symbol();
16
16
 
17
+ const MAPPED_TYPE_SET = new Set(['Omit', 'Pick', 'Required', 'Partial']);
18
+ const isMappedType = (type: string | undefined): type is MappedType['operation'] => MAPPED_TYPE_SET.has(type!);
19
+ const getMappedFields = (type: ts.Type): string[] | undefined => {
20
+ if (type.isStringLiteral()) {
21
+ return [type.value];
22
+ } else if (type.isUnion() && type.types.every(t => t.isStringLiteral())) {
23
+ return type.types.map(t => t.value);
24
+ }
25
+ };
26
+
17
27
  /**
18
28
  * List of global types that can be parameterized
19
29
  */
@@ -39,7 +49,7 @@ const GLOBAL_SIMPLE: Record<string, Function> = {
39
49
 
40
50
  type Category =
41
51
  'tuple' | 'shape' | 'literal' | 'template' | 'managed' |
42
- 'composition' | 'foreign' | 'concrete' | 'unknown';
52
+ 'composition' | 'foreign' | 'concrete' | 'unknown' | 'mapped';
43
53
 
44
54
  /**
45
55
  * Type categorizer, input for builder
@@ -102,9 +112,9 @@ export function TypeCategorize(resolver: TransformResolver, type: ts.Type): { ca
102
112
  return { category: 'tuple', type };
103
113
  } else if (type.isLiteral()) {
104
114
  return { category: 'shape', type };
105
- } else if ((objectFlags & ts.ObjectFlags.Mapped)) { // Mapped types: Pick, Omit, Exclude, Retain
115
+ } else if (objectFlags & ts.ObjectFlags.Mapped) { // Mapped types
106
116
  if (type.getProperties().some(x => x.declarations || x.valueDeclaration)) {
107
- return { category: 'shape', type };
117
+ return { category: 'mapped', type };
108
118
  }
109
119
  }
110
120
  return { category: 'literal', type };
@@ -115,7 +125,7 @@ export function TypeCategorize(resolver: TransformResolver, type: ts.Type): { ca
115
125
  */
116
126
  export const TypeBuilder: {
117
127
  [K in Category]: {
118
- build(resolver: TransformResolver, type: ts.Type, alias?: ts.Symbol): AnyType | undefined;
128
+ build(resolver: TransformResolver, type: ts.Type, context: { alias?: ts.Symbol, node?: ts.Node }): AnyType | undefined;
119
129
  finalize?(type: Type<K>): AnyType;
120
130
  }
121
131
  } = {
@@ -249,10 +259,43 @@ export const TypeBuilder: {
249
259
  return type;
250
260
  }
251
261
  },
262
+ mapped: {
263
+ build: (resolver, type, context) => {
264
+ let mainType: ts.Type | undefined;
265
+ let fields: string[] | undefined;
266
+ let operation: string | undefined;
267
+ let name: string | undefined;
268
+
269
+ const decls = DeclarationUtil.getDeclarations(type).filter(x => ts.isTypeAliasDeclaration(x));
270
+ const ref = decls[0]?.type;
271
+
272
+ if (ref && ts.isTypeReferenceNode(ref) && ref.typeArguments && ref.typeArguments.length > 0) {
273
+ const [first, second] = ref.typeArguments;
274
+ mainType = resolver.getType(first);
275
+ operation = ref.typeName.getText();
276
+ name = resolver.getTypeAsString(type)!;
277
+ fields = !second ? [] : getMappedFields(resolver.getType(second));
278
+ } else if (type.aliasTypeArguments && type.aliasSymbol) {
279
+ mainType = type.aliasTypeArguments[0];
280
+ operation = type.aliasSymbol.escapedName.toString();
281
+ fields = (type.aliasTypeArguments.length > 1) ? getMappedFields(type.aliasTypeArguments[1]) : [];
282
+ name = `${resolver.getTypeAsString(mainType)!}_${operation}_${fields?.join('_')}`;
283
+ }
284
+
285
+ if (!isMappedType(operation) || fields === undefined || !mainType || !mainType.isClass()) {
286
+ return TypeBuilder.shape.build(resolver, type, context);
287
+ }
288
+
289
+ const importName = resolver.getTypeImportName(mainType) ?? '<unknown>';
290
+ const mappedClassName = resolver.getTypeAsString(mainType)!;
291
+
292
+ return { key: 'mapped', name, original: mainType, operation, importName, mappedClassName, fields };
293
+ }
294
+ },
252
295
  shape: {
253
- build: (resolver, type, alias?) => {
296
+ build: (resolver, type, context) => {
254
297
  const tsFieldTypes: Record<string, ts.Type> = {};
255
- const name = CoreUtil.getSymbol(alias ?? type)?.getName();
298
+ const name = CoreUtil.getSymbol(context?.alias ?? type)?.getName();
256
299
  const importName = resolver.getTypeImportName(type) ?? '<unknown>';
257
300
  const tsTypeArguments = resolver.getAllTypeArguments(type);
258
301
  const props = resolver.getPropertiesOfType(type);
@@ -59,7 +59,7 @@ export class SimpleResolver implements TransformResolver {
59
59
  * Resolve an import name (e.g. @module/path/file) for a type
60
60
  */
61
61
  getTypeImportName(type: ts.Type, removeExt?: boolean): string | undefined {
62
- const ogSource = DeclarationUtil.getPrimaryDeclarationNode(type)?.getSourceFile()?.fileName;
62
+ const ogSource = DeclarationUtil.getOptionalPrimaryDeclarationNode(type)?.getSourceFile()?.fileName;
63
63
  return ogSource ? this.getFileImportName(ogSource, removeExt) : undefined;
64
64
  }
65
65
 
@@ -106,7 +106,7 @@ export class SimpleResolver implements TransformResolver {
106
106
  * Get list of properties
107
107
  */
108
108
  getPropertiesOfType(type: ts.Type): ts.Symbol[] {
109
- return this.#tsChecker.getPropertiesOfType(type);
109
+ return this.#tsChecker.getPropertiesOfType(type).filter(x => x.getName() !== '__proto__' && x.getName() !== 'prototype');
110
110
  }
111
111
 
112
112
  /**
@@ -117,13 +117,13 @@ export class SimpleResolver implements TransformResolver {
117
117
  const resolve = (resType: ts.Type, alias?: ts.Symbol, depth = 0): AnyType => {
118
118
 
119
119
  if (depth > 20) { // Max depth is 20
120
- throw new Error('Object structure too nested');
120
+ throw new Error(`Object structure too nested: ${'getText' in node ? node.getText() : ''}`);
121
121
  }
122
122
 
123
123
  const { category, type } = TypeCategorize(this, resType);
124
124
  const { build, finalize } = TypeBuilder[category];
125
125
 
126
- let result = build(this, type, alias);
126
+ let result = build(this, type, { alias, node: node && 'kind' in node ? node : undefined });
127
127
 
128
128
  // Convert via cache if needed
129
129
  result = visited.getOrSet(type, result);
@@ -131,7 +131,9 @@ export class SimpleResolver implements TransformResolver {
131
131
  // Recurse
132
132
  if (result) {
133
133
  result.original = resType;
134
- result.comment = DocUtil.describeDocs(type).description;
134
+ try {
135
+ result.comment = DocUtil.describeDocs(type).description;
136
+ } catch { }
135
137
 
136
138
  if ('tsTypeArguments' in result) {
137
139
  result.typeArguments = result.tsTypeArguments!.map((elType) => resolve(elType, type.aliasSymbol, depth + 1));
@@ -61,23 +61,42 @@ export interface ShapeType extends Type<'shape'> {
61
61
  * Does not include methods, used for shapes not concrete types
62
62
  */
63
63
  fieldTypes: Record<string, AnyType>;
64
-
65
64
  /**
66
65
  * Type Info
67
66
  */
68
67
  tsFieldTypes?: Record<string, ts.Type>;
69
-
70
68
  /**
71
69
  * Type arguments
72
70
  */
73
71
  typeArguments?: AnyType[];
74
-
75
72
  /**
76
73
  * Type Arguments
77
74
  */
78
75
  tsTypeArguments?: ts.Type[];
79
76
  }
80
77
 
78
+ /**
79
+ * A type that is mapped
80
+ */
81
+ export interface MappedType extends Type<'mapped'> {
82
+ /**
83
+ * Location the type came from, for class references
84
+ */
85
+ importName: string;
86
+ /**
87
+ * Mapped class name
88
+ */
89
+ mappedClassName: string;
90
+ /**
91
+ * The type of mapping operation
92
+ */
93
+ operation: 'Omit' | 'Pick' | 'Partial' | 'Required';
94
+ /**
95
+ * The fields being provided for the mapping
96
+ */
97
+ fields: string[];
98
+ }
99
+
81
100
  /**
82
101
  * A literal type, with an optional real value
83
102
  */
@@ -168,7 +187,6 @@ export interface ForeignType extends Type<'foreign'> {
168
187
  * Identifier for type
169
188
  */
170
189
  name: string;
171
-
172
190
  /**
173
191
  * Primary source file
174
192
  */
@@ -181,7 +199,7 @@ export interface ForeignType extends Type<'foreign'> {
181
199
  export interface UnknownType extends Type<'unknown'> { }
182
200
 
183
201
  export type AnyType =
184
- TupleType | ShapeType | CompositionType | LiteralType |
202
+ TupleType | ShapeType | CompositionType | LiteralType | MappedType |
185
203
  ManagedType | PointerType | UnknownType | ForeignType | TemplateType;
186
204
 
187
205
  /**
package/src/state.ts CHANGED
@@ -2,7 +2,7 @@ import ts from 'typescript';
2
2
 
3
3
  import { path, ManifestIndex } from '@travetto/manifest';
4
4
 
5
- import { ManagedType, AnyType, ForeignType } from './resolver/types.ts';
5
+ import { ManagedType, AnyType, ForeignType, MappedType } from './resolver/types.ts';
6
6
  import { State, DecoratorMeta, Transformer, ModuleNameSymbol } from './types/visitor.ts';
7
7
  import { SimpleResolver } from './resolver/service.ts';
8
8
  import { ImportManager } from './importer.ts';
@@ -27,6 +27,8 @@ function isRedefinableDeclaration(x: ts.Node): x is ts.InterfaceDeclaration | ts
27
27
  return ts.isFunctionDeclaration(x) || ts.isClassDeclaration(x) || ts.isInterfaceDeclaration(x);
28
28
  }
29
29
 
30
+ const FOREIGN_TYPE_REGISTRY_FILE = '@travetto/runtime/src/function';
31
+
30
32
  /**
31
33
  * Transformer runtime state
32
34
  */
@@ -64,7 +66,7 @@ export class TransformerState implements State {
64
66
  /**
65
67
  * Get or import the node or external type
66
68
  */
67
- getOrImport(type: ManagedType): ts.Identifier | ts.PropertyAccessExpression {
69
+ getOrImport(type: ManagedType | MappedType): ts.Identifier | ts.PropertyAccessExpression {
68
70
  return this.#imports.getOrImport(this.factory, type);
69
71
  }
70
72
 
@@ -164,20 +166,20 @@ export class TransformerState implements State {
164
166
  */
165
167
  getDecoratorMeta(dec: ts.Decorator): DecoratorMeta | undefined {
166
168
  const ident = DecoratorUtil.getDecoratorIdent(dec);
167
- const decl = DeclarationUtil.getPrimaryDeclarationNode(
168
- this.#resolver.getType(ident)
169
- );
169
+ const type = this.#resolver.getType(ident);
170
+ const decl = DeclarationUtil.getOptionalPrimaryDeclarationNode(type);
170
171
  const src = decl?.getSourceFile().fileName;
171
172
  const mod = src ? this.#resolver.getFileImportName(src, true) : undefined;
172
173
  const file = this.#manifestIndex.getFromImport(mod ?? '')?.outputFile;
173
- const targets = DocUtil.readAugments(this.#resolver.getType(ident));
174
+ const targets = DocUtil.readAugments(type);
175
+ const example = DocUtil.readExample(type);
174
176
  const module = file ? mod : undefined;
175
177
  const name = ident ?
176
178
  ident.escapedText?.toString()! :
177
179
  undefined;
178
180
 
179
181
  if (ident && name) {
180
- return { dec, ident, file, module, targets, name };
182
+ return { dec, ident, file, module, targets, name, options: example };
181
183
  }
182
184
  }
183
185
 
@@ -402,9 +404,13 @@ export class TransformerState implements State {
402
404
  * Produce a foreign target type
403
405
  */
404
406
  getForeignTarget(ret: ForeignType): ts.Expression {
405
- return this.fromLiteral({
406
- Ⲑid: `${ret.source.split('node_modules/')[1]}+${ret.name}`
407
- });
407
+ const file = this.importFile(FOREIGN_TYPE_REGISTRY_FILE);
408
+ return this.factory.createCallExpression(this.createAccess(
409
+ file.ident,
410
+ this.factory.createIdentifier('foreignType'),
411
+ ), [], [
412
+ this.fromLiteral(`${ret.source.split('node_modules/')[1]}+${ret.name}`)
413
+ ]);
408
414
  }
409
415
 
410
416
  /**
@@ -10,6 +10,7 @@ export type DecoratorMeta = {
10
10
  file?: string;
11
11
  targets?: string[];
12
12
  name?: string;
13
+ options?: string[];
13
14
  };
14
15
 
15
16
  export type State = {
@@ -23,8 +24,9 @@ export type State = {
23
24
  export type TransformPhase = 'before' | 'after';
24
25
 
25
26
  export type TransformerType =
26
- 'class' | 'method' | 'property' | 'getter' | 'setter' | 'parameter' |
27
- 'static-method' | 'call' | 'function' | 'file' | 'type' | 'interface';
27
+ 'class' | 'method' | 'property' | 'getter' | 'setter' | 'parameter'
28
+ | 'static-method' | 'call' | 'function' | 'file' | 'type' | 'interface'
29
+ | 'constructor';
28
30
 
29
31
  export const ModuleNameSymbol = Symbol.for('@travetto/transformer:id');
30
32
 
@@ -1,6 +1,8 @@
1
1
  import ts from 'typescript';
2
2
  import { CoreUtil } from './core.ts';
3
3
 
4
+ const isNamed = (o: ts.Declaration): o is ts.Declaration & { name: ts.Node } => 'name' in o && !!o.name;
5
+
4
6
  /**
5
7
  * Declaration utils
6
8
  */
@@ -23,7 +25,8 @@ export class DeclarationUtil {
23
25
  */
24
26
  static isPublic(node: ts.Declaration): boolean {
25
27
  // eslint-disable-next-line no-bitwise
26
- return !(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.NonPublicAccessibilityModifier);
28
+ return !(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.NonPublicAccessibilityModifier) &&
29
+ (!isNamed(node) || !ts.isPrivateIdentifier(node.name));
27
30
  }
28
31
 
29
32
  /**
@@ -42,15 +45,19 @@ export class DeclarationUtil {
42
45
  /**
43
46
  * Find primary declaration out of a list of declarations
44
47
  */
45
- static getPrimaryDeclaration(decls: ts.Declaration[]): ts.Declaration {
46
- return decls?.[0];
48
+ static getPrimaryDeclarationNode(node: ts.Type | ts.Symbol): ts.Declaration {
49
+ const decls = this.getDeclarations(node);
50
+ if (!decls.length) {
51
+ throw new Error('No declarations found for type');
52
+ }
53
+ return decls[0];
47
54
  }
48
55
 
49
56
  /**
50
57
  * Find primary declaration out of a list of declarations
51
58
  */
52
- static getPrimaryDeclarationNode(node: ts.Type | ts.Symbol): ts.Declaration {
53
- return this.getPrimaryDeclaration(this.getDeclarations(node));
59
+ static getOptionalPrimaryDeclarationNode(node: ts.Type | ts.Symbol): ts.Declaration | undefined {
60
+ return this.getDeclarations(node)[0];
54
61
  }
55
62
 
56
63
  /**
@@ -88,4 +95,11 @@ export class DeclarationUtil {
88
95
  }
89
96
  return acc;
90
97
  }
98
+
99
+ static isStatic(node: ts.Declaration): boolean {
100
+ if ('modifiers' in node && Array.isArray(node.modifiers)) {
101
+ return node.modifiers?.some(x => x.kind === ts.SyntaxKind.StaticKeyword) ?? false;
102
+ }
103
+ return false;
104
+ }
91
105
  }
package/src/util/doc.ts CHANGED
@@ -26,22 +26,22 @@ export class DocUtil {
26
26
  * Read JS Docs from a `ts.Declaration`
27
27
  */
28
28
  static describeDocs(node: ts.Declaration | ts.Type): DeclDocumentation {
29
- if (node && !('getSourceFile' in node)) {
30
- node = DeclarationUtil.getPrimaryDeclarationNode(node);
31
- }
29
+ let toDescribe = (node && !('getSourceFile' in node)) ?
30
+ DeclarationUtil.getOptionalPrimaryDeclarationNode(node) : node;
31
+
32
32
  const out: DeclDocumentation = {
33
33
  description: undefined,
34
34
  return: undefined,
35
35
  params: []
36
36
  };
37
37
 
38
- if (node) {
39
- const tags = ts.getJSDocTags(node);
40
- while (!this.hasJSDoc(node) && CoreUtil.hasOriginal(node)) {
41
- node = transformCast<ts.Declaration>(node.original);
38
+ if (toDescribe) {
39
+ const tags = ts.getJSDocTags(toDescribe);
40
+ while (!this.hasJSDoc(toDescribe) && CoreUtil.hasOriginal(toDescribe)) {
41
+ toDescribe = transformCast<ts.Declaration>(toDescribe.original);
42
42
  }
43
43
 
44
- const docs = this.hasJSDoc(node) ? node.jsDoc : undefined;
44
+ const docs = this.hasJSDoc(toDescribe) ? toDescribe.jsDoc : undefined;
45
45
 
46
46
  if (docs) {
47
47
  const top = docs.at(-1)!;
@@ -92,4 +92,12 @@ export class DocUtil {
92
92
  static readAugments(type: ts.Type | ts.Symbol): string[] {
93
93
  return this.readDocTag(type, 'augments').map(x => x.replace(/^.*?([^` ]+).*?$/, (_, b) => b));
94
94
  }
95
+
96
+ /**
97
+ * Read example information
98
+ * @param type
99
+ */
100
+ static readExample(type: ts.Type | ts.Symbol): string[] {
101
+ return this.readDocTag(type, 'example').map(x => x.replace(/^.*?([^` ]+).*?$/, (_, b) => b));
102
+ }
95
103
  }
package/src/util/log.ts CHANGED
@@ -38,7 +38,7 @@ export class LogUtil {
38
38
  const ox = x;
39
39
  const out: Record<string, unknown> = {};
40
40
  for (const key of TypedObject.keys(ox)) {
41
- if (Object.getPrototypeOf(ox[key]) === Function.prototype || exclude.has(key) || ox[key] === undefined) {
41
+ if (ox[key] === null || ox[key] === undefined || Object.getPrototypeOf(ox[key]) === Function.prototype || exclude.has(key)) {
42
42
  continue;
43
43
  }
44
44
  try {
package/src/visitor.ts CHANGED
@@ -16,7 +16,9 @@ export class VisitorFactory<S extends State = State> {
16
16
  * Get the type of transformer from a given a ts.node
17
17
  */
18
18
  static nodeToType(node: ts.Node): TransformerType | undefined {
19
- if (ts.isMethodDeclaration(node)) {
19
+ if (ts.isConstructorDeclaration(node)) {
20
+ return 'constructor';
21
+ } else if (ts.isMethodDeclaration(node)) {
20
22
  // eslint-disable-next-line no-bitwise
21
23
  return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static) ? 'static-method' : 'method';
22
24
  } else if (ts.isPropertyDeclaration(node)) {