@travetto/transformer 6.0.0 → 7.0.0-rc.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/transformer",
3
- "version": "6.0.0",
3
+ "version": "7.0.0-rc.0",
4
4
  "description": "Functionality for AST transformations, with transformer registration, and general utils",
5
5
  "keywords": [
6
6
  "typescript",
@@ -24,9 +24,9 @@
24
24
  "directory": "module/transformer"
25
25
  },
26
26
  "dependencies": {
27
- "@travetto/manifest": "^6.0.0",
27
+ "@travetto/manifest": "^7.0.0-rc.0",
28
28
  "tslib": "^2.8.1",
29
- "typescript": "^5.8.3"
29
+ "typescript": "^5.9.3"
30
30
  },
31
31
  "travetto": {
32
32
  "displayName": "Transformation",
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,13 @@ 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!);
17
19
  /**
18
20
  * List of global types that can be parameterized
19
21
  */
@@ -39,7 +41,7 @@ const GLOBAL_SIMPLE: Record<string, Function> = {
39
41
 
40
42
  type Category =
41
43
  'tuple' | 'shape' | 'literal' | 'template' | 'managed' |
42
- 'composition' | 'foreign' | 'concrete' | 'unknown';
44
+ 'composition' | 'foreign' | 'concrete' | 'unknown' | 'mapped';
43
45
 
44
46
  /**
45
47
  * Type categorizer, input for builder
@@ -102,9 +104,9 @@ export function TypeCategorize(resolver: TransformResolver, type: ts.Type): { ca
102
104
  return { category: 'tuple', type };
103
105
  } else if (type.isLiteral()) {
104
106
  return { category: 'shape', type };
105
- } else if ((objectFlags & ts.ObjectFlags.Mapped)) { // Mapped types: Pick, Omit, Exclude, Retain
107
+ } else if (objectFlags & ts.ObjectFlags.Mapped) { // Mapped types
106
108
  if (type.getProperties().some(x => x.declarations || x.valueDeclaration)) {
107
- return { category: 'shape', type };
109
+ return { category: 'mapped', type };
108
110
  }
109
111
  }
110
112
  return { category: 'literal', type };
@@ -249,6 +251,45 @@ export const TypeBuilder: {
249
251
  return type;
250
252
  }
251
253
  },
254
+ mapped: {
255
+ build: (resolver, type, alias) => {
256
+ let mainType: ts.Type | undefined;
257
+ let fields: string[] | undefined;
258
+ let operation: string | undefined;
259
+
260
+ const decls = DeclarationUtil.getDeclarations(type).filter(x => ts.isTypeAliasDeclaration(x));
261
+ if (decls.length > 0) {
262
+ const ref = decls[0].type;
263
+ if (ts.isTypeReferenceNode(ref) && ref.typeArguments && ref.typeArguments.length > 0) {
264
+ const [first, second] = ref.typeArguments;
265
+ mainType = resolver.getType(first);
266
+ operation = ref.typeName.getText();
267
+ if (second) {
268
+ const resolved = resolver.getType(second);
269
+ if (resolved.isStringLiteral()) {
270
+ fields = [resolved.value];
271
+ } else if (resolved.isUnion() && resolved.types.every(t => t.isStringLiteral())) {
272
+ fields = resolved.types.map(t => t.value);
273
+ }
274
+ }
275
+ }
276
+ } else {
277
+ mainType = type.aliasTypeArguments?.[0]!;
278
+ operation = type.aliasSymbol?.escapedName.toString();
279
+ fields = type.getApparentProperties().map(p => p.getName());
280
+ }
281
+
282
+ if (!isMappedType(operation) || fields === undefined || !mainType || !mainType.isClass()) {
283
+ return TypeBuilder.shape.build(resolver, type, alias);
284
+ }
285
+
286
+ const importName = resolver.getTypeImportName(mainType) ?? '<unknown>';
287
+ const mappedClassName = resolver.getTypeAsString(mainType)!;
288
+ const name = resolver.getTypeAsString(type)!;
289
+
290
+ return { key: 'mapped', name, original: mainType, operation, importName, mappedClassName, fields };
291
+ }
292
+ },
252
293
  shape: {
253
294
  build: (resolver, type, alias?) => {
254
295
  const tsFieldTypes: Record<string, ts.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,7 +117,7 @@ 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);
@@ -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';
@@ -64,7 +64,7 @@ export class TransformerState implements State {
64
64
  /**
65
65
  * Get or import the node or external type
66
66
  */
67
- getOrImport(type: ManagedType): ts.Identifier | ts.PropertyAccessExpression {
67
+ getOrImport(type: ManagedType | MappedType): ts.Identifier | ts.PropertyAccessExpression {
68
68
  return this.#imports.getOrImport(this.factory, type);
69
69
  }
70
70
 
@@ -129,6 +129,16 @@ export class TransformerState implements State {
129
129
  return DocUtil.readDocTag(this.#resolver.getType(node), name);
130
130
  }
131
131
 
132
+ /**
133
+ * Read all JSDoc tags as a list, with split support
134
+ */
135
+ readDocTagList(node: ts.Declaration, name: string): string[] {
136
+ return this.readDocTag(node, name)
137
+ .flatMap(x => x.split(/\s*,\s*/g))
138
+ .map(x => x.replace(/`/g, ''))
139
+ .filter(x => !!x);
140
+ }
141
+
132
142
  /**
133
143
  * Import a decorator, generally to handle erasure
134
144
  */
@@ -154,20 +164,20 @@ export class TransformerState implements State {
154
164
  */
155
165
  getDecoratorMeta(dec: ts.Decorator): DecoratorMeta | undefined {
156
166
  const ident = DecoratorUtil.getDecoratorIdent(dec);
157
- const decl = DeclarationUtil.getPrimaryDeclarationNode(
158
- this.#resolver.getType(ident)
159
- );
167
+ const type = this.#resolver.getType(ident);
168
+ const decl = DeclarationUtil.getOptionalPrimaryDeclarationNode(type);
160
169
  const src = decl?.getSourceFile().fileName;
161
170
  const mod = src ? this.#resolver.getFileImportName(src, true) : undefined;
162
171
  const file = this.#manifestIndex.getFromImport(mod ?? '')?.outputFile;
163
- const targets = DocUtil.readAugments(this.#resolver.getType(ident));
172
+ const targets = DocUtil.readAugments(type);
173
+ const example = DocUtil.readExample(type);
164
174
  const module = file ? mod : undefined;
165
175
  const name = ident ?
166
176
  ident.escapedText?.toString()! :
167
177
  undefined;
168
178
 
169
179
  if (ident && name) {
170
- return { dec, ident, file, module, targets, name };
180
+ return { dec, ident, file, module, targets, name, options: example };
171
181
  }
172
182
  }
173
183
 
@@ -392,9 +402,15 @@ export class TransformerState implements State {
392
402
  * Produce a foreign target type
393
403
  */
394
404
  getForeignTarget(ret: ForeignType): ts.Expression {
395
- return this.fromLiteral({
396
- Ⲑid: `${ret.source.split('node_modules/')[1]}+${ret.name}`
397
- });
405
+ return this.factory.createClassExpression([], undefined, undefined, undefined, [
406
+ this.factory.createPropertyDeclaration(
407
+ [this.factory.createModifier(ts.SyntaxKind.StaticKeyword)],
408
+ 'Ⲑid',
409
+ undefined,
410
+ undefined,
411
+ this.fromLiteral(`${ret.source.split('node_modules/')[1]}+${ret.name}`)
412
+ )
413
+ ]);
398
414
  }
399
415
 
400
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,25 +26,25 @@ 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
- const top = docs[docs.length - 1];
47
+ const top = docs.at(-1)!;
48
48
  if (ts.isJSDoc(top)) {
49
49
  out.description = this.getDocComment(top, out.description);
50
50
  }
@@ -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)) {