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

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.
@@ -1,12 +1,15 @@
1
1
  /* eslint-disable no-bitwise */
2
- import * as ts from 'typescript';
3
- import { dirname } from 'path';
2
+ import ts from 'typescript';
4
3
 
5
- import { PathUtil } from '@travetto/boot';
6
- import { Util } from '@travetto/base';
4
+ import { path } from '@travetto/manifest';
7
5
 
8
- import { Type, AnyType, UnionType, Checker } from './types';
9
- import { DocUtil, CoreUtil, DeclarationUtil, LiteralUtil } from '../util';
6
+ import { DocUtil } from '../util/doc';
7
+ import { CoreUtil } from '../util/core';
8
+ import { DeclarationUtil } from '../util/declaration';
9
+ import { LiteralUtil } from '../util/literal';
10
+
11
+ import { Type, AnyType, UnionType, TransformResolver } from './types';
12
+ import { CoerceUtil } from './coerce';
10
13
 
11
14
  /**
12
15
  * List of global types that can be parameterized
@@ -35,7 +38,7 @@ type Category = 'void' | 'undefined' | 'concrete' | 'unknown' | 'tuple' | 'shape
35
38
  /**
36
39
  * Type categorizer, input for builder
37
40
  */
38
- export function TypeCategorize(checker: ts.TypeChecker, type: ts.Type): { category: Category, type: ts.Type } {
41
+ export function TypeCategorize(resolver: TransformResolver, type: ts.Type): { category: Category, type: ts.Type } {
39
42
  const flags = type.getFlags();
40
43
  const objectFlags = DeclarationUtil.getObjectFlags(type) ?? 0;
41
44
 
@@ -62,9 +65,10 @@ export function TypeCategorize(checker: ts.TypeChecker, type: ts.Type): { catego
62
65
  }
63
66
 
64
67
  const source = DeclarationUtil.getPrimaryDeclarationNode(resolvedType).getSourceFile();
65
- if (source?.fileName.includes('@types/node/globals') || source?.fileName.includes('typescript/lib')) {
68
+ const sourceFile = source.fileName;
69
+ if (sourceFile?.includes('@types/node/globals') || sourceFile?.includes('typescript/lib')) {
66
70
  return { category: 'literal', type };
67
- } else if (!source?.fileName.includes('@travetto') && source?.fileName.endsWith('.d.ts')) {
71
+ } else if (sourceFile?.endsWith('.d.ts') && !resolver.isKnownFile(sourceFile)) {
68
72
  return { category: 'unknown', type };
69
73
  } else if (!resolvedType.isClass()) { // Not a real type
70
74
  return { category: 'shape', type: resolvedType };
@@ -93,32 +97,32 @@ export function TypeCategorize(checker: ts.TypeChecker, type: ts.Type): { catego
93
97
  */
94
98
  export const TypeBuilder: {
95
99
  [K in Category]: {
96
- build(checker: Checker, type: ts.Type, alias?: ts.Symbol): AnyType | undefined;
100
+ build(resolver: TransformResolver, type: ts.Type, alias?: ts.Symbol): AnyType | undefined;
97
101
  finalize?(type: Type<K>): AnyType;
98
102
  }
99
103
  } = {
100
104
  unknown: {
101
- build: (checker, type) => undefined
105
+ build: (resolver, type) => undefined
102
106
  },
103
107
  undefined: {
104
- build: (checker, type) => ({ key: 'literal', name: 'undefined', ctor: undefined })
108
+ build: (resolver, type) => ({ key: 'literal', name: 'undefined', ctor: undefined })
105
109
  },
106
110
  void: {
107
- build: (checker, type) => ({ key: 'literal', name: 'void', ctor: undefined })
111
+ build: (resolver, type) => ({ key: 'literal', name: 'void', ctor: undefined })
108
112
  },
109
113
  tuple: {
110
- build: (checker, type) => ({ key: 'tuple', tsTupleTypes: checker.getAllTypeArguments(type), subTypes: [] })
114
+ build: (resolver, type) => ({ key: 'tuple', tsTupleTypes: resolver.getAllTypeArguments(type), subTypes: [] })
111
115
  },
112
116
  literal: {
113
- build: (checker, type) => {
117
+ build: (resolver, type) => {
114
118
  // Handle void/undefined
115
- const name = checker.getTypeAsString(type) ?? '';
119
+ const name = resolver.getTypeAsString(type) ?? '';
116
120
  const complexName = CoreUtil.getSymbol(type)?.getName() ?? '';
117
121
 
118
122
  if (name in GLOBAL_SIMPLE) {
119
123
  const cons = GLOBAL_SIMPLE[name];
120
124
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
121
- const ret = LiteralUtil.isLiteralType(type) ? Util.coerceType(type.value, cons as typeof String, false) :
125
+ const ret = LiteralUtil.isLiteralType(type) ? CoerceUtil.coerce(type.value, cons as typeof String, false) :
122
126
  undefined;
123
127
 
124
128
  return {
@@ -133,23 +137,21 @@ export const TypeBuilder: {
133
137
  key: 'literal',
134
138
  name: cons.name,
135
139
  ctor: cons,
136
- tsTypeArguments: checker.getAllTypeArguments(type)
140
+ tsTypeArguments: resolver.getAllTypeArguments(type)
137
141
  };
138
142
  }
139
143
  }
140
144
  },
141
145
  external: {
142
- build: (checker, type) => {
143
- const source = DeclarationUtil.getPrimaryDeclarationNode(type).getSourceFile();
146
+ build: (resolver, type) => {
144
147
  const name = CoreUtil.getSymbol(type)?.getName();
145
- return {
146
- key: 'external', name, source: source.fileName,
147
- tsTypeArguments: checker.getAllTypeArguments(type)
148
- };
148
+ const importName = resolver.getTypeImportName(type)!;
149
+ const tsTypeArguments = resolver.getAllTypeArguments(type);
150
+ return { key: 'external', name, importName, tsTypeArguments };
149
151
  }
150
152
  },
151
153
  union: {
152
- build: (checker, uType: ts.UnionType) => {
154
+ build: (resolver, uType: ts.UnionType) => {
153
155
  let undefinable = false;
154
156
  let nullable = false;
155
157
  const remainder = uType.types.filter(ut => {
@@ -175,50 +177,56 @@ export const TypeBuilder: {
175
177
  }
176
178
  },
177
179
  shape: {
178
- build: (checker, type, alias?) => {
179
- const fieldNodes: Record<string, ts.Type> = {};
180
- const name = CoreUtil.getSymbol(alias ?? type);
181
- const source = DeclarationUtil.getPrimaryDeclarationNode(type)?.getSourceFile();
182
- for (const member of checker.getPropertiesOfType(type)) {
180
+ build: (resolver, type, alias?) => {
181
+ const tsFieldTypes: Record<string, ts.Type> = {};
182
+ const name = CoreUtil.getSymbol(alias ?? type)?.getName();
183
+ const importName = resolver.getTypeImportName(type) ?? '<unknown>';
184
+ const tsTypeArguments = resolver.getAllTypeArguments(type);
185
+ const props = resolver.getPropertiesOfType(type);
186
+ if (props.length === 0) {
187
+ return { key: 'unknown', name, importName };
188
+ }
189
+
190
+ for (const member of props) {
183
191
  const dec = DeclarationUtil.getPrimaryDeclarationNode(member);
184
192
  if (DeclarationUtil.isPublic(dec)) { // If public
185
- const memberType = checker.getType(dec);
186
- if (!memberType.getCallSignatures().length) { // if not a function
187
- fieldNodes[member.getName()] = memberType;
193
+ const memberType = resolver.getType(dec);
194
+ if (
195
+ !member.getName().includes('@') && // if not a symbol
196
+ !memberType.getCallSignatures().length // if not a function
197
+ ) {
198
+ tsFieldTypes[member.getName()] = memberType;
188
199
  }
189
200
  }
190
201
  }
191
- return {
192
- key: 'shape', name: name?.getName(),
193
- source: source?.fileName,
194
- tsFieldTypes: fieldNodes,
195
- tsTypeArguments: checker.getAllTypeArguments(type),
196
- fieldTypes: {}
197
- };
202
+ return { key: 'shape', name, importName, tsFieldTypes, tsTypeArguments, fieldTypes: {} };
198
203
  }
199
204
  },
200
205
  concrete: {
201
- build: (checker, type) => {
206
+ build: (resolver, type) => {
202
207
  const [tag] = DocUtil.readDocTag(type, 'concrete');
203
208
  if (tag) {
204
209
  // eslint-disable-next-line prefer-const
205
- let [source, name, ext] = tag.split(':');
210
+ let [importName, name] = tag.split(':');
206
211
  if (!name) {
207
- name = source;
208
- source = '.';
212
+ name = importName;
213
+ importName = '.';
209
214
  }
210
215
 
211
- const sourceFile: string = DeclarationUtil.getDeclarations(type)
212
- ?.find(x => ts.getAllJSDocTags(x, (t): t is ts.JSDocTag => t.tagName.getText() === 'concrete').length)
213
- ?.getSourceFile().fileName ?? '';
216
+ // Resolving relative to source file
217
+ if (importName.startsWith('.')) {
218
+ const rawSourceFile: string = DeclarationUtil.getDeclarations(type)
219
+ ?.find(x => ts.getAllJSDocTags(x, (t): t is ts.JSDocTag => t.tagName.getText() === 'concrete').length)
220
+ ?.getSourceFile().fileName ?? '';
214
221
 
215
- if (source === '.') {
216
- source = sourceFile;
217
- } else if (source.startsWith('.')) {
218
- source = PathUtil.resolveUnix(dirname(sourceFile), source);
222
+ if (importName === '.') {
223
+ importName = resolver.getFileImportName(rawSourceFile);
224
+ } else {
225
+ const base = path.dirname(rawSourceFile);
226
+ importName = resolver.getFileImportName(path.resolve(base, importName));
227
+ }
219
228
  }
220
-
221
- return { key: 'external', name, source: ext === 'node' ? source : PathUtil.resolveUnix(sourceFile, source) };
229
+ return { key: 'external', name, importName };
222
230
  }
223
231
  }
224
232
  }
@@ -1,5 +1,5 @@
1
- import * as ts from 'typescript';
2
- import { AnyType } from './types';
1
+ import ts from 'typescript';
2
+ import type { AnyType } from './types';
3
3
 
4
4
  /**
5
5
  * Cache for handling recursive checks
@@ -0,0 +1,103 @@
1
+ const REGEX_PAT = /[\/](.*)[\/](i|g|m|s)?/;
2
+
3
+ export class CoerceUtil {
4
+ /**
5
+ * Is a value a plain JS object, created using {}
6
+ */
7
+ static #isPlainObject(obj: unknown): obj is Record<string, unknown> {
8
+ return typeof obj === 'object' // separate from primitives
9
+ && obj !== undefined
10
+ && obj !== null // is obvious
11
+ && obj.constructor === Object // separate instances (Array, DOM, ...)
12
+ && Object.prototype.toString.call(obj) === '[object Object]'; // separate build-in like Math
13
+ }
14
+
15
+ /**
16
+ * Create regex from string, including flags
17
+ */
18
+ static #toRegex(input: string | RegExp): RegExp {
19
+ if (input instanceof RegExp) {
20
+ return input;
21
+ } else if (REGEX_PAT.test(input)) {
22
+ const [, pat, mod] = input.match(REGEX_PAT) ?? [];
23
+ return new RegExp(pat, mod);
24
+ } else {
25
+ return new RegExp(input);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Coerce an input of any type to the class provided
31
+ * @param input Input value
32
+ * @param type Class to coerce to (String, Boolean, Number, Date, RegEx, Object)
33
+ * @param strict Should a failure to coerce throw an error?
34
+ */
35
+ static coerce(input: unknown, type: typeof String, strict?: boolean): string;
36
+ static coerce(input: unknown, type: typeof Number, strict?: boolean): number;
37
+ static coerce(input: unknown, type: typeof Boolean, strict?: boolean): boolean;
38
+ static coerce(input: unknown, type: typeof Date, strict?: boolean): Date;
39
+ static coerce(input: unknown, type: typeof RegExp, strict?: boolean): RegExp;
40
+ static coerce(input: unknown, type: Function, strict = true): unknown {
41
+ // Do nothing
42
+ if (input === null || input === undefined) {
43
+ return input;
44
+ } else if (!strict && type !== String && input === '') {
45
+ return undefined; // treat empty string as undefined for non-strings in non-strict mode
46
+ } else if (type && input instanceof type) {
47
+ return input;
48
+ }
49
+
50
+ switch (type) {
51
+ case Date: {
52
+ const res = typeof input === 'number' || /^[-]?\d+$/.test(`${input}`) ?
53
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
54
+ new Date(parseInt(input as string, 10)) : new Date(input as Date);
55
+ if (strict && Number.isNaN(res.getTime())) {
56
+ throw new Error(`Invalid date value: ${input}`);
57
+ }
58
+ return res;
59
+ }
60
+ case Number: {
61
+ const res = `${input}`.includes('.') ? parseFloat(`${input}`) : parseInt(`${input}`, 10);
62
+ if (strict && Number.isNaN(res)) {
63
+ throw new Error(`Invalid numeric value: ${input}`);
64
+ }
65
+ return res;
66
+ }
67
+ case Boolean: {
68
+ const match = `${input}`.match(/^((?<TRUE>true|yes|1|on)|false|no|off|0)$/i);
69
+ if (strict && !match) {
70
+ throw new Error(`Invalid boolean value: ${input}`);
71
+ }
72
+ return !!match?.groups?.TRUE;
73
+ }
74
+ case RegExp: {
75
+ if (typeof input === 'string') {
76
+ try {
77
+ return this.#toRegex(input);
78
+ } catch {
79
+ if (strict) {
80
+ throw new Error(`Invalid regex: ${input}`);
81
+ } else {
82
+ return;
83
+ }
84
+ }
85
+ } else if (strict) {
86
+ throw new Error('Invalid regex type');
87
+ } else {
88
+ return;
89
+ }
90
+ }
91
+ case Object: {
92
+ if (!strict || this.#isPlainObject(input)) {
93
+ return input;
94
+ } else {
95
+ throw new Error('Invalid object type');
96
+ }
97
+ }
98
+ case undefined:
99
+ case String: return `${input}`;
100
+ }
101
+ throw new Error(`Unknown type ${type.name}`);
102
+ }
103
+ }
@@ -1,18 +1,23 @@
1
- import * as ts from 'typescript';
1
+ import ts from 'typescript';
2
2
 
3
- import { AnyType, Checker } from './types';
3
+ import { ManifestIndex, path } from '@travetto/manifest';
4
+
5
+ import type { AnyType, TransformResolver } from './types';
4
6
  import { TypeCategorize, TypeBuilder } from './builder';
5
7
  import { VisitCache } from './cache';
6
- import { DocUtil } from '../util';
8
+ import { DocUtil } from '../util/doc';
9
+ import { DeclarationUtil } from '../util/declaration';
7
10
 
8
11
  /**
9
- * Type resolver
12
+ * Implementation of TransformResolver
10
13
  */
11
- export class TypeResolver implements Checker {
14
+ export class SimpleResolver implements TransformResolver {
12
15
  #tsChecker: ts.TypeChecker;
16
+ #manifestIndex: ManifestIndex;
13
17
 
14
- constructor(tsChecker: ts.TypeChecker) {
18
+ constructor(tsChecker: ts.TypeChecker, manifestIndex: ManifestIndex) {
15
19
  this.#tsChecker = tsChecker;
20
+ this.#manifestIndex = manifestIndex;
16
21
  }
17
22
 
18
23
  /**
@@ -23,6 +28,40 @@ export class TypeResolver implements Checker {
23
28
  return this.#tsChecker;
24
29
  }
25
30
 
31
+ /**
32
+ * Resolve an import name (e.g. @module/path/file) for a file
33
+ */
34
+ getFileImportName(file: string, removeExt?: boolean): string {
35
+ let sourceFile = path.toPosix(file);
36
+
37
+ if (!sourceFile.endsWith('.js') && !sourceFile.endsWith('.ts')) {
38
+ sourceFile = `${sourceFile}.ts`;
39
+ }
40
+
41
+ const imp =
42
+ this.#manifestIndex.getEntry(/[.]ts$/.test(sourceFile) ? sourceFile : `${sourceFile}.js`)?.import ??
43
+ this.#manifestIndex.getFromImport(sourceFile.replace(/^.*node_modules\//, '').replace(/[.]ts$/, ''))?.import ??
44
+ file;
45
+
46
+ return removeExt ? imp.replace(/[.]js$/, '') : imp;
47
+ }
48
+
49
+ /**
50
+ * Resolve an import name (e.g. @module/path/file) for a type
51
+ */
52
+ getTypeImportName(type: ts.Type, removeExt?: boolean): string | undefined {
53
+ const ogSource = DeclarationUtil.getPrimaryDeclarationNode(type)?.getSourceFile()?.fileName;
54
+ return ogSource ? this.getFileImportName(ogSource, removeExt) : undefined;
55
+ }
56
+
57
+ /**
58
+ * Is the file/import known to the index, helpful for determine ownership
59
+ */
60
+ isKnownFile(fileOrImport: string): boolean {
61
+ return (this.#manifestIndex.getFromSource(fileOrImport) !== undefined) ||
62
+ (this.#manifestIndex.getFromImport(fileOrImport) !== undefined);
63
+ }
64
+
26
65
  /**
27
66
  * Get type from element
28
67
  * @param el
@@ -65,7 +104,7 @@ export class TypeResolver implements Checker {
65
104
  /**
66
105
  * Resolve an `AnyType` from a `ts.Type` or a `ts.Node`
67
106
  */
68
- resolveType(node: ts.Type | ts.Node): AnyType {
107
+ resolveType(node: ts.Type | ts.Node, importName: string): AnyType {
69
108
  const visited = new VisitCache();
70
109
  const resolve = (resType: ts.Type, alias?: ts.Symbol, depth = 0): AnyType => {
71
110
 
@@ -73,7 +112,7 @@ export class TypeResolver implements Checker {
73
112
  throw new Error('Object structure too nested');
74
113
  }
75
114
 
76
- const { category, type } = TypeCategorize(this.#tsChecker, resType);
115
+ const { category, type } = TypeCategorize(this, resType);
77
116
  const { build, finalize } = TypeBuilder[category];
78
117
 
79
118
  let result = build(this, type, alias);
@@ -117,7 +156,7 @@ export class TypeResolver implements Checker {
117
156
  if (!(err instanceof Error)) {
118
157
  throw err;
119
158
  }
120
- console.error('Unable to resolve type', err.stack);
159
+ console.error(`Unable to resolve type in ${importName}`, err.stack);
121
160
  return { key: 'literal', ctor: Object, name: 'object' };
122
161
  }
123
162
  }
@@ -1,4 +1,4 @@
1
- import type * as ts from 'typescript';
1
+ import type ts from 'typescript';
2
2
 
3
3
  /**
4
4
  * Base type for a simplistic type structure
@@ -37,7 +37,7 @@ export interface ExternalType extends Type<'external'> {
37
37
  /**
38
38
  * Location the type came from, for class references
39
39
  */
40
- source: string;
40
+ importName: string;
41
41
  /**
42
42
  * Type arguments
43
43
  */
@@ -55,7 +55,7 @@ export interface ShapeType extends Type<'shape'> {
55
55
  /**
56
56
  * Location the type came from, for class references
57
57
  */
58
- source: string;
58
+ importName: string;
59
59
  /**
60
60
  * Does not include methods, used for shapes not concrete types
61
61
  */
@@ -151,7 +151,10 @@ export type AnyType = TupleType | ShapeType | UnionType | LiteralType | External
151
151
  /**
152
152
  * Simple interface for checked methods
153
153
  */
154
- export interface Checker {
154
+ export interface TransformResolver {
155
+ isKnownFile(file: string): boolean;
156
+ getFileImportName(file: string, removeExt?: boolean): string;
157
+ getTypeImportName(type: ts.Type, removeExt?: boolean): string | undefined;
155
158
  getAllTypeArguments(type: ts.Type): ts.Type[];
156
159
  getPropertiesOfType(type: ts.Type): ts.Symbol[];
157
160
  getTypeAsString(type: ts.Type): string | undefined;