@travetto/transformer
Advanced tools
Comparing version
{ | ||
"name": "@travetto/transformer", | ||
"displayName": "Transformation", | ||
"version": "3.0.0-rc.4", | ||
"version": "3.0.0-rc.6", | ||
"description": "Functionality for AST transformations, with transformer registration, and general utils", | ||
@@ -18,7 +17,7 @@ "keywords": [ | ||
"files": [ | ||
"index.ts", | ||
"__index__.ts", | ||
"src", | ||
"test-support" | ||
"support" | ||
], | ||
"main": "index.ts", | ||
"main": "__index__.ts", | ||
"repository": { | ||
@@ -29,4 +28,12 @@ "url": "https://github.com/travetto/travetto.git", | ||
"dependencies": { | ||
"@travetto/base": "^3.0.0-rc.2" | ||
"@travetto/manifest": "^3.0.0-rc.3", | ||
"tslib": "^2.4.1", | ||
"typescript": "^4.9.4" | ||
}, | ||
"travetto": { | ||
"displayName": "Transformation", | ||
"profiles": [ | ||
"compile" | ||
] | ||
}, | ||
"publishConfig": { | ||
@@ -33,0 +40,0 @@ "access": "public" |
<!-- This file was generated by @travetto/doc and should not be modified directly --> | ||
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/transformer/doc.ts and execute "npx trv doc" to rebuild --> | ||
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/transformer/DOC.ts and execute "npx trv doc" to rebuild --> | ||
# Transformation | ||
@@ -13,3 +13,3 @@ ## Functionality for AST transformations, with transformer registration, and general utils | ||
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. | ||
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. | ||
@@ -24,13 +24,11 @@ Because working with the [Typescript](https://typescriptlang.org) API can be delicate (and open to breaking changes), creating new transformers should be done cautiously. | ||
```typescript | ||
import * as ts from 'typescript'; | ||
import ts from 'typescript'; | ||
import { OnProperty, TransformerState, OnMethod, OnClass, TransformerId } from '@travetto/transformer'; | ||
import { OnProperty, TransformerState, OnMethod, OnClass } from '@travetto/transformer'; | ||
export class MakeUpper { | ||
static [TransformerId] = '@trv:transformer-test'; | ||
@OnProperty() | ||
static handleProperty(state: TransformerState, node: ts.PropertyDeclaration): ts.PropertyDeclaration { | ||
if (!state.source.fileName.includes('doc/src')) { | ||
if (!state.file.includes('doc/src')) { | ||
return node; | ||
@@ -40,3 +38,2 @@ } | ||
node, | ||
[], | ||
node.modifiers, | ||
@@ -52,3 +49,3 @@ node.name.getText().toUpperCase(), | ||
static handleClass(state: TransformerState, node: ts.ClassDeclaration): ts.ClassDeclaration { | ||
if (!state.source.fileName.includes('doc/src')) { | ||
if (!state.file.includes('doc/src')) { | ||
return node; | ||
@@ -58,3 +55,2 @@ } | ||
node, | ||
[], | ||
node.modifiers, | ||
@@ -70,3 +66,3 @@ state.createIdentifier(node.name!.getText().toUpperCase()), | ||
static handleMethod(state: TransformerState, node: ts.MethodDeclaration): ts.MethodDeclaration { | ||
if (!state.source.fileName.includes('doc/src')) { | ||
if (!state.file.includes('doc/src')) { | ||
return node; | ||
@@ -76,3 +72,2 @@ } | ||
node, | ||
[], | ||
node.modifiers, | ||
@@ -79,0 +74,0 @@ undefined, |
@@ -1,6 +0,4 @@ | ||
import * as ts from 'typescript'; | ||
import { basename, dirname, relative } from 'path'; | ||
import ts from 'typescript'; | ||
import { PathUtil } from '@travetto/boot'; | ||
import { ModuleUtil } from '@travetto/boot/src/internal/module-util'; | ||
import { PackageUtil, path } from '@travetto/manifest'; | ||
@@ -11,3 +9,8 @@ import { AnyType, ExternalType } from './resolver/types'; | ||
import { Import } from './types/shared'; | ||
import { LiteralUtil } from './util/literal'; | ||
import { DeclarationUtil } from './util/declaration'; | ||
import { TransformerIndex } from './manifest-index'; | ||
const D_OR_D_TS_EXT_RE = /[.]d([.]ts)?$/; | ||
/** | ||
@@ -22,14 +25,79 @@ * Manages imports within a ts.SourceFile | ||
#ids = new Map<string, string>(); | ||
#importName: string; | ||
#index: TransformerIndex; | ||
constructor(public source: ts.SourceFile, public factory: ts.NodeFactory) { | ||
constructor(public source: ts.SourceFile, public factory: ts.NodeFactory, index: TransformerIndex) { | ||
this.#imports = ImportUtil.collectImports(source); | ||
this.#index = index; | ||
this.#importName = index.getImportName(source.fileName); | ||
} | ||
#getImportFile(spec?: ts.Expression): string | undefined { | ||
if (spec && ts.isStringLiteral(spec)) { | ||
return spec.text.replace(/^['"]|["']$/g, ''); | ||
} | ||
} | ||
#rewriteModuleSpecifier(spec: ts.Expression | undefined): ts.Expression | undefined { | ||
const fileOrImport = this.#getImportFile(spec); | ||
if ( | ||
fileOrImport && | ||
(fileOrImport.startsWith('.') || this.#index.getFromImport(fileOrImport)) && | ||
!/[.]([mc]?js|ts|json)$/.test(fileOrImport) | ||
) { | ||
return LiteralUtil.fromLiteral(this.factory, `${fileOrImport}.js`); | ||
} | ||
return spec; | ||
} | ||
#rewriteImportClause( | ||
spec: ts.Expression | undefined, | ||
clause: ts.ImportClause | undefined, | ||
checker: ts.TypeChecker | ||
): ts.ImportClause | undefined { | ||
if (!(spec && clause?.namedBindings && ts.isNamedImports(clause.namedBindings))) { | ||
return clause; | ||
} | ||
const fileOrImport = this.#getImportFile(spec); | ||
if (!(fileOrImport && (fileOrImport.startsWith('.') || this.#index.getFromImport(fileOrImport)))) { | ||
return clause; | ||
} | ||
const bindings = clause.namedBindings; | ||
const newBindings: ts.ImportSpecifier[] = []; | ||
// Remove all type only imports | ||
for (const el of bindings.elements) { | ||
if (!el.isTypeOnly) { | ||
const type = checker.getTypeAtLocation(el.name); | ||
const objFlags = DeclarationUtil.getObjectFlags(type); | ||
const typeFlags = type.getFlags(); | ||
if (objFlags || typeFlags !== 1) { | ||
newBindings.push(el); | ||
} | ||
} | ||
} | ||
if (newBindings.length !== bindings.elements.length) { | ||
return this.factory.updateImportClause( | ||
clause, | ||
clause.isTypeOnly, | ||
clause.name, | ||
this.factory.createNamedImports(newBindings) | ||
); | ||
} else { | ||
return clause; | ||
} | ||
} | ||
/** | ||
* Produces a unique ID for a given file, importing if needed | ||
*/ | ||
getId(file: string): string { | ||
getId(file: string, name?: string): string { | ||
if (!this.#ids.has(file)) { | ||
const key = basename(file).replace(/[.][^.]*$/, '').replace(/[^A-Za-z0-9]+/g, '_'); | ||
this.#ids.set(file, `ᚕ_${key}_${this.#idx[key] = (this.#idx[key] || 0) + 1}`); | ||
if (name) { | ||
this.#ids.set(file, name); | ||
} else { | ||
const key = path.basename(file).replace(/[.][^.]*$/, '').replace(/[^A-Za-z0-9]+/g, '_'); | ||
this.#ids.set(file, `Ⲑ_${key}_${this.#idx[key] = (this.#idx[key] || 0) + 1}`); | ||
} | ||
} | ||
@@ -42,25 +110,13 @@ return this.#ids.get(file)!; | ||
*/ | ||
importFile(file: string, base?: string): Import { | ||
file = ModuleUtil.normalizePath(file); | ||
importFile(file: string, name?: string): Import { | ||
file = this.#index.getImportName(file); | ||
// Allow for node classes to be imported directly | ||
if (/@types\/node/.test(file)) { | ||
file = require.resolve(file.replace(/.*@types\/node\//, '').replace(/[.]d([.]ts)?$/, '')); | ||
file = PackageUtil.resolveImport(file.replace(/.*@types\/node\//, '').replace(D_OR_D_TS_EXT_RE, '')); | ||
} | ||
// Handle relative imports | ||
if (file.startsWith('.') && base && | ||
!base.startsWith('@travetto') && !base.includes('node_modules') | ||
) { // Relative path | ||
const fileDir = dirname(PathUtil.resolveUnix(file)); | ||
const baseDir = dirname(PathUtil.resolveUnix(base)); | ||
file = `${relative(baseDir, fileDir) || '.'}/${basename(file)}`; | ||
if (/^[A-Za-z]/.test(file)) { | ||
file = `./${file}`; | ||
} | ||
} | ||
if (!D_OR_D_TS_EXT_RE.test(file) && !this.#newImports.has(file)) { | ||
const id = this.getId(file, name); | ||
if (!/[.]d([.]ts)?$/.test(file) && !this.#newImports.has(file)) { | ||
const id = this.getId(file); | ||
if (this.#imports.has(id)) { // Already imported, be cool | ||
@@ -83,4 +139,4 @@ return this.#imports.get(id)!; | ||
for (const type of types) { | ||
if (type.key === 'external' && type.source && type.source !== this.source.fileName) { | ||
this.importFile(type.source, this.source.fileName); | ||
if (type.key === 'external' && type.importName && type.importName !== this.#importName) { | ||
this.importFile(type.importName); | ||
} | ||
@@ -106,7 +162,7 @@ switch (type.key) { | ||
try { | ||
const importStmts = [...this.#newImports.values()].map(({ path, ident }) => { | ||
const importStmts = [...this.#newImports.values()].map(({ path: resolved, ident }) => { | ||
const importStmt = this.factory.createImportDeclaration( | ||
undefined, undefined, | ||
undefined, | ||
this.factory.createImportClause(false, undefined, this.factory.createNamespaceImport(ident)), | ||
this.factory.createStringLiteral(path) | ||
this.factory.createStringLiteral(resolved) | ||
); | ||
@@ -124,3 +180,3 @@ return importStmt; | ||
} | ||
const out = new Error(`${err.message} in ${file.fileName.replace(PathUtil.cwd, '.')}`); | ||
const out = new Error(`${err.message} in ${file.fileName.replace(process.cwd(), '.')}`); | ||
out.stack = err.stack; | ||
@@ -131,7 +187,41 @@ throw out; | ||
finalizeImportExportExtension(ret: ts.SourceFile, checker: ts.TypeChecker): ts.SourceFile { | ||
const toAdd: ts.Statement[] = []; | ||
for (const stmt of ret.statements) { | ||
if (ts.isExportDeclaration(stmt)) { | ||
if (!stmt.isTypeOnly) { | ||
toAdd.push(this.factory.updateExportDeclaration( | ||
stmt, | ||
stmt.modifiers, | ||
stmt.isTypeOnly, | ||
stmt.exportClause, | ||
this.#rewriteModuleSpecifier(stmt.moduleSpecifier), | ||
stmt.assertClause | ||
)); | ||
} | ||
} else if (ts.isImportDeclaration(stmt)) { | ||
if (!stmt.importClause?.isTypeOnly) { | ||
toAdd.push(this.factory.updateImportDeclaration( | ||
stmt, | ||
stmt.modifiers, | ||
this.#rewriteImportClause(stmt.moduleSpecifier, stmt.importClause, checker)!, | ||
this.#rewriteModuleSpecifier(stmt.moduleSpecifier)!, | ||
stmt.assertClause | ||
)); | ||
} | ||
} else { | ||
toAdd.push(stmt); | ||
} | ||
} | ||
return CoreUtil.updateSource(this.factory, ret, toAdd); | ||
} | ||
/** | ||
* Reset the imports into the source file | ||
*/ | ||
finalize(ret: ts.SourceFile): ts.SourceFile { | ||
return this.finalizeNewImports(ret) ?? ret; | ||
finalize(ret: ts.SourceFile, checker: ts.TypeChecker): ts.SourceFile { | ||
ret = this.finalizeNewImports(ret) ?? ret; | ||
ret = this.finalizeImportExportExtension(ret, checker) ?? ret; | ||
return ret; | ||
} | ||
@@ -143,6 +233,6 @@ | ||
getOrImport(factory: ts.NodeFactory, type: ExternalType): ts.Identifier | ts.PropertyAccessExpression { | ||
if (type.source === this.source.fileName) { | ||
if (type.importName === this.#importName) { | ||
return factory.createIdentifier(type.name!); | ||
} else { | ||
const { ident } = this.#imports.get(type.source) ?? this.importFile(type.source, this.source.fileName); | ||
const { ident } = this.#imports.get(type.importName) ?? this.importFile(type.importName); | ||
return factory.createPropertyAccessExpression(ident, type.name!); | ||
@@ -149,0 +239,0 @@ } |
@@ -1,8 +0,13 @@ | ||
import * as ts from 'typescript'; | ||
import { DecoratorMeta, NodeTransformer, State, TransformPhase, TransformerType, Transformer, TransformerId } from './types/visitor'; | ||
import ts from 'typescript'; | ||
const HandlersProp = Symbol.for('@trv:transformer/handlers'); | ||
import { DecoratorMeta, NodeTransformer, State, TransformPhase, TransformerType, Transformer, ModuleNameⲐ } from './types/visitor'; | ||
const HandlersProp = Symbol.for('@travetto/transformer:handlers'); | ||
type TransformerWithHandlers = Transformer & { [HandlersProp]?: NodeTransformer[] }; | ||
function isTransformer(x: unknown): x is Transformer { | ||
return x !== null && x !== undefined && typeof x === 'function'; | ||
} | ||
/** | ||
@@ -12,4 +17,15 @@ * Get all transformers | ||
*/ | ||
export function getAllTransformers(obj: Record<string, { [HandlersProp]?: NodeTransformer[] }>): NodeTransformer[] { | ||
return Object.values(obj).flatMap(x => x[HandlersProp] ?? []); | ||
export function getAllTransformers(obj: Record<string, { [HandlersProp]?: NodeTransformer[] }>, module: string): NodeTransformer[] { | ||
return Object.values(obj) | ||
.flatMap(x => { | ||
if (isTransformer(x)) { | ||
x[ModuleNameⲐ] = module; | ||
} | ||
return (x[HandlersProp] ?? []); | ||
}) | ||
.map(handler => ({ | ||
...handler, | ||
key: `${module}:${handler.key}`, | ||
target: handler.target?.map(t => `${module}:${t}`) | ||
})); | ||
} | ||
@@ -19,11 +35,24 @@ | ||
function storeHandler(cls: TransformerWithHandlers, fn: Function, phase: TransformPhase, type: TransformerType, target?: string[]): void { | ||
if (target) { | ||
const ns = cls[TransformerId].split('/')[0]; // Everything before the '/' | ||
target = target.map(x => x.startsWith('@') ? x : `${ns}/${x}`); | ||
} | ||
cls[HandlersProp] = cls[HandlersProp] ?? []; | ||
cls[HandlersProp]!.push({ key: `${cls[TransformerId]}/${fn.name}`, [phase]: fn.bind(cls), type, target }); | ||
(cls[HandlersProp] ??= []).push({ key: fn.name, [phase]: fn.bind(cls), type, target }); | ||
} | ||
/** | ||
* Wraps entire file before transforming | ||
*/ | ||
export function OnFile(...target: string[]) { | ||
return <S extends State = State, R extends ts.Node = ts.Node>( | ||
inst: Transformer, __: unknown, d: TypedPropertyDescriptor<(state: S, node: ts.SourceFile) => R> | ||
): void => storeHandler(inst, d.value!, 'before', 'file', target); | ||
} | ||
/** | ||
* Wraps entire file after transforming | ||
*/ | ||
export function AfterFile(...target: string[]) { | ||
return <S extends State = State, R extends ts.Node = ts.Node>( | ||
inst: Transformer, __: unknown, d: TypedPropertyDescriptor<(state: S, node: ts.SourceFile) => R> | ||
): void => storeHandler(inst, d.value!, 'before', 'file', target); | ||
} | ||
/** | ||
* Listens for a `ts.CallExpression`, on descent | ||
@@ -30,0 +59,0 @@ */ |
/* eslint-disable no-bitwise */ | ||
import * as ts from 'typescript'; | ||
import { dirname } from 'path'; | ||
import ts from 'typescript'; | ||
import { PathUtil } from '@travetto/boot'; | ||
import { Util } from '@travetto/base'; | ||
import { ManifestIndex, path } from '@travetto/manifest'; | ||
import { DocUtil } from '../util/doc'; | ||
import { CoreUtil } from '../util/core'; | ||
import { DeclarationUtil } from '../util/declaration'; | ||
import { LiteralUtil } from '../util/literal'; | ||
import { Type, AnyType, UnionType, Checker } from './types'; | ||
import { DocUtil, CoreUtil, DeclarationUtil, LiteralUtil } from '../util'; | ||
import { CoerceUtil } from './coerce'; | ||
@@ -38,3 +41,3 @@ /** | ||
*/ | ||
export function TypeCategorize(checker: ts.TypeChecker, type: ts.Type): { category: Category, type: ts.Type } { | ||
export function TypeCategorize(checker: ts.TypeChecker, type: ts.Type, index: ManifestIndex): { category: Category, type: ts.Type } { | ||
const flags = type.getFlags(); | ||
@@ -66,5 +69,6 @@ const objectFlags = DeclarationUtil.getObjectFlags(type) ?? 0; | ||
const source = DeclarationUtil.getPrimaryDeclarationNode(resolvedType).getSourceFile(); | ||
if (source?.fileName.includes('@types/node/globals') || source?.fileName.includes('typescript/lib')) { | ||
const sourceFile = source.fileName; | ||
if (sourceFile?.includes('@types/node/globals') || sourceFile?.includes('typescript/lib')) { | ||
return { category: 'literal', type }; | ||
} else if (!source?.fileName.includes('@travetto') && source?.fileName.endsWith('.d.ts')) { | ||
} else if (sourceFile?.endsWith('.d.ts') && !index.getFromSource(sourceFile)) { | ||
return { category: 'unknown', type }; | ||
@@ -123,3 +127,3 @@ } else if (!resolvedType.isClass()) { // Not a real type | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
const ret = LiteralUtil.isLiteralType(type) ? Util.coerceType(type.value, cons as typeof String, false) : | ||
const ret = LiteralUtil.isLiteralType(type) ? CoerceUtil.coerce(type.value, cons as typeof String, false) : | ||
undefined; | ||
@@ -146,8 +150,6 @@ | ||
build: (checker, type) => { | ||
const source = DeclarationUtil.getPrimaryDeclarationNode(type).getSourceFile(); | ||
const name = CoreUtil.getSymbol(type)?.getName(); | ||
return { | ||
key: 'external', name, source: source.fileName, | ||
tsTypeArguments: checker.getAllTypeArguments(type) | ||
}; | ||
const importName = checker.getIndex().getImportName(type); | ||
const tsTypeArguments = checker.getAllTypeArguments(type); | ||
return { key: 'external', name, importName, tsTypeArguments }; | ||
} | ||
@@ -183,5 +185,6 @@ }, | ||
build: (checker, type, alias?) => { | ||
const fieldNodes: Record<string, ts.Type> = {}; | ||
const name = CoreUtil.getSymbol(alias ?? type); | ||
const source = DeclarationUtil.getPrimaryDeclarationNode(type)?.getSourceFile(); | ||
const tsFieldTypes: Record<string, ts.Type> = {}; | ||
const name = CoreUtil.getSymbol(alias ?? type)?.getName(); | ||
const importName = checker.getIndex().getImportName(type); | ||
const tsTypeArguments = checker.getAllTypeArguments(type); | ||
for (const member of checker.getPropertiesOfType(type)) { | ||
@@ -195,13 +198,7 @@ const dec = DeclarationUtil.getPrimaryDeclarationNode(member); | ||
) { | ||
fieldNodes[member.getName()] = memberType; | ||
tsFieldTypes[member.getName()] = memberType; | ||
} | ||
} | ||
} | ||
return { | ||
key: 'shape', name: name?.getName(), | ||
source: source?.fileName, | ||
tsFieldTypes: fieldNodes, | ||
tsTypeArguments: checker.getAllTypeArguments(type), | ||
fieldTypes: {} | ||
}; | ||
return { key: 'shape', name, importName, tsFieldTypes, tsTypeArguments, fieldTypes: {} }; | ||
} | ||
@@ -214,19 +211,22 @@ }, | ||
// eslint-disable-next-line prefer-const | ||
let [source, name, ext] = tag.split(':'); | ||
let [importName, name] = tag.split(':'); | ||
if (!name) { | ||
name = source; | ||
source = '.'; | ||
name = importName; | ||
importName = '.'; | ||
} | ||
const sourceFile: string = DeclarationUtil.getDeclarations(type) | ||
?.find(x => ts.getAllJSDocTags(x, (t): t is ts.JSDocTag => t.tagName.getText() === 'concrete').length) | ||
?.getSourceFile().fileName ?? ''; | ||
// Resolving relative to source file | ||
if (importName.startsWith('.')) { | ||
const rawSourceFile: string = DeclarationUtil.getDeclarations(type) | ||
?.find(x => ts.getAllJSDocTags(x, (t): t is ts.JSDocTag => t.tagName.getText() === 'concrete').length) | ||
?.getSourceFile().fileName ?? ''; | ||
if (source === '.') { | ||
source = sourceFile; | ||
} else if (source.startsWith('.')) { | ||
source = PathUtil.resolveUnix(dirname(sourceFile), source); | ||
if (importName === '.') { | ||
importName = checker.getIndex().getImportName(rawSourceFile); | ||
} else { | ||
const base = path.dirname(rawSourceFile); | ||
importName = checker.getIndex().getImportName(path.resolve(base, importName)); | ||
} | ||
} | ||
return { key: 'external', name, source: ext === 'node' ? source : PathUtil.resolveUnix(sourceFile, source) }; | ||
return { key: 'external', name, importName }; | ||
} | ||
@@ -233,0 +233,0 @@ } |
@@ -1,3 +0,3 @@ | ||
import * as ts from 'typescript'; | ||
import { AnyType } from './types'; | ||
import ts from 'typescript'; | ||
import type { AnyType } from './types'; | ||
@@ -4,0 +4,0 @@ /** |
@@ -1,7 +0,8 @@ | ||
import * as ts from 'typescript'; | ||
import ts from 'typescript'; | ||
import { AnyType, Checker } from './types'; | ||
import type { AnyType, Checker } from './types'; | ||
import { TypeCategorize, TypeBuilder } from './builder'; | ||
import { VisitCache } from './cache'; | ||
import { DocUtil } from '../util'; | ||
import { DocUtil } from '../util/doc'; | ||
import { TransformerIndex } from '../manifest-index'; | ||
@@ -13,5 +14,7 @@ /** | ||
#tsChecker: ts.TypeChecker; | ||
#index: TransformerIndex; | ||
constructor(tsChecker: ts.TypeChecker) { | ||
constructor(tsChecker: ts.TypeChecker, idx: TransformerIndex) { | ||
this.#tsChecker = tsChecker; | ||
this.#index = idx; | ||
} | ||
@@ -27,2 +30,6 @@ | ||
getIndex(): TransformerIndex { | ||
return this.#index; | ||
} | ||
/** | ||
@@ -78,3 +85,3 @@ * Get type from element | ||
const { category, type } = TypeCategorize(this.#tsChecker, resType); | ||
const { category, type } = TypeCategorize(this.#tsChecker, resType, this.#index); | ||
const { build, finalize } = TypeBuilder[category]; | ||
@@ -81,0 +88,0 @@ |
@@ -1,3 +0,5 @@ | ||
import type * as ts from 'typescript'; | ||
import type ts from 'typescript'; | ||
import { TransformerIndex } from '../manifest-index'; | ||
/** | ||
@@ -40,3 +42,3 @@ * Base type for a simplistic type structure | ||
*/ | ||
source: string; | ||
importName: string; | ||
/** | ||
@@ -59,3 +61,3 @@ * Type arguments | ||
*/ | ||
source: string; | ||
importName: string; | ||
/** | ||
@@ -161,2 +163,3 @@ * Does not include methods, used for shapes not concrete types | ||
getType(node: ts.Node): ts.Type; | ||
getIndex(): TransformerIndex; | ||
} |
152
src/state.ts
@@ -1,16 +0,18 @@ | ||
import * as ts from 'typescript'; | ||
import ts from 'typescript'; | ||
import { SystemUtil } from '@travetto/boot/src/internal/system'; | ||
import { ModuleUtil } from '@travetto/boot/src/internal/module-util'; | ||
import { Util } from '@travetto/base'; | ||
import { path } from '@travetto/manifest'; | ||
import { ExternalType, AnyType } from './resolver/types'; | ||
import { State, DecoratorMeta, Transformer, TransformerId } from './types/visitor'; | ||
import { State, DecoratorMeta, Transformer, ModuleNameⲐ } from './types/visitor'; | ||
import { TypeResolver } from './resolver/service'; | ||
import { ImportManager } from './importer'; | ||
import { Import } from './types/shared'; | ||
import { DocUtil } from './util/doc'; | ||
import { DecoratorUtil } from './util/decorator'; | ||
import { DeclarationUtil } from './util/declaration'; | ||
import { CoreUtil, LiteralUtil } from './util'; | ||
import { Import } from './types/shared'; | ||
import { CoreUtil } from './util/core'; | ||
import { LiteralUtil } from './util/literal'; | ||
import { SystemUtil } from './util/system'; | ||
import { TransformerIndex } from './manifest-index'; | ||
@@ -31,19 +33,31 @@ function hasOriginal(n: unknown): n is { original: ts.Node } { | ||
export class TransformerState implements State { | ||
static SYNTHETIC_EXT = 'ᚕsyn'; | ||
static SYNTHETIC_EXT = 'Ⲑsyn'; | ||
#resolver: TypeResolver; | ||
#imports: ImportManager; | ||
#index: TransformerIndex; | ||
#syntheticIdentifiers = new Map<string, ts.Identifier>(); | ||
#decorators = new Map<string, ts.PropertyAccessExpression>(); | ||
#options: ts.CompilerOptions; | ||
added = new Map<number, ts.Statement[]>(); | ||
module: string; | ||
importName: string; | ||
file: string; | ||
constructor(public source: ts.SourceFile, public factory: ts.NodeFactory, checker: ts.TypeChecker) { | ||
this.#imports = new ImportManager(source, factory); | ||
this.#resolver = new TypeResolver(checker); | ||
this.module = ModuleUtil.normalizePath(this.source.fileName); | ||
constructor(public source: ts.SourceFile, public factory: ts.NodeFactory, checker: ts.TypeChecker, index: TransformerIndex, options: ts.CompilerOptions) { | ||
this.#index = index; | ||
this.#imports = new ImportManager(source, factory, index); | ||
this.#resolver = new TypeResolver(checker, index); | ||
this.file = path.toPosix(this.source.fileName); | ||
this.importName = this.#index.getImportName(this.file, true); | ||
this.#options = options; | ||
} | ||
/** | ||
* Are we building ESM Output? | ||
*/ | ||
isEsmOutput(): boolean { | ||
return this.#options.module !== ts.ModuleKind.CommonJS; | ||
} | ||
/** | ||
* Allow access to resolver | ||
@@ -66,4 +80,4 @@ * @private | ||
*/ | ||
importFile(file: string): Import { | ||
return this.#imports.importFile(file); | ||
importFile(file: string, name?: string): Import { | ||
return this.#imports.importFile(file, name); | ||
} | ||
@@ -86,3 +100,5 @@ | ||
if (resolved.key !== 'external') { | ||
throw new Error(`Unable to import non-external type: ${node.getText()} ${resolved.key}: ${node.getSourceFile().fileName}`); | ||
const file = node.getSourceFile().fileName; | ||
const src = this.#index.getImportName(file); | ||
throw new Error(`Unable to import non-external type: ${node.getText()} ${resolved.key}: ${src}`); | ||
} | ||
@@ -147,3 +163,3 @@ return resolved; | ||
*/ | ||
getDecoratorMeta(dec: ts.Decorator): DecoratorMeta { | ||
getDecoratorMeta(dec: ts.Decorator): DecoratorMeta | undefined { | ||
const ident = DecoratorUtil.getDecoratorIdent(dec); | ||
@@ -153,14 +169,15 @@ const decl = DeclarationUtil.getPrimaryDeclarationNode( | ||
); | ||
const src = decl?.getSourceFile().fileName; | ||
const mod = src ? this.#index.getImportName(src, true) : undefined; | ||
const file = this.#index.getFromImport(mod ?? '')?.output; | ||
const targets = DocUtil.readAugments(this.#resolver.getType(ident)); | ||
const module = file ? mod : undefined; | ||
const name = ident ? | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
ident.escapedText! as string : | ||
undefined; | ||
return { | ||
dec, | ||
ident, | ||
file: decl?.getSourceFile().fileName, | ||
module: decl ? ModuleUtil.normalizePath(decl.getSourceFile().fileName) : undefined, // All #decorators will be absolute | ||
targets: DocUtil.readAugments(this.#resolver.getType(ident)), | ||
name: ident ? | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
ident.escapedText! as string : | ||
undefined | ||
}; | ||
if (ident && name) { | ||
return { dec, ident, file, module, targets, name }; | ||
} | ||
} | ||
@@ -174,3 +191,3 @@ | ||
.map(dec => this.getDecoratorMeta(dec)) | ||
.filter(x => !!x.ident) : []; | ||
.filter((x): x is DecoratorMeta => !!x) : []; | ||
} | ||
@@ -190,21 +207,26 @@ | ||
*/ | ||
addStatement(stmt: ts.Statement, before?: ts.Node): void { | ||
addStatements(added: ts.Statement[], before?: ts.Node | number): void { | ||
const stmts = this.source.statements.slice(0); | ||
let idx = stmts.length; | ||
let n = before; | ||
if (hasOriginal(n)) { | ||
n = n.original; | ||
let idx = stmts.length + 1000; | ||
if (before && typeof before !== 'number') { | ||
let n = before; | ||
if (hasOriginal(n)) { | ||
n = n.original; | ||
} | ||
while (n && !ts.isSourceFile(n.parent) && n !== n.parent) { | ||
n = n.parent; | ||
} | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
const nStmt: ts.Statement = n as ts.Statement; | ||
if (n && ts.isSourceFile(n.parent) && stmts.indexOf(nStmt) >= 0) { | ||
idx = stmts.indexOf(nStmt) - 1; | ||
} | ||
} else if (before !== undefined) { | ||
idx = before; | ||
} | ||
while (n && !ts.isSourceFile(n.parent)) { | ||
n = n.parent; | ||
} | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
const nStmt: ts.Statement = n as ts.Statement; | ||
if (n && ts.isSourceFile(n.parent) && stmts.indexOf(nStmt) >= 0) { | ||
idx = stmts.indexOf(nStmt) - 1; | ||
} | ||
if (!this.added.has(idx)) { | ||
this.added.set(idx, []); | ||
} | ||
this.added.get(idx)!.push(stmt); | ||
this.added.get(idx)!.push(...added); | ||
} | ||
@@ -216,3 +238,3 @@ | ||
finalize(ret: ts.SourceFile): ts.SourceFile { | ||
ret = this.#imports.finalize(ret); | ||
ret = this.#imports.finalize(ret, this.#resolver.getChecker()); | ||
return ret; | ||
@@ -222,11 +244,2 @@ } | ||
/** | ||
* Get Filename as ᚕsrc | ||
*/ | ||
getFilenameAsSrc(): ts.CallExpression { | ||
const ident = this.factory.createIdentifier('ᚕsrc'); | ||
ident.getSourceFile = (): ts.SourceFile => this.source; | ||
return this.factory.createCallExpression(ident, [], [this.createIdentifier('__filename')]); | ||
} | ||
/** | ||
* From literal | ||
@@ -255,3 +268,3 @@ */ | ||
*/ | ||
createAccess(first: string | ts.Expression, second: string | ts.Identifier, ...items: (string | ts.Identifier)[]): ts.PropertyAccessExpression { | ||
createAccess(first: string | ts.Expression, second: string | ts.Identifier, ...items: (string | number | ts.Identifier)[]): ts.Expression { | ||
return CoreUtil.createAccess(this.factory, first, second, ...items); | ||
@@ -276,2 +289,20 @@ } | ||
/** | ||
* Get filename identifier, regardless of module system | ||
*/ | ||
getFilenameIdentifier(): ts.Expression { | ||
return this.isEsmOutput() ? | ||
this.createAccess('import', 'meta', 'url') : | ||
this.createIdentifier('__filename'); | ||
} | ||
/** | ||
* Get the entry file identifier, supports both ESM and commonjs | ||
*/ | ||
getEntryFileIdentifier(): ts.Expression { | ||
return this.isEsmOutput() ? | ||
this.createAccess('process', 'argv', 1) : | ||
this.createAccess('require', 'main', 'filename'); | ||
} | ||
/** | ||
* Find decorator, relative to registered key | ||
@@ -283,6 +314,7 @@ * @param state | ||
*/ | ||
findDecorator(cls: Transformer, node: ts.Node, name: string, module?: string): ts.Decorator | undefined { | ||
const target = `${cls[TransformerId]}/${name}`; | ||
return this.getDecoratorList(node) | ||
.find(x => x.targets?.includes(target) && (!module || x.name === name && x.module === module))?.dec; | ||
findDecorator(mod: string | Transformer, node: ts.Node, name: string, module?: string): ts.Decorator | undefined { | ||
mod = typeof mod === 'string' ? mod : mod[ModuleNameⲐ]!; | ||
const target = `${mod}:${name}`; | ||
const list = this.getDecoratorList(node); | ||
return list.find(x => x.targets?.includes(target) && (!module || x.name === name && x.module === module))?.dec; | ||
} | ||
@@ -306,3 +338,3 @@ | ||
// Determine type unique ident | ||
unique = Util.uuid(type.name ? 5 : 10); | ||
unique = SystemUtil.uuid(type.name ? 5 : 10); | ||
} | ||
@@ -309,0 +341,0 @@ // Otherwise read name with uuid |
@@ -1,2 +0,2 @@ | ||
import * as ts from 'typescript'; | ||
import type { default as ts } from 'typescript'; | ||
@@ -3,0 +3,0 @@ /** |
@@ -1,2 +0,2 @@ | ||
import * as ts from 'typescript'; | ||
import ts from 'typescript'; | ||
@@ -17,3 +17,3 @@ /** | ||
source: ts.SourceFile; | ||
module: string; | ||
importName: string; | ||
added: Map<number, ts.Statement[]>; | ||
@@ -26,8 +26,10 @@ getDecoratorList(node: ts.Node): DecoratorMeta[]; | ||
export type TransformerType = 'class' | 'method' | 'property' | 'getter' | 'setter' | 'parameter' | 'static-method' | 'call' | 'function'; | ||
export type TransformerType = | ||
'class' | 'method' | 'property' | 'getter' | 'setter' | 'parameter' | | ||
'static-method' | 'call' | 'function' | 'file'; | ||
export const TransformerId = Symbol.for('@trv:transformer/id'); | ||
export const ModuleNameⲐ = Symbol.for('@travetto/transformer:id'); | ||
export type Transformer = { | ||
[TransformerId]: string; | ||
[ModuleNameⲐ]?: string; | ||
name: string; | ||
@@ -34,0 +36,0 @@ }; |
@@ -1,2 +0,2 @@ | ||
import * as ts from 'typescript'; | ||
import ts from 'typescript'; | ||
@@ -7,2 +7,3 @@ /** | ||
export class CoreUtil { | ||
/** | ||
@@ -55,3 +56,2 @@ * See if inbound node has an original property | ||
return factory.createPropertyDeclaration( | ||
undefined, | ||
[factory.createToken(ts.SyntaxKind.StaticKeyword)], | ||
@@ -93,9 +93,11 @@ name, undefined, undefined, val | ||
second: string | ts.Identifier, | ||
...items: (string | ts.Identifier)[] | ||
): ts.PropertyAccessExpression { | ||
...items: (string | number | ts.Identifier)[] | ||
): ts.Expression { | ||
if (typeof first === 'string') { | ||
first = factory.createIdentifier(first); | ||
} | ||
return items.reduce( | ||
(acc, p) => factory.createPropertyAccessExpression(acc, p), | ||
return items.reduce<ts.Expression>( | ||
(acc, p) => typeof p === 'number' ? | ||
factory.createElementAccessExpression(acc, p) : | ||
factory.createPropertyAccessExpression(acc, p), | ||
factory.createPropertyAccessExpression(first, second) | ||
@@ -102,0 +104,0 @@ ); |
@@ -1,2 +0,2 @@ | ||
import * as ts from 'typescript'; | ||
import ts from 'typescript'; | ||
import { CoreUtil } from './core'; | ||
@@ -3,0 +3,0 @@ |
@@ -1,2 +0,2 @@ | ||
import * as ts from 'typescript'; | ||
import ts from 'typescript'; | ||
import { CoreUtil } from './core'; | ||
@@ -3,0 +3,0 @@ |
@@ -1,2 +0,2 @@ | ||
import * as ts from 'typescript'; | ||
import ts from 'typescript'; | ||
@@ -3,0 +3,0 @@ import { DeclDocumentation } from '../types/shared'; |
@@ -1,5 +0,4 @@ | ||
import * as ts from 'typescript'; | ||
import { resolve as pathResolve } from 'path'; | ||
import ts from 'typescript'; | ||
import { PathUtil } from '@travetto/boot'; | ||
import { PackageUtil, path } from '@travetto/manifest'; | ||
@@ -16,5 +15,13 @@ import { Import } from '../types/shared'; | ||
static optionalResolve(file: string, base?: string): string { | ||
file = base ? pathResolve(base, file) : file; | ||
if (base?.endsWith('.ts')) { | ||
base = path.dirname(base); | ||
} | ||
if (base && file.startsWith('.')) { | ||
return path.resolve(base, file); | ||
// TODO: Replace with manifest reverse lookup | ||
} else if (file.startsWith('@')) { | ||
return path.resolve('node_modules', file); | ||
} | ||
try { | ||
return require.resolve(file); | ||
return PackageUtil.resolveImport(file); | ||
} catch { | ||
@@ -29,4 +36,4 @@ return file; | ||
static collectImports(src: ts.SourceFile): Map<string, Import> { | ||
const pth = require.resolve(src.fileName); | ||
const base = PathUtil.toUnix(pth); | ||
// TODO: Replace with manifest reverse lookup | ||
const base = path.toPosix(src.fileName); | ||
@@ -37,3 +44,3 @@ const imports = new Map<string, Import>(); | ||
if (ts.isImportDeclaration(stmt) && ts.isStringLiteral(stmt.moduleSpecifier)) { | ||
const path = this.optionalResolve(stmt.moduleSpecifier.text, base); | ||
const resolved = this.optionalResolve(stmt.moduleSpecifier.text, base); | ||
@@ -44,6 +51,6 @@ if (stmt.importClause) { | ||
if (ts.isNamespaceImport(bindings)) { | ||
imports.set(bindings.name.text, { path, ident: bindings.name, stmt }); | ||
imports.set(bindings.name.text, { path: resolved, ident: bindings.name, stmt }); | ||
} else if (ts.isNamedImports(bindings)) { | ||
for (const n of bindings.elements) { | ||
imports.set(n.name.text, { path, ident: n.name, stmt }); | ||
imports.set(n.name.text, { path: resolved, ident: n.name, stmt }); | ||
} | ||
@@ -50,0 +57,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import * as ts from 'typescript'; | ||
import ts from 'typescript'; | ||
@@ -51,3 +51,4 @@ /** | ||
const pairs: ts.PropertyAssignment[] = []; | ||
for (const k of Object.keys(ov)) { | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
for (const k of Object.keys(ov) as (keyof typeof ov)[]) { | ||
if (ov[k] !== undefined) { | ||
@@ -54,0 +55,0 @@ pairs.push( |
@@ -1,3 +0,1 @@ | ||
import { Util } from '@travetto/base'; | ||
const exclude = new Set([ | ||
@@ -25,3 +23,3 @@ 'parent', 'checker', 'end', 'pos', 'id', 'source', 'sourceFile', 'getSourceFile', | ||
static collapseNode(x: unknown, cache: Set<unknown> = new Set()): unknown { | ||
if (!x || Util.isPrimitive(x)) { | ||
if (!x || !(typeof x === 'object' || typeof x === 'function')) { | ||
return x; | ||
@@ -43,3 +41,3 @@ } | ||
for (const key of Object.keys(ox)) { | ||
if (Util.isFunction(ox[key]) || exclude.has(key) || ox[key] === undefined) { | ||
if (Object.getPrototypeOf(ox[key]) === Function.prototype || exclude.has(key) || ox[key] === undefined) { | ||
continue; | ||
@@ -46,0 +44,0 @@ } |
@@ -1,9 +0,4 @@ | ||
import * as ts from 'typescript'; | ||
import { createWriteStream } from 'fs'; | ||
import ts from 'typescript'; | ||
import { ConsoleManager } from '@travetto/base/src/console'; | ||
import { AppCache } from '@travetto/boot'; | ||
import { DecoratorMeta, TransformerType, NodeTransformer, TransformerSet, State, TransformPhase } from './types/visitor'; | ||
import { LogUtil } from './util/log'; | ||
import { CoreUtil } from './util/core'; | ||
@@ -31,3 +26,3 @@ | ||
return 'parameter'; | ||
} else if (ts.isFunctionDeclaration(node) || (ts.isFunctionExpression(node) && !ts.isArrowFunction(node))) { | ||
} else if ((ts.isFunctionDeclaration(node) && node.body) || (ts.isFunctionExpression(node) && !ts.isArrowFunction(node))) { | ||
return 'function'; | ||
@@ -38,2 +33,4 @@ } else if (ts.isGetAccessor(node)) { | ||
return 'setter'; | ||
} else if (ts.isSourceFile(node)) { | ||
return 'file'; | ||
} | ||
@@ -43,12 +40,8 @@ } | ||
#transformers = new Map<TransformerType, TransformerSet<S>>(); | ||
#logTarget: string; | ||
#getState: (context: ts.TransformationContext, src: ts.SourceFile) => S; | ||
#logger: Console | undefined; | ||
constructor( | ||
getState: (context: ts.TransformationContext, src: ts.SourceFile) => S, | ||
transformers: NodeTransformer<S, TransformerType, ts.Node>[], | ||
logTarget = 'compiler.log' | ||
transformers: NodeTransformer<S, TransformerType, ts.Node>[] | ||
) { | ||
this.#logTarget = logTarget; | ||
this.#getState = getState; | ||
@@ -85,10 +78,2 @@ this.#init(transformers); | ||
get logger(): Console { | ||
this.#logger ??= new console.Console({ | ||
stdout: createWriteStream(AppCache.toEntryName(this.#logTarget), { flags: 'a' }), | ||
inspectOptions: { depth: 4 }, | ||
}); | ||
return this.#logger; | ||
} | ||
/** | ||
@@ -99,9 +84,7 @@ * Produce a visitor for a given a file | ||
return (context: ts.TransformationContext) => (file: ts.SourceFile): ts.SourceFile => { | ||
if (!file.fileName.endsWith('.ts')) { // Skip all non-ts files | ||
return file; | ||
} | ||
try { | ||
const c = this.logger; | ||
ConsoleManager.set({ | ||
onLog: (level, ctx, args) => c[level](level, ctx, ...LogUtil.collapseNodes(args)) | ||
}); | ||
console.debug('Processing', { file: file.fileName, pid: process.pid }); | ||
const state = this.#getState(context, file); | ||
@@ -114,3 +97,3 @@ let ret = this.visit(state, context, file); | ||
while (state.added.size) { | ||
for (const [k, all] of [...state.added]) { | ||
for (const [k, all] of [...state.added].sort(([idxA], [idxB]) => idxB - idxA)) { | ||
const idx = k === -1 ? state.added.size : k; | ||
@@ -134,8 +117,6 @@ statements = [ | ||
} | ||
console.error('Failed transforming', { error: `${err.message}\n${err.stack}`, file: file.fileName }); | ||
console!.error('Failed transforming', { error: `${err.message}\n${err.stack}`, file: file.fileName }); | ||
const out = new Error(`Failed transforming: ${file.fileName}: ${err.message}`); | ||
out.stack = err.stack; | ||
throw out; | ||
} finally { | ||
ConsoleManager.clear(); // Reset logging | ||
} | ||
@@ -142,0 +123,0 @@ }; |
85749
14.07%25
8.7%2229
15.25%3
200%80
-5.88%+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed