@travetto/transformer 3.0.0-rc.2 → 3.0.0-rc.20

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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <!-- This file was generated by @travetto/doc and should not be modified directly -->
2
- <!-- Please modify https://github.com/travetto/travetto/tree/main/module/transformer/doc.ts and execute "npx trv doc" to rebuild -->
2
+ <!-- Please modify https://github.com/travetto/travetto/tree/main/module/transformer/DOC.ts and execute "npx trv doc" to rebuild -->
3
3
  # Transformation
4
4
  ## Functionality for AST transformations, with transformer registration, and general utils
5
5
 
@@ -10,7 +10,7 @@ npm install @travetto/transformer
10
10
 
11
11
  This module provides support for enhanced AST transformations, and declarative transformer registration, with common patterns to support all the transformers used throughout the framework. Transformations are located by `support/transformer.<name>.ts` as the filename.
12
12
 
13
- The module is primarily aimed at extremely advanced usages for things that cannot be detected at runtime. The [Registry](https://github.com/travetto/travetto/tree/main/module/registry#readme "Patterns and utilities for handling registration of metadata and functionality for run-time use") module already has knowledge of all `class`es and `field`s, and is able to listen to changes there. Many of the modules build upon work by some of the foundational transformers defined in [Registry](https://github.com/travetto/travetto/tree/main/module/registry#readme "Patterns and utilities for handling registration of metadata and functionality for run-time use"), [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding. ") and [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support."). These all center around defining a registry of classes, and associated type information.
13
+ The module is primarily aimed at extremely advanced usages for things that cannot be detected at runtime. The [Registry](https://github.com/travetto/travetto/tree/main/module/registry#readme "Patterns and utilities for handling registration of metadata and functionality for run-time use") module already has knowledge of all `class`es and `field`s, and is able to listen to changes there. Many of the modules build upon work by some of the foundational transformers defined in [Registry](https://github.com/travetto/travetto/tree/main/module/registry#readme "Patterns and utilities for handling registration of metadata and functionality for run-time use"), [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding.") and [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support."). These all center around defining a registry of classes, and associated type information.
14
14
 
15
15
  Because working with the [Typescript](https://typescriptlang.org) API can be delicate (and open to breaking changes), creating new transformers should be done cautiously.
16
16
 
@@ -20,22 +20,19 @@ Below is an example of a transformer that upper cases all `class`, `method` and
20
20
 
21
21
  **Code: Sample Transformer - Upper case all declarations**
22
22
  ```typescript
23
- import * as ts from 'typescript';
23
+ import ts from 'typescript';
24
24
 
25
- import { OnProperty, TransformerState, OnMethod, OnClass, TransformerId } from '@travetto/transformer';
25
+ import { OnProperty, TransformerState, OnMethod, OnClass } from '@travetto/transformer';
26
26
 
27
27
  export class MakeUpper {
28
28
 
29
- static [TransformerId] = '@trv:transformer-test';
30
-
31
29
  @OnProperty()
32
30
  static handleProperty(state: TransformerState, node: ts.PropertyDeclaration): ts.PropertyDeclaration {
33
- if (!state.source.fileName.includes('doc/src')) {
31
+ if (!state.file.includes('doc/src')) {
34
32
  return node;
35
33
  }
36
34
  return state.factory.updatePropertyDeclaration(
37
35
  node,
38
- [],
39
36
  node.modifiers,
40
37
  node.name.getText().toUpperCase(),
41
38
  undefined,
@@ -46,12 +43,11 @@ export class MakeUpper {
46
43
 
47
44
  @OnClass()
48
45
  static handleClass(state: TransformerState, node: ts.ClassDeclaration): ts.ClassDeclaration {
49
- if (!state.source.fileName.includes('doc/src')) {
46
+ if (!state.file.includes('doc/src')) {
50
47
  return node;
51
48
  }
52
49
  return state.factory.updateClassDeclaration(
53
50
  node,
54
- [],
55
51
  node.modifiers,
56
52
  state.createIdentifier(node.name!.getText().toUpperCase()),
57
53
  node.typeParameters,
@@ -62,12 +58,11 @@ export class MakeUpper {
62
58
 
63
59
  @OnMethod()
64
60
  static handleMethod(state: TransformerState, node: ts.MethodDeclaration): ts.MethodDeclaration {
65
- if (!state.source.fileName.includes('doc/src')) {
61
+ if (!state.file.includes('doc/src')) {
66
62
  return node;
67
63
  }
68
64
  return state.factory.updateMethodDeclaration(
69
65
  node,
70
- [],
71
66
  node.modifiers,
72
67
  undefined,
73
68
  state.createIdentifier(node.name.getText().toUpperCase()),
package/__index__.ts ADDED
@@ -0,0 +1,15 @@
1
+ export * from './src/state';
2
+ export * from './src/visitor';
3
+ export * from './src/register';
4
+ export * from './src/types/visitor';
5
+ export * from './src/types/shared';
6
+ export type { AnyType } from './src/resolver/types';
7
+ export * from './src/manager';
8
+
9
+ export * from './src/util/core';
10
+ export * from './src/util/declaration';
11
+ export * from './src/util/decorator';
12
+ export * from './src/util/doc';
13
+ export * from './src/util/literal';
14
+ export * from './src/util/log';
15
+ export * from './src/util/system';
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/transformer",
3
- "displayName": "Transformation",
4
- "version": "3.0.0-rc.2",
3
+ "version": "3.0.0-rc.20",
5
4
  "description": "Functionality for AST transformations, with transformer registration, and general utils",
6
5
  "keywords": [
7
6
  "typescript",
@@ -15,17 +14,25 @@
15
14
  "name": "Travetto Framework"
16
15
  },
17
16
  "files": [
18
- "index.ts",
17
+ "__index__.ts",
19
18
  "src",
20
- "test-support"
19
+ "support"
21
20
  ],
22
- "main": "index.ts",
21
+ "main": "__index__.ts",
23
22
  "repository": {
24
23
  "url": "https://github.com/travetto/travetto.git",
25
24
  "directory": "module/transformer"
26
25
  },
27
26
  "dependencies": {
28
- "@travetto/base": "^3.0.0-rc.0"
27
+ "@travetto/manifest": "^3.0.0-rc.16",
28
+ "tslib": "^2.5.0",
29
+ "typescript": "^4.9.5"
30
+ },
31
+ "travetto": {
32
+ "displayName": "Transformation",
33
+ "profiles": [
34
+ "compile"
35
+ ]
29
36
  },
30
37
  "publishConfig": {
31
38
  "access": "public"
package/src/importer.ts CHANGED
@@ -1,13 +1,15 @@
1
- import * as ts from 'typescript';
2
- import { basename, dirname, relative } from 'path';
1
+ import ts from 'typescript';
3
2
 
4
- import { PathUtil } from '@travetto/boot';
5
- import { ModuleUtil } from '@travetto/boot/src/internal/module-util';
3
+ import { PackageUtil, path } from '@travetto/manifest';
6
4
 
7
- import { AnyType, ExternalType } from './resolver/types';
5
+ import { AnyType, TransformResolver, ExternalType } from './resolver/types';
8
6
  import { ImportUtil } from './util/import';
9
7
  import { CoreUtil } from './util/core';
10
8
  import { Import } from './types/shared';
9
+ import { LiteralUtil } from './util/literal';
10
+ import { DeclarationUtil } from './util/declaration';
11
+
12
+ const D_OR_D_TS_EXT_RE = /[.]d([.]ts)?$/;
11
13
 
12
14
  /**
13
15
  * Manages imports within a ts.SourceFile
@@ -18,18 +20,79 @@ export class ImportManager {
18
20
  #imports: Map<string, Import>;
19
21
  #idx: Record<string, number> = {};
20
22
  #ids = new Map<string, string>();
23
+ #importName: string;
24
+ #resolver: TransformResolver;
21
25
 
22
- constructor(public source: ts.SourceFile, public factory: ts.NodeFactory) {
26
+ constructor(public source: ts.SourceFile, public factory: ts.NodeFactory, resolver: TransformResolver) {
23
27
  this.#imports = ImportUtil.collectImports(source);
28
+ this.#resolver = resolver;
29
+ this.#importName = this.#resolver.getFileImportName(source.fileName);
30
+ }
31
+
32
+ #getImportFile(spec?: ts.Expression): string | undefined {
33
+ if (spec && ts.isStringLiteral(spec)) {
34
+ return spec.text.replace(/^['"]|["']$/g, '');
35
+ }
36
+ }
37
+
38
+ #rewriteModuleSpecifier(spec: ts.Expression | undefined): ts.Expression | undefined {
39
+ const fileOrImport = this.#getImportFile(spec);
40
+ if (
41
+ fileOrImport &&
42
+ (fileOrImport.startsWith('.') || this.#resolver.isKnownFile(fileOrImport)) &&
43
+ !/[.]([mc]?js|ts|json)$/.test(fileOrImport)
44
+ ) {
45
+ return LiteralUtil.fromLiteral(this.factory, `${fileOrImport}.js`);
46
+ }
47
+ return spec;
48
+ }
49
+
50
+ #rewriteImportClause(spec: ts.Expression | undefined, clause: ts.ImportClause | undefined): ts.ImportClause | undefined {
51
+ if (!(spec && clause?.namedBindings && ts.isNamedImports(clause.namedBindings))) {
52
+ return clause;
53
+ }
54
+
55
+ const fileOrImport = this.#getImportFile(spec);
56
+ if (!(fileOrImport && (fileOrImport.startsWith('.') || this.#resolver.isKnownFile(fileOrImport)))) {
57
+ return clause;
58
+ }
59
+
60
+ const bindings = clause.namedBindings;
61
+ const newBindings: ts.ImportSpecifier[] = [];
62
+ // Remove all type only imports
63
+ for (const el of bindings.elements) {
64
+ if (!el.isTypeOnly) {
65
+ const type = this.#resolver.getType(el.name);
66
+ const objFlags = DeclarationUtil.getObjectFlags(type);
67
+ const typeFlags = type.getFlags();
68
+ if (objFlags || typeFlags !== 1) {
69
+ newBindings.push(el);
70
+ }
71
+ }
72
+ }
73
+ if (newBindings.length !== bindings.elements.length) {
74
+ return this.factory.updateImportClause(
75
+ clause,
76
+ clause.isTypeOnly,
77
+ clause.name,
78
+ this.factory.createNamedImports(newBindings)
79
+ );
80
+ } else {
81
+ return clause;
82
+ }
24
83
  }
25
84
 
26
85
  /**
27
86
  * Produces a unique ID for a given file, importing if needed
28
87
  */
29
- getId(file: string): string {
88
+ getId(file: string, name?: string): string {
30
89
  if (!this.#ids.has(file)) {
31
- const key = basename(file).replace(/[.][^.]*$/, '').replace(/[^A-Za-z0-9]+/g, '_');
32
- this.#ids.set(file, `ᚕ_${key}_${this.#idx[key] = (this.#idx[key] || 0) + 1}`);
90
+ if (name) {
91
+ this.#ids.set(file, name);
92
+ } else {
93
+ const key = path.basename(file).replace(/[.][^.]*$/, '').replace(/[^A-Za-z0-9]+/g, '_');
94
+ this.#ids.set(file, `Ⲑ_${key}_${this.#idx[key] = (this.#idx[key] || 0) + 1}`);
95
+ }
33
96
  }
34
97
  return this.#ids.get(file)!;
35
98
  }
@@ -37,28 +100,16 @@ export class ImportManager {
37
100
  /**
38
101
  * Import a file if needed, and record it's identifier
39
102
  */
40
- importFile(file: string, base?: string): Import {
41
- file = ModuleUtil.normalizePath(file);
103
+ importFile(file: string, name?: string): Import {
104
+ file = this.#resolver.getFileImportName(file);
42
105
 
43
106
  // Allow for node classes to be imported directly
44
107
  if (/@types\/node/.test(file)) {
45
- file = require.resolve(file.replace(/.*@types\/node\//, '').replace(/[.]d([.]ts)?$/, ''));
46
- }
47
-
48
- // Handle relative imports
49
- if (file.startsWith('.') && base &&
50
- !base.startsWith('@travetto') && !base.includes('node_modules')
51
- ) { // Relative path
52
- const fileDir = dirname(PathUtil.resolveUnix(file));
53
- const baseDir = dirname(PathUtil.resolveUnix(base));
54
- file = `${relative(baseDir, fileDir) || '.'}/${basename(file)}`;
55
- if (/^[A-Za-z]/.test(file)) {
56
- file = `./${file}`;
57
- }
108
+ file = PackageUtil.resolveImport(file.replace(/.*@types\/node\//, '').replace(D_OR_D_TS_EXT_RE, ''));
58
109
  }
59
110
 
60
- if (!/[.]d([.]ts)?$/.test(file) && !this.#newImports.has(file)) {
61
- const id = this.getId(file);
111
+ if (!D_OR_D_TS_EXT_RE.test(file) && !this.#newImports.has(file)) {
112
+ const id = this.getId(file, name);
62
113
 
63
114
  if (this.#imports.has(id)) { // Already imported, be cool
64
115
  return this.#imports.get(id)!;
@@ -77,8 +128,8 @@ export class ImportManager {
77
128
  */
78
129
  importFromResolved(...types: AnyType[]): void {
79
130
  for (const type of types) {
80
- if (type.key === 'external' && type.source && type.source !== this.source.fileName) {
81
- this.importFile(type.source, this.source.fileName);
131
+ if (type.key === 'external' && type.importName && type.importName !== this.#importName) {
132
+ this.importFile(type.importName);
82
133
  }
83
134
  switch (type.key) {
84
135
  case 'external':
@@ -99,11 +150,11 @@ export class ImportManager {
99
150
  }
100
151
 
101
152
  try {
102
- const importStmts = [...this.#newImports.values()].map(({ path, ident }) => {
153
+ const importStmts = [...this.#newImports.values()].map(({ path: resolved, ident }) => {
103
154
  const importStmt = this.factory.createImportDeclaration(
104
- undefined, undefined,
155
+ undefined,
105
156
  this.factory.createImportClause(false, undefined, this.factory.createNamespaceImport(ident)),
106
- this.factory.createStringLiteral(path)
157
+ this.factory.createStringLiteral(resolved)
107
158
  );
108
159
  return importStmt;
109
160
  });
@@ -116,27 +167,61 @@ export class ImportManager {
116
167
  if (!(err instanceof Error)) {
117
168
  throw err;
118
169
  }
119
- const out = new Error(`${err.message} in ${file.fileName.replace(PathUtil.cwd, '.')}`);
170
+ const out = new Error(`${err.message} in ${file.fileName.replace(process.cwd(), '.')}`);
120
171
  out.stack = err.stack;
121
172
  throw out;
122
173
  }
123
174
  }
124
175
 
176
+ finalizeImportExportExtension(ret: ts.SourceFile): ts.SourceFile {
177
+ const toAdd: ts.Statement[] = [];
178
+
179
+ for (const stmt of ret.statements) {
180
+ if (ts.isExportDeclaration(stmt)) {
181
+ if (!stmt.isTypeOnly) {
182
+ toAdd.push(this.factory.updateExportDeclaration(
183
+ stmt,
184
+ stmt.modifiers,
185
+ stmt.isTypeOnly,
186
+ stmt.exportClause,
187
+ this.#rewriteModuleSpecifier(stmt.moduleSpecifier),
188
+ stmt.assertClause
189
+ ));
190
+ }
191
+ } else if (ts.isImportDeclaration(stmt)) {
192
+ if (!stmt.importClause?.isTypeOnly) {
193
+ toAdd.push(this.factory.updateImportDeclaration(
194
+ stmt,
195
+ stmt.modifiers,
196
+ this.#rewriteImportClause(stmt.moduleSpecifier, stmt.importClause)!,
197
+ this.#rewriteModuleSpecifier(stmt.moduleSpecifier)!,
198
+ stmt.assertClause
199
+ ));
200
+ }
201
+ } else {
202
+ toAdd.push(stmt);
203
+ }
204
+ }
205
+ return CoreUtil.updateSource(this.factory, ret, toAdd);
206
+ }
207
+
125
208
  /**
126
209
  * Reset the imports into the source file
127
210
  */
128
211
  finalize(ret: ts.SourceFile): ts.SourceFile {
129
- return this.finalizeNewImports(ret) ?? ret;
212
+ ret = this.finalizeNewImports(ret) ?? ret;
213
+ ret = this.finalizeImportExportExtension(ret) ?? ret;
214
+ return ret;
130
215
  }
131
216
 
132
217
  /**
133
218
  * Get the identifier and import if needed
134
219
  */
135
220
  getOrImport(factory: ts.NodeFactory, type: ExternalType): ts.Identifier | ts.PropertyAccessExpression {
136
- if (type.source === this.source.fileName) {
221
+ if (type.importName === this.#importName) {
137
222
  return factory.createIdentifier(type.name!);
138
223
  } else {
139
- const { ident } = this.#imports.get(type.source) ?? this.importFile(type.source, this.source.fileName);
224
+ const { ident } = this.#imports.get(type.importName) ?? this.importFile(type.importName);
140
225
  return factory.createPropertyAccessExpression(ident, type.name!);
141
226
  }
142
227
  }
package/src/manager.ts ADDED
@@ -0,0 +1,74 @@
1
+ import ts from 'typescript';
2
+
3
+ import { ManifestIndex, path, RootIndex } from '@travetto/manifest';
4
+
5
+ import { NodeTransformer } from './types/visitor';
6
+ import { VisitorFactory } from './visitor';
7
+ import { TransformerState } from './state';
8
+ import { getAllTransformers } from './register';
9
+
10
+ /**
11
+ * Manages the typescript transformers
12
+ */
13
+ export class TransformerManager {
14
+
15
+ /**
16
+ * Create transformer manager
17
+ * @param transformerFiles
18
+ * @param manifest
19
+ * @returns
20
+ */
21
+ static async create(manifestIndex: ManifestIndex): Promise<TransformerManager> {
22
+ const transformerFiles = Object.values(manifestIndex.manifest.modules).flatMap(
23
+ x => (x.files.$transformer ?? []).map(([f]) =>
24
+ path.resolve(manifestIndex.manifest.workspacePath, x.sourceFolder, f)
25
+ )
26
+ );
27
+
28
+ const transformers: NodeTransformer<TransformerState>[] = [];
29
+
30
+ for (const file of transformerFiles) { // Exclude based on blacklist
31
+ const entry = RootIndex.getEntry(file)!;
32
+ transformers.push(...getAllTransformers(await import(entry.import), entry.module));
33
+ }
34
+
35
+ for (const x of transformers) {
36
+ process.send?.(['debug', `Loaded Transformer: ${x.key}#${x.type}`]);
37
+ }
38
+
39
+ // Prepare a new visitor factory with a given type checker
40
+ return new TransformerManager(manifestIndex, transformers);
41
+ }
42
+
43
+ #cached: ts.CustomTransformers | undefined;
44
+ #transformers: NodeTransformer<TransformerState>[];
45
+ #manifestIndex: ManifestIndex;
46
+
47
+ constructor(manifestIndex: ManifestIndex, transformers: NodeTransformer<TransformerState>[]) {
48
+ this.#transformers = transformers;
49
+ this.#manifestIndex = manifestIndex;
50
+ }
51
+
52
+ /**
53
+ * Initialize with type checker
54
+ * @param checker
55
+ */
56
+ init(checker: ts.TypeChecker): void {
57
+ const visitor = new VisitorFactory(
58
+ (ctx, src) => new TransformerState(src, ctx.factory, checker, this.#manifestIndex),
59
+ this.#transformers
60
+ );
61
+
62
+ // Define transformers for the compiler
63
+ this.#cached = {
64
+ before: [visitor.visitor()]
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Get typescript transformer object
70
+ */
71
+ get(): ts.CustomTransformers | undefined {
72
+ return this.#cached!;
73
+ }
74
+ }
package/src/register.ts CHANGED
@@ -1,26 +1,55 @@
1
- import * as ts from 'typescript';
2
- import { DecoratorMeta, NodeTransformer, State, TransformPhase, TransformerType, Transformer, TransformerId } from './types/visitor';
1
+ import ts from 'typescript';
3
2
 
4
- const HandlersProp = Symbol.for('@trv:transformer/handlers');
3
+ import { DecoratorMeta, NodeTransformer, State, TransformPhase, TransformerType, Transformer, ModuleNameⲐ } from './types/visitor';
4
+
5
+ const HandlersProp = Symbol.for('@travetto/transformer:handlers');
5
6
 
6
7
  type TransformerWithHandlers = Transformer & { [HandlersProp]?: NodeTransformer[] };
7
8
 
9
+ function isTransformer(x: unknown): x is Transformer {
10
+ return x !== null && x !== undefined && typeof x === 'function';
11
+ }
12
+
8
13
  /**
9
14
  * Get all transformers
10
15
  * @param obj Object to search for transformers
11
16
  */
12
- export function getAllTransformers(obj: Record<string, { [HandlersProp]?: NodeTransformer[] }>): NodeTransformer[] {
13
- return Object.values(obj).flatMap(x => x[HandlersProp] ?? []);
17
+ export function getAllTransformers(obj: Record<string, { [HandlersProp]?: NodeTransformer[] }>, module: string): NodeTransformer[] {
18
+ return Object.values(obj)
19
+ .flatMap(x => {
20
+ if (isTransformer(x)) {
21
+ x[ModuleNameⲐ] = module;
22
+ }
23
+ return (x[HandlersProp] ?? []);
24
+ })
25
+ .map(handler => ({
26
+ ...handler,
27
+ key: `${module}:${handler.key}`,
28
+ target: handler.target?.map(t => `${module}:${t}`)
29
+ }));
14
30
  }
15
31
 
16
32
  // Store handlers in class
17
33
  function storeHandler(cls: TransformerWithHandlers, fn: Function, phase: TransformPhase, type: TransformerType, target?: string[]): void {
18
- if (target) {
19
- const ns = cls[TransformerId].split('/')[0]; // Everything before the '/'
20
- target = target.map(x => x.startsWith('@') ? x : `${ns}/${x}`);
21
- }
22
- cls[HandlersProp] = cls[HandlersProp] ?? [];
23
- cls[HandlersProp]!.push({ key: `${cls[TransformerId]}/${fn.name}`, [phase]: fn.bind(cls), type, target });
34
+ (cls[HandlersProp] ??= []).push({ key: fn.name, [phase]: fn.bind(cls), type, target });
35
+ }
36
+
37
+ /**
38
+ * Wraps entire file before transforming
39
+ */
40
+ export function OnFile(...target: string[]) {
41
+ return <S extends State = State, R extends ts.Node = ts.Node>(
42
+ inst: Transformer, __: unknown, d: TypedPropertyDescriptor<(state: S, node: ts.SourceFile) => R>
43
+ ): void => storeHandler(inst, d.value!, 'before', 'file', target);
44
+ }
45
+
46
+ /**
47
+ * Wraps entire file after transforming
48
+ */
49
+ export function AfterFile(...target: string[]) {
50
+ return <S extends State = State, R extends ts.Node = ts.Node>(
51
+ inst: Transformer, __: unknown, d: TypedPropertyDescriptor<(state: S, node: ts.SourceFile) => R>
52
+ ): void => storeHandler(inst, d.value!, 'before', 'file', target);
24
53
  }
25
54
 
26
55
  /**