diff --git a/packages/type-compiler/src/compiler.ts b/packages/type-compiler/src/compiler.ts index 520f59107..f62df4dbb 100644 --- a/packages/type-compiler/src/compiler.ts +++ b/packages/type-compiler/src/compiler.ts @@ -7,9 +7,8 @@ * * You should have received a copy of the MIT License along with this program. */ - +import { knownLibFilesForCompilerOptions } from '@typescript/vfs'; import type { - __String, ArrayTypeNode, ArrowFunction, Block, @@ -22,9 +21,9 @@ import type { CompilerOptions, ConciseBody, ConditionalTypeNode, + ConstructSignatureDeclaration, ConstructorDeclaration, ConstructorTypeNode, - ConstructSignatureDeclaration, CustomTransformer, CustomTransformerFactory, Declaration, @@ -38,8 +37,8 @@ import type { FunctionTypeNode, Identifier, ImportDeclaration, - IndexedAccessTypeNode, IndexSignatureDeclaration, + IndexedAccessTypeNode, InferTypeNode, InterfaceDeclaration, IntersectionTypeNode, @@ -71,31 +70,43 @@ import type { TypeQueryNode, TypeReferenceNode, UnionTypeNode, + __String, } from 'typescript'; - import ts from 'typescript'; +import { MappedModifier, ReflectionOp, TypeNumberBrand } from '@deepkit/type-spec'; + import { + Matcher, + ReflectionConfig, + ReflectionConfigCache, + getResolver, + loadReflectionConfig, + reflectionModeMatcher, +} from './config.js'; +import { debug } from './debug.js'; +import { External, ExternalLibraryImport } from './external.js'; +import { + NodeConverter, + PackExpression, ensureImportIsEmitted, extractJSDocAttribute, findSourceFile, + getEntityName, + getExternalRuntimeTypeName, getGlobalsOfSourceFile, getIdentifierName, getNameAsString, getPropertyName, + getRuntimeTypeName, hasModifier, + hasSourceFile, + isBuiltType, isNodeWithLocals, - NodeConverter, - PackExpression, serializeEntityNameAsExpression, } from './reflection-ast.js'; -import { SourceFile } from './ts-types.js'; -import { MappedModifier, ReflectionOp, TypeNumberBrand } from '@deepkit/type-spec'; import { Resolver } from './resolver.js'; -import { knownLibFilesForCompilerOptions } from '@typescript/vfs'; -import { debug } from './debug.js'; -import { getResolver, loadReflectionConfig, Matcher, ReflectionConfig, ReflectionConfigCache, reflectionModeMatcher } from './config.js'; - +import { SourceFile } from './ts-types.js'; const { visitEachChild, @@ -162,7 +173,9 @@ export function encodeOps(ops: ReflectionOp[]): string { return ops.map(v => String.fromCharCode(v + 33)).join(''); } -function filterUndefined(object: { [name: string]: any }): { [name: string]: any } { +function filterUndefined(object: { [name: string]: any }): { + [name: string]: any; +} { return Object.fromEntries(Object.entries(object).filter(([, v]) => v !== undefined)); } @@ -207,7 +220,11 @@ const OPs: { [op in ReflectionOp]?: { params: number } } = { [ReflectionOp.implements]: { params: 1 }, }; -export function debugPackStruct(sourceFile: SourceFile, forType: Node, pack: { ops: ReflectionOp[], stack: PackExpression[] }): void { +export function debugPackStruct( + sourceFile: SourceFile, + forType: Node, + pack: { ops: ReflectionOp[]; stack: PackExpression[] }, +): void { const items: any[] = []; for (let i = 0; i < pack.ops.length; i++) { @@ -235,13 +252,17 @@ export function debugPackStruct(sourceFile: SourceFile, forType: Node, pack: { o } interface Frame { - variables: { name: string, index: number }[], + variables: { name: string; index: number }[]; opIndex: number; conditional?: true; previous?: Frame; } -function findVariable(frame: Frame, name: string, frameOffset: number = 0): { frameOffset: number, stackIndex: number } | undefined { +function findVariable( + frame: Frame, + name: string, + frameOffset: number = 0, +): { frameOffset: number; stackIndex: number } | undefined { const variable = frame.variables.find(v => v.name === name); if (variable) { return { frameOffset, stackIndex: variable.index }; @@ -273,8 +294,10 @@ class CompilerProgram { protected activeCoRoutines: { ops: ReflectionOp[] }[] = []; protected coRoutines: { ops: ReflectionOp[] }[] = []; - constructor(public forNode: Node, public sourceFile: SourceFile) { - } + constructor( + public forNode: Node, + public sourceFile: SourceFile, + ) {} buildPackStruct() { const ops: ReflectionOp[] = [...this.ops]; @@ -388,7 +411,9 @@ class CompilerProgram { */ pushFrame(implicit: boolean = false) { if (!implicit) this.pushOp(ReflectionOp.frame); - const opIndex = this.activeCoRoutines.length ? this.activeCoRoutines[this.activeCoRoutines.length - 1].ops.length : this.ops.length; + const opIndex = this.activeCoRoutines.length + ? this.activeCoRoutines[this.activeCoRoutines.length - 1].ops.length + : this.ops.length; this.frame = { previous: this.frame, variables: [], opIndex }; return this.frame; } @@ -420,7 +445,10 @@ class CompilerProgram { } pushTemplateParameter(name: string, withDefault: boolean = false): number { - this.pushOp(withDefault ? ReflectionOp.typeParameterDefault : ReflectionOp.typeParameter, this.findOrAddStackEntry(name)); + this.pushOp( + withDefault ? ReflectionOp.typeParameterDefault : ReflectionOp.typeParameter, + this.findOrAddStackEntry(name), + ); this.frame.variables.push({ index: this.frame.variables.length, name, @@ -438,7 +466,12 @@ function getAssignTypeExpression(call: Expression): Expression | undefined { call = call.expression; } - if (isCallExpression(call) && isIdentifier(call.expression) && getIdentifierName(call.expression) === '__assignType' && call.arguments.length > 0) { + if ( + isCallExpression(call) && + isIdentifier(call.expression) && + getIdentifierName(call.expression) === '__assignType' && + call.arguments.length > 0 + ) { return call.arguments[0]; } @@ -451,9 +484,14 @@ function getReceiveTypeParameter(type: TypeNode): TypeReferenceNode | undefined const rfn = getReceiveTypeParameter(t); if (rfn) return rfn; } - } else if (isTypeReferenceNode(type) && isIdentifier(type.typeName) - && getIdentifierName(type.typeName) === 'ReceiveType' && !!type.typeArguments - && type.typeArguments.length === 1) return type; + } else if ( + isTypeReferenceNode(type) && + isIdentifier(type.typeName) && + getIdentifierName(type.typeName) === 'ReceiveType' && + !!type.typeArguments && + type.typeArguments.length === 1 + ) + return type; return; } @@ -493,22 +531,23 @@ export class ReflectionTransformer implements CustomTransformer { */ protected compileDeclarations = new Map< TypeAliasDeclaration | InterfaceDeclaration | EnumDeclaration, - { name: EntityName, sourceFile: SourceFile, compiled?: Statement[] } + { name: EntityName; sourceFile: SourceFile; compiled?: Statement[] } >(); /** * Types added to this map will get a type program at the top root level of the program. * This is for imported types, which need to be inlined into the current file, as we do not emit type imports (TS will omit them). */ - protected embedDeclarations = new Map(); + protected embedDeclarations = new Map(); /** * When a node was embedded or compiled (from the maps above), we store it here to know to not add it again. */ protected compiledDeclarations = new Set(); - protected addImports: { from: Expression, identifier: Identifier }[] = []; + protected addImports: { from: Expression; identifier: Identifier }[] = []; + protected external: External; protected nodeConverter: NodeConverter; protected typeChecker?: TypeChecker; protected resolver: Resolver; @@ -527,10 +566,9 @@ export class ReflectionTransformer implements CustomTransformer { constructor( protected context: TransformationContext, - protected cache: Cache = new Cache, + protected cache: Cache = new Cache(), ) { this.f = context.factory; - this.nodeConverter = new NodeConverter(this.f); //it is important to not have undefined values like {paths: undefined} because it would override the read tsconfig.json this.compilerOptions = filterUndefined(context.getCompilerOptions()); // compilerHost has no internal cache and is cheap to build, so no cache needed. @@ -541,11 +579,19 @@ export class ReflectionTransformer implements CustomTransformer { useCaseSensitiveFileNames: true, fileExists: (path: string) => this.host.fileExists(path), readFile: (path: string) => this.host.readFile(path), - readDirectory: (path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number) => { + readDirectory: ( + path: string, + extensions?: readonly string[], + exclude?: readonly string[], + include?: readonly string[], + depth?: number, + ) => { if (!this.host.readDirectory) return []; return this.host.readDirectory(path, extensions || [], exclude, include || [], depth); }, }; + this.external = new External(this.resolver); + this.nodeConverter = new NodeConverter(this.f, this.external); } forHost(host: CompilerHost): this { @@ -558,7 +604,11 @@ export class ReflectionTransformer implements CustomTransformer { withReflection(config: ReflectionConfig): this { this.overriddenReflectionMatcher = (path: string) => { const mode = reflectionModeMatcher(config, path); - return { mode, tsConfigPath: '' }; + return { + mode, + tsConfigPath: '', + inlineExternalLibraryImports: config.inlineExternalLibraryImports, + }; }; return this; } @@ -592,8 +642,13 @@ export class ReflectionTransformer implements CustomTransformer { return loadReflectionConfig(this.cache.resolver, this.parseConfigHost, this.compilerOptions, sourceFile); } - transformSourceFile(sourceFile: SourceFile): SourceFile { + protected setSourceFile(sourceFile: SourceFile) { this.sourceFile = sourceFile; + this.external.sourceFile = sourceFile; + } + + transformSourceFile(sourceFile: SourceFile): SourceFile { + this.setSourceFile(sourceFile); //if it's not a TS/TSX file, we do not transform it if (sourceFile.scriptKind !== ScriptKind.TS && sourceFile.scriptKind !== ScriptKind.TSX) return sourceFile; @@ -607,7 +662,9 @@ export class ReflectionTransformer implements CustomTransformer { const reflection = this.getReflectionConfig(sourceFile); if (reflection.mode === 'never') { - debug(`Transform file with reflection=${reflection.mode} (${this.getModuleType()}) ${sourceFile.fileName} via config ${reflection.tsConfigPath || 'none'}.`); + debug( + `Transform file with reflection=${reflection.mode} (${this.getModuleType()}) ${sourceFile.fileName} via config ${reflection.tsConfigPath || 'none'}.`, + ); return sourceFile; } @@ -618,16 +675,20 @@ export class ReflectionTransformer implements CustomTransformer { if (sourceFile.kind !== SyntaxKind.SourceFile) { if ('undefined' === typeof require) { - throw new Error(`Invalid TypeScript library imported. SyntaxKind different ${sourceFile.kind} !== ${SyntaxKind.SourceFile}.`); + throw new Error( + `Invalid TypeScript library imported. SyntaxKind different ${sourceFile.kind} !== ${SyntaxKind.SourceFile}.`, + ); } const path = require.resolve('typescript'); - throw new Error(`Invalid TypeScript library imported. SyntaxKind different ${sourceFile.kind} !== ${SyntaxKind.SourceFile}. typescript package path: ${path}`); + throw new Error( + `Invalid TypeScript library imported. SyntaxKind different ${sourceFile.kind} !== ${SyntaxKind.SourceFile}. typescript package path: ${path}`, + ); } const visitor = (node: Node): any => { node = visitEachChild(node, visitor, this.context); - if ((isInterfaceDeclaration(node) || isTypeAliasDeclaration(node) || isEnumDeclaration(node))) { + if (isInterfaceDeclaration(node) || isTypeAliasDeclaration(node) || isEnumDeclaration(node)) { if (this.isWithReflection(sourceFile, node)) { this.compileDeclarations.set(node, { name: node.name, @@ -642,12 +703,18 @@ export class ReflectionTransformer implements CustomTransformer { //so that __type can be added. //{default(){}} can not be converted without losing the function name, so we skip that for the moment. let valid = true; - if (node.name.kind === SyntaxKind.Identifier && getIdentifierName(node.name) === 'default') valid = false; + if (node.name.kind === SyntaxKind.Identifier && getIdentifierName(node.name) === 'default') + valid = false; if (valid) { const method = this.decorateFunctionExpression( this.f.createFunctionExpression( - node.modifiers as ReadonlyArray, node.asteriskToken, isIdentifier(node.name) ? node.name : undefined, - node.typeParameters, node.parameters, node.type, node.body, + node.modifiers as ReadonlyArray, + node.asteriskToken, + isIdentifier(node.name) ? node.name : undefined, + node.typeParameters, + node.parameters, + node.type, + node.body, ), ); node = this.f.createPropertyAssignment(node.name, method); @@ -658,7 +725,9 @@ export class ReflectionTransformer implements CustomTransformer { return this.decorateClass(sourceFile, node); } else if (isParameter(node) && node.parent && node.type) { // ReceiveType - const typeParameters = isConstructorDeclaration(node.parent) ? node.parent.parent.typeParameters : node.parent.typeParameters; + const typeParameters = isConstructorDeclaration(node.parent) + ? node.parent.parent.typeParameters + : node.parent.typeParameters; if (!typeParameters) return node; const receiveType = getReceiveTypeParameter(node.type); @@ -674,20 +743,32 @@ export class ReflectionTransformer implements CustomTransformer { const next = this.getArrowFunctionΩPropertyAccessIdentifier(node.parent); if (!next) return node; container = next; - } else if ((isFunctionDeclaration(node.parent) || isFunctionExpression(node.parent)) && node.parent.name) { + } else if ( + (isFunctionDeclaration(node.parent) || isFunctionExpression(node.parent)) && + node.parent.name + ) { container = node.parent.name; } else if (isMethodDeclaration(node.parent) && isIdentifier(node.parent.name)) { - container = this.f.createPropertyAccessExpression(this.f.createIdentifier('this'), node.parent.name); + container = this.f.createPropertyAccessExpression( + this.f.createIdentifier('this'), + node.parent.name, + ); } else if (isConstructorDeclaration(node.parent)) { - container = this.f.createPropertyAccessExpression(this.f.createIdentifier('this'), 'constructor'); + container = this.f.createPropertyAccessExpression( + this.f.createIdentifier('this'), + 'constructor', + ); } - return this.f.updateParameterDeclaration(node, node.modifiers as ReadonlyArray, node.dotDotDotToken, node.name, - node.questionToken, receiveType, this.f.createElementAccessChain( - this.f.createPropertyAccessExpression( - container, - this.f.createIdentifier('Ω'), - ), + return this.f.updateParameterDeclaration( + node, + node.modifiers as ReadonlyArray, + node.dotDotDotToken, + node.name, + node.questionToken, + receiveType, + this.f.createElementAccessChain( + this.f.createPropertyAccessExpression(container, this.f.createIdentifier('Ω')), this.f.createToken(SyntaxKind.QuestionDotToken), this.f.createNumericLiteral(index), ), @@ -704,11 +785,17 @@ export class ReflectionTransformer implements CustomTransformer { return this.injectResetΩ(node); } else if (isArrowFunction(node)) { return this.decorateArrowFunction(this.injectResetΩ(node)); - } else if ((isNewExpression(node) || isCallExpression(node)) && node.typeArguments && node.typeArguments.length > 0) { - + } else if ( + (isNewExpression(node) || isCallExpression(node)) && + node.typeArguments && + node.typeArguments.length > 0 + ) { if (isCallExpression(node)) { const autoTypeFunctions = ['valuesOf', 'propertiesOf', 'typeOf']; - if (isIdentifier(node.expression) && autoTypeFunctions.includes(getIdentifierName(node.expression))) { + if ( + isIdentifier(node.expression) && + autoTypeFunctions.includes(getIdentifierName(node.expression)) + ) { const args: Expression[] = [...node.arguments]; if (!args.length) { @@ -720,7 +807,12 @@ export class ReflectionTransformer implements CustomTransformer { if (!type) return node; args.push(type); - return this.f.updateCallExpression(node, node.expression, node.typeArguments, this.f.createNodeArray(args)); + return this.f.updateCallExpression( + node, + node.expression, + node.typeArguments, + this.f.createNodeArray(args), + ); } } @@ -767,27 +859,29 @@ export class ReflectionTransformer implements CustomTransformer { this.f.createArrayLiteralExpression(typeExpressions), ); - return update(node, + return update( + node, this.f.createPropertyAccessExpression( - this.f.createParenthesizedExpression(this.f.createBinaryExpression( + this.f.createParenthesizedExpression( this.f.createBinaryExpression( this.f.createBinaryExpression( - r, - this.f.createToken(ts.SyntaxKind.EqualsToken), - node.expression.expression, + this.f.createBinaryExpression( + r, + this.f.createToken(ts.SyntaxKind.EqualsToken), + node.expression.expression, + ), + this.f.createToken(ts.SyntaxKind.CommaToken), + assignQ, ), this.f.createToken(ts.SyntaxKind.CommaToken), - assignQ, + r, ), - this.f.createToken(ts.SyntaxKind.CommaToken), - r, - )), + ), node.expression.name, ), node.typeArguments, node.arguments, ); - } else if (isParenthesizedExpression(node.expression.expression)) { //e.g. (http.deep()).response(); //only work necessary when `http.deep()` is using type args and was converted to: @@ -841,16 +935,14 @@ export class ReflectionTransformer implements CustomTransformer { } //(fn.Ω = [], call()) - return this.f.createParenthesizedExpression(this.f.createBinaryExpression( - assignQ, - this.f.createToken(SyntaxKind.CommaToken), - node, - )); + return this.f.createParenthesizedExpression( + this.f.createBinaryExpression(assignQ, this.f.createToken(SyntaxKind.CommaToken), node), + ); } return node; }; - this.sourceFile = visitNode(this.sourceFile, visitor); + this.setSourceFile(visitNode(this.sourceFile, visitor)); const newTopStatements: Statement[] = []; @@ -862,7 +954,13 @@ export class ReflectionTransformer implements CustomTransformer { break; } - if (this.embedDeclarations.size === 0 && allCompiled) break; + if ( + this.embedDeclarations.size === 0 && + this.external.compileExternalLibraryImports.size === 0 && + allCompiled + ) { + break; + } for (const [node, d] of [...this.compileDeclarations.entries()]) { if (d.compiled) continue; @@ -879,13 +977,53 @@ export class ReflectionTransformer implements CustomTransformer { newTopStatements.push(...this.createProgramVarFromNode(node, d.name, d.sourceFile)); } } + + if (this.external.compileExternalLibraryImports.size) { + for (const imports of this.external.compileExternalLibraryImports.values()) { + for (const { declaration } of imports.values()) { + this.compiledDeclarations.add(declaration); + } + } + const entries = Array.from(this.external.compileExternalLibraryImports.entries()); + this.external.compileExternalLibraryImports.clear(); + for (const [library, imports] of entries) { + if (!this.external.embeddedLibraryVariables.has(library)) { + const objectLiteral = this.f.createObjectLiteralExpression(); + const variableDeclaration = this.f.createVariableDeclaration( + this.f.createIdentifier(getExternalRuntimeTypeName(library)), + undefined, + undefined, + objectLiteral, + ); + const variableStatement = this.f.createVariableStatement( + [], + this.f.createVariableDeclarationList([variableDeclaration], NodeFlags.Const), + ); + newTopStatements.unshift(variableStatement); + this.external.embeddedLibraryVariables.add(library); + } + + for (const value of imports.values()) { + this.external.startEmbeddingExternalLibraryImport(value); + newTopStatements.push( + this.createProgramVarForExternalLibraryImport( + value.declaration, + value.name, + value.sourceFile, + value.module.packageId.name, + ), + ); + this.external.finishEmbeddingExternalLibraryImport(); + } + } + } } //externalize type aliases const compileDeclarations = (node: Node): any => { node = visitEachChild(node, compileDeclarations, this.context); - if ((isTypeAliasDeclaration(node) || isInterfaceDeclaration(node) || isEnumDeclaration(node))) { + if (isTypeAliasDeclaration(node) || isInterfaceDeclaration(node) || isEnumDeclaration(node)) { const d = this.compileDeclarations.get(node); if (!d) { return node; @@ -899,7 +1037,8 @@ export class ReflectionTransformer implements CustomTransformer { return node; }; - this.sourceFile = visitNode(this.sourceFile, compileDeclarations); + + this.setSourceFile(visitNode(this.sourceFile, compileDeclarations)); if (this.addImports.length) { const handledIdentifier: string[] = []; @@ -909,11 +1048,24 @@ export class ReflectionTransformer implements CustomTransformer { if (this.getModuleType() === 'cjs') { //var {identifier} = require('./bar') const test = this.f.createIdentifier(getIdentifierName(imp.identifier)); - const variable = this.f.createVariableStatement(undefined, this.f.createVariableDeclarationList([this.f.createVariableDeclaration( - this.f.createObjectBindingPattern([this.f.createBindingElement(undefined, undefined, test)]), - undefined, undefined, - this.f.createCallExpression(this.f.createIdentifier('require'), undefined, [imp.from]), - )], NodeFlags.Const)); + const variable = this.f.createVariableStatement( + undefined, + this.f.createVariableDeclarationList( + [ + this.f.createVariableDeclaration( + this.f.createObjectBindingPattern([ + this.f.createBindingElement(undefined, undefined, test), + ]), + undefined, + undefined, + this.f.createCallExpression(this.f.createIdentifier('require'), undefined, [ + imp.from, + ]), + ), + ], + NodeFlags.Const, + ), + ); const typeDeclWithComment = addSyntheticLeadingComment( variable, SyntaxKind.MultiLineCommentTrivia, @@ -927,8 +1079,10 @@ export class ReflectionTransformer implements CustomTransformer { // that's probably a bit unstable. const specifier = this.f.createImportSpecifier(false, undefined, imp.identifier); const namedImports = this.f.createNamedImports([specifier]); - const importStatement = this.f.createImportDeclaration(undefined, - this.f.createImportClause(false, undefined, namedImports), imp.from, + const importStatement = this.f.createImportDeclaration( + undefined, + this.f.createImportClause(false, undefined, namedImports), + imp.from, ); const typeDeclWithComment = addSyntheticLeadingComment( importStatement, @@ -968,14 +1122,16 @@ export class ReflectionTransformer implements CustomTransformer { undefined, //this.f.createKeywordTypeNode(SyntaxKind.AnyKeyword), this.f.createBlock( [ - this.f.createExpressionStatement(this.f.createBinaryExpression( - this.f.createPropertyAccessExpression( - this.f.createIdentifier('fn'), - this.f.createIdentifier('__type'), + this.f.createExpressionStatement( + this.f.createBinaryExpression( + this.f.createPropertyAccessExpression( + this.f.createIdentifier('fn'), + this.f.createIdentifier('__type'), + ), + this.f.createToken(SyntaxKind.EqualsToken), + this.f.createIdentifier('args'), ), - this.f.createToken(SyntaxKind.EqualsToken), - this.f.createIdentifier('args'), - )), + ), this.f.createReturnStatement(this.f.createIdentifier('fn')), ], true, @@ -989,12 +1145,7 @@ export class ReflectionTransformer implements CustomTransformer { this.f.createVariableStatement( undefined, this.f.createVariableDeclarationList( - [this.f.createVariableDeclaration( - this.tempResultIdentifier, - undefined, - undefined, - undefined, - )], + [this.f.createVariableDeclaration(this.tempResultIdentifier, undefined, undefined, undefined)], ts.NodeFlags.None, ), ), @@ -1003,27 +1154,35 @@ export class ReflectionTransformer implements CustomTransformer { if (newTopStatements.length) { // we want to keep "use strict", or "use client", etc at the very top - const indexOfFirstLiteralExpression = this.sourceFile.statements.findIndex(v => isExpressionStatement(v) && isStringLiteral(v.expression)); - - const newStatements = indexOfFirstLiteralExpression === -1 - ? [...newTopStatements, ...this.sourceFile.statements] - : [ - ...this.sourceFile.statements.slice(0, indexOfFirstLiteralExpression + 1), - ...newTopStatements, - ...this.sourceFile.statements.slice(indexOfFirstLiteralExpression + 1), - ]; - this.sourceFile = this.f.updateSourceFile(this.sourceFile, newStatements); + const indexOfFirstLiteralExpression = this.sourceFile.statements.findIndex( + v => isExpressionStatement(v) && isStringLiteral(v.expression), + ); + + const newStatements = + indexOfFirstLiteralExpression === -1 + ? [...newTopStatements, ...this.sourceFile.statements] + : [ + ...this.sourceFile.statements.slice(0, indexOfFirstLiteralExpression + 1), + ...newTopStatements, + ...this.sourceFile.statements.slice(indexOfFirstLiteralExpression + 1), + ]; + this.setSourceFile(this.f.updateSourceFile(this.sourceFile, newStatements)); // this.sourceFile = this.f.updateSourceFile(this.sourceFile, [...newTopStatements, ...this.sourceFile.statements]); } // console.log(createPrinter().printNode(EmitHint.SourceFile, this.sourceFile, this.sourceFile)); const took = Date.now() - start; - debug(`Transform file with reflection=${reflection.mode} took ${took}ms (${this.getModuleType()}) ${sourceFile.fileName} via config ${reflection.tsConfigPath || 'none'}.`); + debug( + `Transform file with reflection=${reflection.mode} took ${took}ms (${this.getModuleType()}) ${sourceFile.fileName} via config ${reflection.tsConfigPath || 'none'}.`, + ); return this.sourceFile; } protected getModuleType(): 'cjs' | 'esm' { - if (this.compilerOptions.module === ts.ModuleKind.Node16 || this.compilerOptions.module === ts.ModuleKind.NodeNext) { + if ( + this.compilerOptions.module === ts.ModuleKind.Node16 || + this.compilerOptions.module === ts.ModuleKind.NodeNext + ) { if (this.sourceFile.impliedNodeFormat === ts.ModuleKind.ESNext) { return 'esm'; } @@ -1056,7 +1215,9 @@ export class ReflectionTransformer implements CustomTransformer { return; } - protected injectResetΩ(node: T): T { + protected injectResetΩ< + T extends FunctionDeclaration | FunctionExpression | MethodDeclaration | ConstructorDeclaration | ArrowFunction, + >(node: T): T { let hasReceiveType = false; for (const param of node.parameters) { if (param.type && getReceiveTypeParameter(param.type)) hasReceiveType = true; @@ -1076,34 +1237,83 @@ export class ReflectionTransformer implements CustomTransformer { container = this.f.createPropertyAccessExpression(this.f.createIdentifier('this'), 'constructor'); } - const reset: Statement = this.f.createExpressionStatement(this.f.createBinaryExpression( - this.f.createPropertyAccessExpression( - container, - this.f.createIdentifier('Ω'), + const reset: Statement = this.f.createExpressionStatement( + this.f.createBinaryExpression( + this.f.createPropertyAccessExpression(container, this.f.createIdentifier('Ω')), + this.f.createToken(ts.SyntaxKind.EqualsToken), + this.f.createIdentifier('undefined'), ), - this.f.createToken(ts.SyntaxKind.EqualsToken), - this.f.createIdentifier('undefined'), - )); - const body = node.body ? this.f.updateBlock(node.body as Block, [reset, ...(node.body as Block).statements]) : undefined; + ); + const body = node.body + ? this.f.updateBlock(node.body as Block, [reset, ...(node.body as Block).statements]) + : undefined; if (isArrowFunction(node)) { - return this.f.updateArrowFunction(node, node.modifiers, node.typeParameters, node.parameters, node.type, node.equalsGreaterThanToken, body as ConciseBody) as T; + return this.f.updateArrowFunction( + node, + node.modifiers, + node.typeParameters, + node.parameters, + node.type, + node.equalsGreaterThanToken, + body as ConciseBody, + ) as T; } else if (isFunctionDeclaration(node)) { - return this.f.updateFunctionDeclaration(node, node.modifiers, node.asteriskToken, node.name, - node.typeParameters, node.parameters, node.type, body) as T; + return this.f.updateFunctionDeclaration( + node, + node.modifiers, + node.asteriskToken, + node.name, + node.typeParameters, + node.parameters, + node.type, + body, + ) as T; } else if (isFunctionExpression(node)) { - return this.f.updateFunctionExpression(node, node.modifiers, node.asteriskToken, node.name, - node.typeParameters, node.parameters, node.type, body || node.body) as T; + return this.f.updateFunctionExpression( + node, + node.modifiers, + node.asteriskToken, + node.name, + node.typeParameters, + node.parameters, + node.type, + body || node.body, + ) as T; } else if (isMethodDeclaration(node)) { - return this.f.updateMethodDeclaration(node, node.modifiers as ReadonlyArray, node.asteriskToken, node.name, - node.questionToken, node.typeParameters, node.parameters, node.type, body) as T; + return this.f.updateMethodDeclaration( + node, + node.modifiers as ReadonlyArray, + node.asteriskToken, + node.name, + node.questionToken, + node.typeParameters, + node.parameters, + node.type, + body, + ) as T; } else if (isConstructorDeclaration(node)) { return this.f.updateConstructorDeclaration(node, node.modifiers, node.parameters, body) as T; } return node; } - protected createProgramVarFromNode(node: Node, name: EntityName, sourceFile: SourceFile): Statement[] { + protected createProgramVarForExternalLibraryImport( + node: Node, + name: EntityName, + sourceFile: SourceFile, + libraryName: string, + ): Statement { + const typeProgramExpression = this.createTypeProgramExpression(node, sourceFile); + const left = this.f.createPropertyAccessExpression( + this.f.createIdentifier(getExternalRuntimeTypeName(libraryName)), + getNameAsString(name), + ); + const assignment = this.f.createAssignment(left, typeProgramExpression!); + return this.f.createExpressionStatement(assignment); + } + + protected createTypeProgramExpression(node: Node, sourceFile: SourceFile): Expression | undefined { const typeProgram = new CompilerProgram(node, sourceFile); if ((isTypeAliasDeclaration(node) || isInterfaceDeclaration(node)) && node.typeParameters) { @@ -1116,24 +1326,40 @@ export class ReflectionTransformer implements CustomTransformer { } } - this.extractPackStructOfType(node, typeProgram); + if (isTypeAliasDeclaration(node)) { + this.extractPackStructOfType(node.type, typeProgram); + } else { + this.extractPackStructOfType(node, typeProgram); + } - if (isTypeAliasDeclaration(node) || isInterfaceDeclaration(node) || isClassDeclaration(node) || isClassExpression(node)) { + if ( + isTypeAliasDeclaration(node) || + isInterfaceDeclaration(node) || + isClassDeclaration(node) || + isClassExpression(node) + ) { typeProgram.pushOp(ReflectionOp.nominal); } - const typeProgramExpression = this.packOpsAndStack(typeProgram); + return this.packOpsAndStack(typeProgram); + } + + protected createProgramVarFromNode(node: Node, name: EntityName, sourceFile: SourceFile): Statement[] { + const typeProgramExpression = this.createTypeProgramExpression(node, sourceFile); const variable = this.f.createVariableStatement( [], - this.f.createVariableDeclarationList([ - this.f.createVariableDeclaration( - this.getDeclarationVariableName(name), - undefined, - undefined, - typeProgramExpression, - ), - ], NodeFlags.Const), + this.f.createVariableDeclarationList( + [ + this.f.createVariableDeclaration( + this.getDeclarationVariableName(name), + undefined, + undefined, + typeProgramExpression, + ), + ], + NodeFlags.Const, + ), ); //when its commonJS, the `variable` would be exported as `exports.$name = $value`, but all references point just to $name. @@ -1142,16 +1368,27 @@ export class ReflectionTransformer implements CustomTransformer { //propertyName in ExportSpecifier is set to avoid a TS compile error: // TypeError: Cannot read properties of undefined (reading 'escapedText') // at Object.idText (/Users/marc/bude/deepkit-framework/packages/benchmark/node_modules/typescript/lib/typescript.js:11875:67) - const exportNode = this.f.createExportDeclaration(undefined, false, this.f.createNamedExports([ - this.f.createExportSpecifier(false, this.getDeclarationVariableName(name), this.getDeclarationVariableName(name)), - ])); + const exportNode = this.f.createExportDeclaration( + undefined, + false, + this.f.createNamedExports([ + this.f.createExportSpecifier( + false, + this.getDeclarationVariableName(name), + this.getDeclarationVariableName(name), + ), + ]), + ); return [variable, exportNode]; } return [variable]; } - protected extractPackStructOfType(node: Node | Declaration | ClassDeclaration | ClassExpression, program: CompilerProgram): void { + protected extractPackStructOfType( + node: Node | Declaration | ClassDeclaration | ClassExpression, + program: CompilerProgram, + ): void { if (isParenthesizedTypeNode(node)) return this.extractPackStructOfType(node.type, program); switch (node.kind) { @@ -1242,7 +1479,14 @@ export class ReflectionTransformer implements CustomTransformer { } } const index = program.pushStack( - this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, this.nodeConverter.toExpression(extendType.expression)), + this.f.createArrowFunction( + undefined, + undefined, + [], + undefined, + undefined, + this.nodeConverter.toExpression(extendType.expression), + ), ); program.pushOp(ReflectionOp.classReference, index); program.popFrameImplicit(); @@ -1363,7 +1607,9 @@ export class ReflectionTransformer implements CustomTransformer { case SyntaxKind.TypeAliasDeclaration: { const narrowed = node as TypeAliasDeclaration; this.extractPackStructOfType(narrowed.type, program); - if (narrowed.name) this.resolveTypeName(getIdentifierName(narrowed.name), program); + if (narrowed.name) { + this.resolveTypeName(getIdentifierName(narrowed.name), program); + } break; } case SyntaxKind.TypeLiteral: @@ -1391,7 +1637,8 @@ export class ReflectionTransformer implements CustomTransformer { if (isTypeLiteralNode(narrowed)) { descriptionNode = narrowed.parent; } - const description = descriptionNode && extractJSDocAttribute(this.sourceFile, descriptionNode, 'description'); + const description = + descriptionNode && extractJSDocAttribute(this.sourceFile, descriptionNode, 'description'); if (description) program.pushOp(ReflectionOp.description, program.findOrAddStackEntry(description)); if (isInterfaceDeclaration(narrowed)) { @@ -1484,10 +1731,20 @@ export class ReflectionTransformer implements CustomTransformer { if (narrowed.initializer) { //important to use Function, since it will be called using a different `this` - program.pushOp(ReflectionOp.defaultValue, program.findOrAddStackEntry( - this.f.createFunctionExpression(undefined, undefined, undefined, undefined, undefined, undefined, - this.f.createBlock([this.f.createReturnStatement(narrowed.initializer)])), - )); + program.pushOp( + ReflectionOp.defaultValue, + program.findOrAddStackEntry( + this.f.createFunctionExpression( + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + this.f.createBlock([this.f.createReturnStatement(narrowed.initializer)]), + ), + ), + ); } const description = extractJSDocAttribute(this.sourceFile, narrowed, 'description'); @@ -1499,12 +1756,13 @@ export class ReflectionTransformer implements CustomTransformer { //TypeScript does not narrow types down const narrowed = node as ConditionalTypeNode; - // Depending on whether this a distributive conditional type or not, it has to be moved to its own function // my understanding of when a distributive conditional type is used is: // 1. the `checkType` is a simple identifier (just `T`, no `[T]`, no `T | x`, no `{a: T}`, etc) - const distributiveOverIdentifier: Identifier | undefined = isTypeReferenceNode(narrowed.checkType) && isIdentifier(narrowed.checkType.typeName) - ? narrowed.checkType.typeName : undefined; + const distributiveOverIdentifier: Identifier | undefined = + isTypeReferenceNode(narrowed.checkType) && isIdentifier(narrowed.checkType.typeName) + ? narrowed.checkType.typeName + : undefined; if (distributiveOverIdentifier) { program.pushFrame(); @@ -1571,14 +1829,27 @@ export class ReflectionTransformer implements CustomTransformer { case SyntaxKind.CallSignature: case SyntaxKind.FunctionDeclaration: { //TypeScript does not narrow types down - const narrowed = node as MethodSignature | MethodDeclaration | CallSignatureDeclaration | ConstructorTypeNode - | ConstructSignatureDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression | FunctionTypeNode | FunctionDeclaration; + const narrowed = node as + | MethodSignature + | MethodDeclaration + | CallSignatureDeclaration + | ConstructorTypeNode + | ConstructSignatureDeclaration + | ConstructorDeclaration + | ArrowFunction + | FunctionExpression + | FunctionTypeNode + | FunctionDeclaration; if (!this.isWithReflection(program.sourceFile, narrowed)) return; const name = isCallSignatureDeclaration(node) - ? '' : isConstructorTypeNode(narrowed) || isConstructSignatureDeclaration(node) - ? 'new' : isConstructorDeclaration(narrowed) ? 'constructor' : getPropertyName(this.f, narrowed.name); + ? '' + : isConstructorTypeNode(narrowed) || isConstructSignatureDeclaration(node) + ? 'new' + : isConstructorDeclaration(narrowed) + ? 'constructor' + : getPropertyName(this.f, narrowed.name); if (!narrowed.type && narrowed.parameters.length === 0 && !name) return; program.pushFrame(); @@ -1587,7 +1858,10 @@ export class ReflectionTransformer implements CustomTransformer { const parameterName = isIdentifier(parameter.name) ? getNameAsString(parameter.name) : 'param' + i; const type = parameter.type - ? (parameter.dotDotDotToken && isArrayTypeNode(parameter.type) ? parameter.type.elementType : parameter.type) : undefined; + ? parameter.dotDotDotToken && isArrayTypeNode(parameter.type) + ? parameter.type.elementType + : parameter.type + : undefined; if (type) { this.extractPackStructOfType(type, program); @@ -1611,7 +1885,16 @@ export class ReflectionTransformer implements CustomTransformer { if (parameter.initializer && parameter.type && !getReceiveTypeParameter(parameter.type)) { program.pushOp( ReflectionOp.defaultValue, - program.findOrAddStackEntry(this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, parameter.initializer)), + program.findOrAddStackEntry( + this.f.createArrowFunction( + undefined, + undefined, + [], + undefined, + undefined, + parameter.initializer, + ), + ), ); } } @@ -1623,11 +1906,14 @@ export class ReflectionTransformer implements CustomTransformer { } program.pushOp( - isCallSignatureDeclaration(node) ? ReflectionOp.callSignature : - isMethodSignature(narrowed) || isConstructSignatureDeclaration(narrowed) - ? ReflectionOp.methodSignature - : isMethodDeclaration(narrowed) || isConstructorDeclaration(narrowed) - ? ReflectionOp.method : ReflectionOp.function, program.findOrAddStackEntry(name), + isCallSignatureDeclaration(node) + ? ReflectionOp.callSignature + : isMethodSignature(narrowed) || isConstructSignatureDeclaration(narrowed) + ? ReflectionOp.methodSignature + : isMethodDeclaration(narrowed) || isConstructorDeclaration(narrowed) + ? ReflectionOp.method + : ReflectionOp.function, + program.findOrAddStackEntry(name), ); if (isMethodDeclaration(narrowed)) { @@ -1705,7 +1991,16 @@ export class ReflectionTransformer implements CustomTransformer { if (type.initializer) { program.pushOp( ReflectionOp.defaultValue, - program.findOrAddStackEntry(this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, type.initializer)), + program.findOrAddStackEntry( + this.f.createArrowFunction( + undefined, + undefined, + [], + undefined, + undefined, + type.initializer, + ), + ), ); } } @@ -1744,15 +2039,29 @@ export class ReflectionTransformer implements CustomTransformer { // this.addImports.push({ identifier: narrowed.exprName, from: originImportStatement.moduleSpecifier }); // } // } + let expression: Expression = serializeEntityNameAsExpression(this.f, narrowed.exprName); if (isIdentifier(narrowed.exprName)) { const resolved = this.resolveDeclaration(narrowed.exprName); - if (resolved && findSourceFile(resolved.declaration) !== this.sourceFile && resolved.importDeclaration) { - ensureImportIsEmitted(resolved.importDeclaration, narrowed.exprName); + if ( + resolved && + findSourceFile(resolved.declaration) !== this.sourceFile && + resolved.importDeclaration + ) { + expression = this.resolveImportExpression( + resolved.declaration, + resolved.importDeclaration, + narrowed.exprName, + expression, + ); } } - const expression = serializeEntityNameAsExpression(this.f, narrowed.exprName); - program.pushOp(ReflectionOp.typeof, program.pushStack(this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, expression))); + program.pushOp( + ReflectionOp.typeof, + program.pushStack( + this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, expression), + ), + ); break; } case SyntaxKind.TypeOperator: { @@ -1812,23 +2121,23 @@ export class ReflectionTransformer implements CustomTransformer { } protected knownClasses: { [name: string]: ReflectionOp } = { - 'Int8Array': ReflectionOp.int8Array, - 'Uint8Array': ReflectionOp.uint8Array, - 'Uint8ClampedArray': ReflectionOp.uint8ClampedArray, - 'Int16Array': ReflectionOp.int16Array, - 'Uint16Array': ReflectionOp.uint16Array, - 'Int32Array': ReflectionOp.int32Array, - 'Uint32Array': ReflectionOp.uint32Array, - 'Float32Array': ReflectionOp.float32Array, - 'Float64Array': ReflectionOp.float64Array, - 'ArrayBuffer': ReflectionOp.arrayBuffer, - 'BigInt64Array': ReflectionOp.bigInt64Array, - 'Date': ReflectionOp.date, - 'RegExp': ReflectionOp.regexp, - 'String': ReflectionOp.string, - 'Number': ReflectionOp.number, - 'BigInt': ReflectionOp.bigint, - 'Boolean': ReflectionOp.boolean, + Int8Array: ReflectionOp.int8Array, + Uint8Array: ReflectionOp.uint8Array, + Uint8ClampedArray: ReflectionOp.uint8ClampedArray, + Int16Array: ReflectionOp.int16Array, + Uint16Array: ReflectionOp.uint16Array, + Int32Array: ReflectionOp.int32Array, + Uint32Array: ReflectionOp.uint32Array, + Float32Array: ReflectionOp.float32Array, + Float64Array: ReflectionOp.float64Array, + ArrayBuffer: ReflectionOp.arrayBuffer, + BigInt64Array: ReflectionOp.bigInt64Array, + Date: ReflectionOp.date, + RegExp: ReflectionOp.regexp, + String: ReflectionOp.string, + Number: ReflectionOp.number, + BigInt: ReflectionOp.bigint, + Boolean: ReflectionOp.boolean, }; protected getGlobalLibs(): SourceFile[] { @@ -1841,14 +2150,17 @@ export class ReflectionTransformer implements CustomTransformer { //currently knownLibFilesForCompilerOptions from @typescript/vfs doesn't return correct lib files for esnext, //so we switch here to es2022 if bigger than es2022. const options = { ...this.compilerOptions }; - if (options.target && (options.target === ScriptTarget.ESNext)) { + if (options.target && options.target === ScriptTarget.ESNext) { options.target = ScriptTarget.ES2022; } const libs = knownLibFilesForCompilerOptions(options, ts); for (const lib of libs) { if (this.isExcluded(lib)) continue; - const sourceFile = this.resolver.resolveSourceFile(this.sourceFile, this.f.createStringLiteral('typescript/lib/' + lib.replace('.d.ts', ''))); + const sourceFile = this.resolver.resolveSourceFile( + this.sourceFile, + this.f.createStringLiteral('typescript/lib/' + lib.replace('.d.ts', '')), + ); if (!sourceFile) continue; this.cache.globalSourceFiles.push(sourceFile); } @@ -1859,11 +2171,17 @@ export class ReflectionTransformer implements CustomTransformer { * This is a custom resolver based on populated `locals` from the binder. It uses a custom resolution algorithm since * we have no access to the binder/TypeChecker directly and instantiating a TypeChecker per file/transformer is incredible slow. */ - protected resolveDeclaration(typeName: EntityName): { declaration: Node, importDeclaration?: ImportDeclaration, typeOnly?: boolean } | void { + protected resolveDeclaration(typeName: EntityName): { + declaration: Node; + importDeclaration?: ImportDeclaration; + sourceFile: SourceFile; + typeOnly: boolean; + } | void { let current: Node = typeName.parent; if (typeName.kind === SyntaxKind.QualifiedName) return; //namespace access not supported yet, e.g. type a = Namespace.X; let declaration: Node | undefined = undefined; + let sourceFile: SourceFile = this.sourceFile; while (current) { if (isNodeWithLocals(current) && current.locals) { @@ -1906,18 +2224,29 @@ export class ReflectionTransformer implements CustomTransformer { } if (importDeclaration) { - if (importDeclaration.importClause && importDeclaration.importClause.isTypeOnly) typeOnly = true; + if (importDeclaration.importClause && importDeclaration.importClause.isTypeOnly) { + typeOnly = true; + } declaration = this.resolveImportSpecifier(typeName.escapedText, importDeclaration, this.sourceFile); + //might be an external library + if (!declaration && hasSourceFile(importDeclaration)) { + sourceFile = importDeclaration.getSourceFile(); + declaration = this.resolveImportSpecifier(typeName.escapedText, importDeclaration, sourceFile); + } } - if (declaration && declaration.kind === SyntaxKind.TypeParameter && declaration.parent.kind === SyntaxKind.TypeAliasDeclaration) { + if ( + declaration && + declaration.kind === SyntaxKind.TypeParameter && + declaration.parent.kind === SyntaxKind.TypeAliasDeclaration + ) { //for alias like `type MyAlias = T`, `T` is returned from `typeChecker.getDeclaredTypeOfSymbol(symbol)`. declaration = declaration.parent as TypeAliasDeclaration; } if (!declaration) return; - return { declaration, importDeclaration, typeOnly }; + return { declaration, importDeclaration, typeOnly, sourceFile }; } // protected resolveType(node: TypeNode): Declaration | Node { @@ -1940,17 +2269,74 @@ export class ReflectionTransformer implements CustomTransformer { // return node; // } + protected getRuntimeTypeName(typeName: EntityName): Identifier { + return this.f.createIdentifier(getRuntimeTypeName(getNameAsString(typeName))); + } + + protected getExternalRuntimeTypeName( + typeName: EntityName, + externalLibraryImport?: ExternalLibraryImport, + ): Identifier { + const { expression, name } = this.nodeConverter.createExternalRuntimeTypePropertyAccessExpression( + getNameAsString(typeName), + externalLibraryImport, + ); + return this.f.createIdentifier(`${getNameAsString(expression as any)}.${getNameAsString(name)}`); + } + protected getDeclarationVariableName(typeName: EntityName): Identifier { - if (isIdentifier(typeName)) { - return this.f.createIdentifier('__Ω' + getIdentifierName(typeName)); + return this.external.isEmbeddingExternalLibraryImport() && + !this.external.knownGlobalTypeNames.has(getNameAsString(typeName)) + ? this.getExternalRuntimeTypeName(typeName) + : this.getRuntimeTypeName(typeName); + } + + protected shouldEmbedExternalLibraryImport(importDeclaration: ImportDeclaration, typeName: Identifier): boolean { + if (this.external.isEmbeddingExternalLibraryImport()) return true; + + const runtimeTypeName = this.getRuntimeTypeName(typeName); + const sourceFile = importDeclaration.getSourceFile(); + const builtType = isBuiltType(runtimeTypeName, sourceFile); + + return ( + !builtType && + this.external.shouldInlineExternalLibraryImport( + importDeclaration, + typeName, + this.getReflectionConfig(sourceFile), + ) + ); + } + + protected resolveImportExpression( + declaration: Node, + importDeclaration: ImportDeclaration, + typeName: Identifier, + expression: Expression, + ): Expression { + ensureImportIsEmitted(importDeclaration, typeName); + + if (!hasSourceFile(importDeclaration)) return expression; + + // these will be inferred at runtime + if (isTypeAliasDeclaration(declaration) || isVariableDeclaration(declaration)) { + return expression; } - function joinQualifiedName(name: EntityName): string { - if (isIdentifier(name)) return getIdentifierName(name); - return joinQualifiedName(name.left) + '_' + getIdentifierName(name.right); + if (this.shouldEmbedExternalLibraryImport(importDeclaration, typeName)) { + const { module } = this.external.processExternalLibraryImport( + typeName, + declaration, + importDeclaration.getSourceFile(), + importDeclaration, + ); + return this.f.createPropertyAccessExpression( + this.f.createIdentifier(getExternalRuntimeTypeName(module.packageId.name)), + getNameAsString(typeName), + ); } - return this.f.createIdentifier('__Ω' + joinQualifiedName(typeName)); + return expression; } /** @@ -1964,20 +2350,37 @@ export class ReflectionTransformer implements CustomTransformer { return res === 'never'; } - protected extractPackStructOfTypeReference(type: TypeReferenceNode | ExpressionWithTypeArguments, program: CompilerProgram): void { - const typeName: EntityName | undefined = isTypeReferenceNode(type) ? type.typeName : (isIdentifier(type.expression) ? type.expression : undefined); + protected extractPackStructOfTypeReference( + type: TypeReferenceNode | ExpressionWithTypeArguments, + program: CompilerProgram, + ): void { + const typeName: EntityName | undefined = isTypeReferenceNode(type) + ? type.typeName + : isIdentifier(type.expression) + ? type.expression + : undefined; if (!typeName) { program.pushOp(ReflectionOp.any); return; } - if (isIdentifier(typeName) && getIdentifierName(typeName) === 'InlineRuntimeType' && type.typeArguments && type.typeArguments[0] && isTypeQueryNode(type.typeArguments[0])) { + if ( + isIdentifier(typeName) && + getIdentifierName(typeName) === 'InlineRuntimeType' && + type.typeArguments && + type.typeArguments[0] && + isTypeQueryNode(type.typeArguments[0]) + ) { const expression = serializeEntityNameAsExpression(this.f, type.typeArguments[0].exprName); program.pushOp(ReflectionOp.arg, program.pushStack(expression)); return; } - if (isIdentifier(typeName) && getIdentifierName(typeName) !== 'constructor' && this.knownClasses[getIdentifierName(typeName)]) { + if ( + isIdentifier(typeName) && + getIdentifierName(typeName) !== 'constructor' && + this.knownClasses[getIdentifierName(typeName)] + ) { const name = getIdentifierName(typeName); const op = this.knownClasses[name]; program.pushOp(op); @@ -1991,7 +2394,11 @@ export class ReflectionTransformer implements CustomTransformer { program.pushOp(ReflectionOp.promise); } else if (isIdentifier(typeName) && getIdentifierName(typeName) === 'integer') { program.pushOp(ReflectionOp.numberBrand, TypeNumberBrand.integer as number); - } else if (isIdentifier(typeName) && getIdentifierName(typeName) !== 'constructor' && TypeNumberBrand[getIdentifierName(typeName) as any] !== undefined) { + } else if ( + isIdentifier(typeName) && + getIdentifierName(typeName) !== 'constructor' && + TypeNumberBrand[getIdentifierName(typeName) as any] !== undefined + ) { program.pushOp(ReflectionOp.numberBrand, TypeNumberBrand[getIdentifierName(typeName) as any] as any); } else { //check if it references a variable @@ -2019,14 +2426,27 @@ export class ReflectionTransformer implements CustomTransformer { for (const member of resolved.declaration.members) { if (getNameAsString(member.name) === getNameAsString(typeName.right)) { if (member.initializer) { - program.pushOp(ReflectionOp.arg, program.pushStack(this.nodeConverter.toExpression(member.initializer))); + program.pushOp( + ReflectionOp.arg, + program.pushStack(this.nodeConverter.toExpression(member.initializer)), + ); } else if (lastExpression) { const exp = this.nodeConverter.toExpression(lastExpression); - program.pushOp(ReflectionOp.arg, program.pushStack( - this.f.createBinaryExpression(exp, SyntaxKind.PlusToken, this.nodeConverter.toExpression(indexValue)), - )); + program.pushOp( + ReflectionOp.arg, + program.pushStack( + this.f.createBinaryExpression( + exp, + SyntaxKind.PlusToken, + this.nodeConverter.toExpression(indexValue), + ), + ), + ); } else { - program.pushOp(ReflectionOp.arg, program.pushStack(this.nodeConverter.toExpression(indexValue))); + program.pushOp( + ReflectionOp.arg, + program.pushStack(this.nodeConverter.toExpression(indexValue)), + ); } return; } else { @@ -2057,14 +2477,28 @@ export class ReflectionTransformer implements CustomTransformer { } if (isModuleDeclaration(declaration) && resolved.importDeclaration) { - if (isIdentifier(typeName)) ensureImportIsEmitted(resolved.importDeclaration, typeName); + let expression: Expression = serializeEntityNameAsExpression(this.f, typeName); + if (isIdentifier(typeName)) { + expression = this.resolveImportExpression( + declaration, + resolved.importDeclaration, + typeName, + expression, + ); + } //we can not infer from module declaration, so do `typeof T` in runtime program.pushOp( ReflectionOp.typeof, - program.pushStack(this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, serializeEntityNameAsExpression(this.f, typeName))), + program.pushStack( + this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, expression), + ), ); - } else if (isTypeAliasDeclaration(declaration) || isInterfaceDeclaration(declaration) || isEnumDeclaration(declaration)) { + } else if ( + isTypeAliasDeclaration(declaration) || + isInterfaceDeclaration(declaration) || + isEnumDeclaration(declaration) + ) { //Set/Map are interface declarations const name = getNameAsString(typeName); if (name === 'Array') { @@ -2104,103 +2538,146 @@ export class ReflectionTransformer implements CustomTransformer { return; } - const runtimeTypeName = this.getDeclarationVariableName(typeName); + let runtimeTypeName = this.getRuntimeTypeName(typeName); + let externalLibraryImport: ExternalLibraryImport | undefined; //to break recursion, we track which declaration has already been compiled if (!this.compiledDeclarations.has(declaration) && !this.compileDeclarations.has(declaration)) { const declarationSourceFile = findSourceFile(declaration) || this.sourceFile; - const isGlobal = resolved.importDeclaration === undefined && declarationSourceFile.fileName !== this.sourceFile.fileName; - const isFromImport = resolved.importDeclaration !== undefined; if (this.isExcluded(declarationSourceFile.fileName)) { program.pushOp(ReflectionOp.any); return; } - if (isGlobal) { - //we don't embed non-global imported declarations anymore, only globals - this.embedDeclarations.set(declaration, { - name: typeName, - sourceFile: declarationSourceFile, - }); - } else if (isFromImport) { - if (resolved.importDeclaration) { - //if explicit `import {type T}`, we do not emit an import and instead push any - if (resolved.typeOnly) { - this.resolveTypeOnlyImport(typeName, program); - return; - } - - // we need to find the declaration file of the import - const found = this.resolver.resolve(this.sourceFile, resolved.importDeclaration); - if (!found) { - debug('module not found'); - program.pushOp(ReflectionOp.any); - return; - } - - // debug('import', getNameAsString(typeName), 'from', - // (resolved.importDeclaration.moduleSpecifier as StringLiteral).text, ' in', program.sourceFile.fileName); - - // Previously we checked for tsconfig.json/package.json with a "reflection" option. - // This is now changed, and we look directly if there is a __Ω{name} exported. - // If so, then we can be 100% sure that the referenced module is built with runtime types. - // Note that if `found` is a TypeScript file (not d.ts), then we need to check using the fileName - // since it is part of the current transpilation phase. Thus, it depends on the - // current config + @reflection decorator instead. - if (found.fileName.endsWith('.d.ts')) { - // Note that if import was something like `import { XY } from 'my-module'` then resolve() - // returns the index.d.ts file of the module, not the actual file where XY is exported. - // this is necessary since we emit an additional import `import { __ΩXY } from 'my-module'`, - // so we check if whatever file we get from resolve() actually exports __ΩXY. - const resolverDecVariable = this.resolveImportSpecifier( - runtimeTypeName.escapedText, - resolved.importDeclaration, - this.sourceFile, - ); - - if (!resolverDecVariable) { - debug(`Symbol ${runtimeTypeName.escapedText} not found in ${found.fileName}`); - //no __Ω{name} exported, so we can not be sure if the module is built with runtime types + if ( + this.external.hasSourceFile(declarationSourceFile) && + this.external.isEmbeddingExternalLibraryImport() + ) { + externalLibraryImport = this.external.processExternalLibraryImport( + typeName, + declaration, + declarationSourceFile, + resolved.importDeclaration, + ); + } else { + const isGlobal = + resolved.importDeclaration === undefined && + declarationSourceFile.fileName !== this.sourceFile.fileName; + const isFromImport = resolved.importDeclaration !== undefined; + + if (isGlobal) { + this.external.knownGlobalTypeNames.add(getNameAsString(typeName)); + this.embedDeclarations.set(declaration, { + name: typeName, + sourceFile: declarationSourceFile, + }); + } else if (isFromImport) { + if (resolved.importDeclaration) { + //if explicit `import {type T}`, we do not emit an import and instead push any + if (resolved.typeOnly) { this.resolveTypeOnlyImport(typeName, program); return; } - this.addImports.push({ identifier: runtimeTypeName, from: resolved.importDeclaration.moduleSpecifier }); - } else { - const reflection = this.getReflectionConfig(found); - // if this is never, then its generally disabled for this file - if (reflection.mode === 'never') { - this.resolveTypeOnlyImport(typeName, program); + // we need to find the declaration file of the import + const found = this.resolver.resolve(resolved.sourceFile, resolved.importDeclaration); + if (!found) { + debug('module not found'); + program.pushOp(ReflectionOp.any); return; } - const declarationReflection = this.isWithReflection(found, declaration); - if (!declarationReflection) { - this.resolveTypeOnlyImport(typeName, program); - return; - } + // Previously we checked for tsconfig.json/package.json with a "reflection" option. + // This is now changed, and we look directly if there is a __Ω{name} exported. + // If so, then we can be 100% sure that the referenced module is built with runtime types. + // Note that if `found` is a TypeScript file (not d.ts), then we need to check using the fileName + // since it is part of the current transpilation phase. Thus, it depends on the + // current config + @reflection decorator instead. + if (found.fileName.endsWith('.d.ts')) { + // Note that if import was something like `import { XY } from 'my-module'` then resolve() + // returns the index.d.ts file of the module, not the actual file where XY is exported. + // this is necessary since we emit an additional import `import { __ΩXY } from 'my-module'`, + // so we check if whatever file we get from resolve() actually exports __ΩXY. + const resolverDecVariable = this.resolveImportSpecifier( + runtimeTypeName.escapedText, + resolved.importDeclaration, + this.sourceFile, + ); - this.addImports.push({ identifier: runtimeTypeName, from: resolved.importDeclaration.moduleSpecifier }); + if (resolverDecVariable) { + this.addImports.push({ + identifier: runtimeTypeName, + from: resolved.importDeclaration.moduleSpecifier, + }); + } else { + const declarationReflection = this.getReflectionConfig(this.sourceFile); + if ( + this.external.shouldInlineExternalLibraryImport( + resolved.importDeclaration, + typeName, + declarationReflection, + ) + ) { + externalLibraryImport = this.external.processExternalLibraryImport( + typeName, + declaration, + declarationSourceFile, + resolved.importDeclaration, + ); + } else { + debug( + `Symbol ${runtimeTypeName.escapedText} not found in ${found.fileName}`, + ); + //no __Ω{name} exported, so we can not be sure if the module is built with runtime types + this.resolveTypeOnlyImport(typeName, program); + return; + } + } + } else { + const reflection = this.getReflectionConfig(found); + // if this is never, then its generally disabled for this file + if (reflection.mode === 'never') { + this.resolveTypeOnlyImport(typeName, program); + return; + } + + const declarationReflection = this.isWithReflection(found, declaration); + if (!declarationReflection) { + this.resolveTypeOnlyImport(typeName, program); + return; + } + + this.addImports.push({ + identifier: runtimeTypeName, + from: resolved.importDeclaration.moduleSpecifier, + }); + } + } + } else { + //it's a reference type inside the same file. Make sure its type is reflected + const reflection = this.isWithReflection(program.sourceFile, declaration); + if (!reflection) { + this.resolveTypeOnlyImport(typeName, program); + return; } - } - } else { - //it's a reference type inside the same file. Make sure its type is reflected - const reflection = this.isWithReflection(program.sourceFile, declaration); - if (!reflection) { - this.resolveTypeOnlyImport(typeName, program); - return; - } - this.compileDeclarations.set(declaration, { - name: typeName, - sourceFile: declarationSourceFile, - }); + this.compileDeclarations.set(declaration, { + name: typeName, + sourceFile: declarationSourceFile, + }); + } } } + runtimeTypeName = externalLibraryImport + ? this.getExternalRuntimeTypeName(typeName, externalLibraryImport) + : this.getDeclarationVariableName(typeName); + const index = program.pushStack( - program.forNode === declaration ? 0 : this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, runtimeTypeName), + program.forNode === declaration + ? 0 + : this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, runtimeTypeName), ); if (type.typeArguments) { for (const argument of type.typeArguments) { @@ -2226,7 +2703,12 @@ export class ReflectionTransformer implements CustomTransformer { // //{[Property in keyof Type]: boolean;}; // this.extractPackStructOfType(declaration, program); // return; - } else if (isClassDeclaration(declaration) || isFunctionDeclaration(declaration) || isFunctionExpression(declaration) || isArrowFunction(declaration)) { + } else if ( + isClassDeclaration(declaration) || + isFunctionDeclaration(declaration) || + isFunctionExpression(declaration) || + isArrowFunction(declaration) + ) { // classes, functions and arrow functions are handled differently, since they exist in runtime. //if explicit `import {type T}`, we do not emit an import and instead push any @@ -2242,16 +2724,30 @@ export class ReflectionTransformer implements CustomTransformer { return; } - if (resolved.importDeclaration && isIdentifier(typeName)) ensureImportIsEmitted(resolved.importDeclaration, typeName); program.pushFrame(); if (type.typeArguments) { for (const typeArgument of type.typeArguments) { this.extractPackStructOfType(typeArgument, program); } } - const body = isIdentifier(typeName) ? typeName : this.createAccessorForEntityName(typeName); - const index = program.pushStack(this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, body)); - program.pushOp(isClassDeclaration(declaration) ? ReflectionOp.classReference : ReflectionOp.functionReference, index); + let body: Identifier | Expression = isIdentifier(typeName) + ? typeName + : this.createAccessorForEntityName(typeName); + if (resolved.importDeclaration && isIdentifier(typeName)) { + body = this.resolveImportExpression( + resolved.declaration, + resolved.importDeclaration, + typeName, + body, + ); + } + const index = program.pushStack( + this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, body), + ); + program.pushOp( + isClassDeclaration(declaration) ? ReflectionOp.classReference : ReflectionOp.functionReference, + index, + ); program.popFrameImplicit(); } else if (isTypeParameterDeclaration(declaration)) { this.resolveTypeParameter(declaration, type, program); @@ -2272,7 +2768,12 @@ export class ReflectionTransformer implements CustomTransformer { if (current.kind === SyntaxKind.ClassExpression) return current; //return the class if (current.kind === SyntaxKind.Constructor) return current.parent; //return the class if (current.kind === SyntaxKind.MethodDeclaration) return current.parent; //return the class - if (current.kind === SyntaxKind.ArrowFunction || current.kind === SyntaxKind.FunctionDeclaration || current.kind === SyntaxKind.FunctionExpression) return current; + if ( + current.kind === SyntaxKind.ArrowFunction || + current.kind === SyntaxKind.FunctionDeclaration || + current.kind === SyntaxKind.FunctionExpression + ) + return current; current = current.parent; } @@ -2333,7 +2834,10 @@ export class ReflectionTransformer implements CustomTransformer { * } * ``` */ - protected needsToBeInferred(declaration: TypeParameterDeclaration, type: TypeReferenceNode | ExpressionWithTypeArguments): boolean { + protected needsToBeInferred( + declaration: TypeParameterDeclaration, + type: TypeReferenceNode | ExpressionWithTypeArguments, + ): boolean { const declarationUser = this.getTypeUser(declaration); const typeUser = this.getTypeUser(type); @@ -2342,9 +2846,7 @@ export class ReflectionTransformer implements CustomTransformer { protected resolveTypeOnlyImport(entityName: EntityName, program: CompilerProgram) { program.pushOp(ReflectionOp.any); - const typeName = ts.isIdentifier(entityName) - ? getIdentifierName(entityName) - : getIdentifierName(entityName.right); + const typeName = getEntityName(entityName); this.resolveTypeName(typeName, program); } @@ -2353,16 +2855,22 @@ export class ReflectionTransformer implements CustomTransformer { program.pushOp(ReflectionOp.typeName, program.findOrAddStackEntry(typeName)); } - protected resolveTypeParameter(declaration: TypeParameterDeclaration, type: TypeReferenceNode | ExpressionWithTypeArguments, program: CompilerProgram) { + protected resolveTypeParameter( + declaration: TypeParameterDeclaration, + type: TypeReferenceNode | ExpressionWithTypeArguments, + program: CompilerProgram, + ) { //check if `type` was used in an expression. if so, we need to resolve it from runtime, otherwise we mark it as T const isUsedInFunction = isFunctionLike(declaration.parent); - const resolveRuntimeTypeParameter = (isUsedInFunction && program.isResolveFunctionParameters(declaration.parent)) || (this.needsToBeInferred(declaration, type)); + const resolveRuntimeTypeParameter = + (isUsedInFunction && program.isResolveFunctionParameters(declaration.parent)) || + this.needsToBeInferred(declaration, type); if (resolveRuntimeTypeParameter) { //go through all parameters and look where `type.name.escapedText` is used (recursively). //go through all found parameters and replace `T` with `infer T` and embed its type in `typeof parameter extends Type ? T : never`, if T is not directly used const argumentName = declaration.name.escapedText as string; //T - const foundUsers: { type: Node, parameterName: Identifier }[] = []; + const foundUsers: { type: Node; parameterName: Identifier }[] = []; if (isUsedInFunction) { for (const parameter of (declaration.parent as SignatureDeclaration).parameters) { @@ -2384,7 +2892,10 @@ export class ReflectionTransformer implements CustomTransformer { if (isIdentifier(parameter.name)) { const updatedParameterType = visitEachChild(parameter.type, searchArgument, this.context); if (found) { - foundUsers.push({ type: updatedParameterType, parameterName: parameter.name }); + foundUsers.push({ + type: updatedParameterType, + parameterName: parameter.name, + }); } } } @@ -2399,7 +2910,21 @@ export class ReflectionTransformer implements CustomTransformer { for (const foundUser of foundUsers) { program.pushConditionalFrame(); - program.pushOp(ReflectionOp.typeof, program.pushStack(this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, foundUser.parameterName))); + console.log(getIdentifierName(foundUser.parameterName)); + + program.pushOp( + ReflectionOp.typeof, + program.pushStack( + this.f.createArrowFunction( + undefined, + undefined, + [], + undefined, + undefined, + foundUser.parameterName, + ), + ), + ); this.extractPackStructOfType(foundUser.type, program); program.pushOp(ReflectionOp.extends); @@ -2418,7 +2943,6 @@ export class ReflectionTransformer implements CustomTransformer { if (foundUsers.length > 1) { //todo: intersection end } - } else if (declaration.constraint) { if (isUsedInFunction) program.resolveFunctionParametersIncrease(declaration.parent); const constraint = getEffectiveConstraintOfTypeParameter(declaration); @@ -2438,10 +2962,16 @@ export class ReflectionTransformer implements CustomTransformer { } protected createAccessorForEntityName(e: QualifiedName): PropertyAccessExpression { - return this.f.createPropertyAccessExpression(isIdentifier(e.left) ? e.left : this.createAccessorForEntityName(e.left), e.right); + return this.f.createPropertyAccessExpression( + isIdentifier(e.left) ? e.left : this.createAccessorForEntityName(e.left), + e.right, + ); } - protected findDeclarationInFile(sourceFile: SourceFile | ModuleDeclaration, declarationName: __String): Declaration | undefined { + protected findDeclarationInFile( + sourceFile: SourceFile | ModuleDeclaration, + declarationName: __String, + ): Declaration | undefined { if (isNodeWithLocals(sourceFile) && sourceFile.locals) { const declarationSymbol = sourceFile.locals.get(declarationName); if (declarationSymbol && declarationSymbol.declarations && declarationSymbol.declarations[0]) { @@ -2451,14 +2981,22 @@ export class ReflectionTransformer implements CustomTransformer { return; } - protected resolveImportSpecifier(declarationName: __String, importOrExport: ExportDeclaration | ImportDeclaration, sourceFile: SourceFile): Declaration | undefined { + protected resolveImportSpecifier( + declarationName: __String, + importOrExport: ExportDeclaration | ImportDeclaration, + sourceFile: SourceFile, + ): Declaration | undefined { if (!importOrExport.moduleSpecifier) return; if (!isStringLiteral(importOrExport.moduleSpecifier)) return; const source: SourceFile | ModuleDeclaration | undefined = this.resolver.resolve(sourceFile, importOrExport); if (!source) { - debug('module not found', (importOrExport.moduleSpecifier as any).text, 'Is transpileOnly enabled? It needs to be disabled.'); + debug( + 'module not found', + (importOrExport.moduleSpecifier as any).text, + 'Is transpileOnly enabled? It needs to be disabled.', + ); return; } @@ -2487,14 +3025,22 @@ export class ReflectionTransformer implements CustomTransformer { return; } - protected followExport(declarationName: __String, statement: ExportDeclaration, sourceFile: SourceFile): Declaration | undefined { + protected followExport( + declarationName: __String, + statement: ExportDeclaration, + sourceFile: SourceFile, + ): Declaration | undefined { if (statement.exportClause) { //export {y} from 'x' if (isNamedExports(statement.exportClause)) { for (const element of statement.exportClause.elements) { //see if declarationName is exported if (element.name.escapedText === declarationName) { - const found = this.resolveImportSpecifier(element.propertyName ? element.propertyName.escapedText : declarationName, statement, sourceFile); + const found = this.resolveImportSpecifier( + element.propertyName ? element.propertyName.escapedText : declarationName, + statement, + sourceFile, + ); if (found) return found; } } @@ -2551,20 +3097,31 @@ export class ReflectionTransformer implements CustomTransformer { } const type = this.getTypeOfType(node); const __type = this.f.createPropertyDeclaration( - this.f.createModifiersFromModifierFlags(ModifierFlags.Static), '__type', - undefined, undefined, - type); + this.f.createModifiersFromModifierFlags(ModifierFlags.Static), + '__type', + undefined, + undefined, + type, + ); if (isClassDeclaration(node)) { // return node; - return this.f.updateClassDeclaration(node, node.modifiers, - node.name, node.typeParameters, node.heritageClauses, + return this.f.updateClassDeclaration( + node, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, this.f.createNodeArray([...node.members, __type]), ); } - return this.f.updateClassExpression(node, node.modifiers, - node.name, node.typeParameters, node.heritageClauses, + return this.f.updateClassExpression( + node, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, this.f.createNodeArray([...node.members, __type]), ); } @@ -2587,7 +3144,6 @@ export class ReflectionTransformer implements CustomTransformer { * => function name() {}; name.__type = 34; */ protected decorateFunctionDeclaration(declaration: FunctionDeclaration) { - const encodedType = this.getTypeOfType(declaration); if (!encodedType) return declaration; @@ -2598,18 +3154,43 @@ export class ReflectionTransformer implements CustomTransformer { //since a new default export is created, we do not need ExportKey&DefaultKeyword on the function anymore, //but it should preserve all others like Async. const modifier: readonly Modifier[] = declaration.modifiers - ? declaration.modifiers.filter(v => v.kind !== SyntaxKind.ExportKeyword && v.kind !== SyntaxKind.DefaultKeyword && v.kind !== SyntaxKind.Decorator) as Modifier[] + ? (declaration.modifiers.filter( + v => + v.kind !== SyntaxKind.ExportKeyword && + v.kind !== SyntaxKind.DefaultKeyword && + v.kind !== SyntaxKind.Decorator, + ) as Modifier[]) : []; - return this.f.createExportAssignment(undefined, undefined, this.wrapWithAssignType( - this.f.createFunctionExpression(modifier, declaration.asteriskToken, declaration.name, declaration.typeParameters, declaration.parameters, declaration.type, declaration.body), - encodedType, - )); + return this.f.createExportAssignment( + undefined, + undefined, + this.wrapWithAssignType( + this.f.createFunctionExpression( + modifier, + declaration.asteriskToken, + declaration.name, + declaration.typeParameters, + declaration.parameters, + declaration.type, + declaration.body, + ), + encodedType, + ), + ); } const statements: Statement[] = [declaration]; - statements.push(this.f.createExpressionStatement( - this.f.createAssignment(this.f.createPropertyAccessExpression(serializeEntityNameAsExpression(this.f, declaration.name), '__type'), encodedType), - )); + statements.push( + this.f.createExpressionStatement( + this.f.createAssignment( + this.f.createPropertyAccessExpression( + serializeEntityNameAsExpression(this.f, declaration.name), + '__type', + ), + encodedType, + ), + ), + ); return statements; } @@ -2634,14 +3215,7 @@ export class ReflectionTransformer implements CustomTransformer { protected wrapWithAssignType(fn: Expression, type: Expression) { this.embedAssignType = true; - return this.f.createCallExpression( - this.f.createIdentifier('__assignType'), - undefined, - [ - fn, - type, - ], - ); + return this.f.createCallExpression(this.f.createIdentifier('__assignType'), undefined, [fn, type]); } protected isWithReflection(sourceFile: SourceFile, node: Node & { __deepkitConfig?: ReflectionConfig }): boolean { @@ -2655,13 +3229,23 @@ export class ReflectionTransformer implements CustomTransformer { current = current.parent; } - if (reflectionComment === '' || reflectionComment === 'true' || reflectionComment === 'default' - || reflectionComment === 'enabled' || reflectionComment === '1') { + if ( + reflectionComment === '' || + reflectionComment === 'true' || + reflectionComment === 'default' || + reflectionComment === 'enabled' || + reflectionComment === '1' + ) { return true; } - if (reflectionComment === 'false' || reflectionComment === 'disabled' || reflectionComment === 'never' - || reflectionComment === 'no' || reflectionComment === '0') { + if ( + reflectionComment === 'false' || + reflectionComment === 'disabled' || + reflectionComment === 'never' || + reflectionComment === 'no' || + reflectionComment === '0' + ) { return false; } @@ -2677,7 +3261,7 @@ export class DeclarationTransformer extends ReflectionTransformer { if ((sourceFile as any).deepkitDeclarationTransformed) return sourceFile; (sourceFile as any).deepkitDeclarationTransformed = true; - this.sourceFile = sourceFile; + this.setSourceFile(sourceFile); this.addExports = []; const reflection = this.getReflectionConfig(sourceFile); @@ -2686,16 +3270,22 @@ export class DeclarationTransformer extends ReflectionTransformer { const visitor = (node: Node): any => { node = visitEachChild(node, visitor, this.context); - if ((isTypeAliasDeclaration(node) || isInterfaceDeclaration(node)) && hasModifier(node, SyntaxKind.ExportKeyword)) { + if ( + (isTypeAliasDeclaration(node) || isInterfaceDeclaration(node)) && + hasModifier(node, SyntaxKind.ExportKeyword) + ) { const reflection = this.isWithReflection(sourceFile, node); if (reflection) { - this.addExports.push({ identifier: getIdentifierName(this.getDeclarationVariableName(node.name)) }); + this.addExports.push({ + identifier: getIdentifierName(this.getDeclarationVariableName(node.name)), + }); } } return node; }; - this.sourceFile = visitNode(this.sourceFile, visitor); + + this.setSourceFile(visitNode(this.sourceFile, visitor)); if (this.addExports.length) { const exports: Statement[] = []; @@ -2705,16 +3295,20 @@ export class DeclarationTransformer extends ReflectionTransformer { handledIdentifier.push(imp.identifier); //export declare type __ΩXY = any[]; - exports.push(this.f.createTypeAliasDeclaration([ - this.f.createModifier(SyntaxKind.ExportKeyword), - this.f.createModifier(SyntaxKind.DeclareKeyword), - ], this.f.createIdentifier(imp.identifier), - undefined, - this.f.createArrayTypeNode(this.f.createKeywordTypeNode(SyntaxKind.AnyKeyword)), - )); + exports.push( + this.f.createTypeAliasDeclaration( + [ + this.f.createModifier(SyntaxKind.ExportKeyword), + this.f.createModifier(SyntaxKind.DeclareKeyword), + ], + this.f.createIdentifier(imp.identifier), + undefined, + this.f.createArrayTypeNode(this.f.createKeywordTypeNode(SyntaxKind.AnyKeyword)), + ), + ); } - this.sourceFile = this.f.updateSourceFile(this.sourceFile, [...this.sourceFile.statements, ...exports]); + this.setSourceFile(this.f.updateSourceFile(this.sourceFile, [...this.sourceFile.statements, ...exports])); } return this.sourceFile; @@ -2722,7 +3316,7 @@ export class DeclarationTransformer extends ReflectionTransformer { } let loaded = false; -const cache = new Cache; +const cache = new Cache(); export const transformer: CustomTransformerFactory = function deepkitTransformer(context) { if (!loaded) { @@ -2736,4 +3330,3 @@ export const transformer: CustomTransformerFactory = function deepkitTransformer export const declarationTransformer: CustomTransformerFactory = function deepkitDeclarationTransformer(context) { return new DeclarationTransformer(context, cache); }; - diff --git a/packages/type-compiler/src/config.ts b/packages/type-compiler/src/config.ts index 4d73a5d0c..24e1964a8 100644 --- a/packages/type-compiler/src/config.ts +++ b/packages/type-compiler/src/config.ts @@ -56,6 +56,11 @@ export interface TsConfigJson { * Per default a few global .d.ts files are excluded like `lib.dom*.d.ts` and `*typedarrays.d.ts`. */ exclude?: string[]; + + /** + * External library imports to reflect + */ + inlineExternalLibraryImports?: true | Record; }; } @@ -104,6 +109,11 @@ export interface ReflectionConfig { * or a list of globs to match against. */ reflection?: string[] | Mode; + + /** + * External library imports to reflect + */ + inlineExternalLibraryImports?: true | Record; } export interface CurrentConfig extends ReflectionConfig { @@ -217,6 +227,7 @@ function applyConfigValues(existing: CurrentConfig, parent: TsConfigJson, baseDi export interface MatchResult { tsConfigPath: string; mode: (typeof reflectionModes)[number]; + inlineExternalLibraryImports?: true | Record; } export const defaultExcluded = [ @@ -300,20 +311,25 @@ export function getResolver( exclude: config.exclude, reflection: config.reflection, mergeStrategy: config.mergeStrategy || defaultMergeStrategy, + inlineExternalLibraryImports: config.inlineExternalLibraryImports, }; - if (isDebug()) { - debug( - `Found config ${resolvedConfig.path}:\nreflection:`, - resolvedConfig.reflection, - `\nexclude:`, - resolvedConfig.exclude, - ); - } + debug( + `Found config ${resolvedConfig.path}:\nreflection:`, + resolvedConfig.reflection, + `\nexclude:`, + resolvedConfig.exclude, + `\ninlineExternalLibraryImports:`, + resolvedConfig.inlineExternalLibraryImports, + ); const match = (path: string) => { const mode = reflectionModeMatcher(config, path); - return { mode, tsConfigPath }; + return { + mode, + tsConfigPath, + inlineExternalLibraryImports: config.inlineExternalLibraryImports, + }; }; return (cache[tsConfigPath] = { config: resolvedConfig, match }); diff --git a/packages/type-compiler/src/external.ts b/packages/type-compiler/src/external.ts new file mode 100644 index 000000000..2c874f442 --- /dev/null +++ b/packages/type-compiler/src/external.ts @@ -0,0 +1,128 @@ +import { EntityName, ImportDeclaration, Node, ResolvedModuleFull, SourceFile, isStringLiteral } from 'typescript'; + +import { ReflectionConfig } from './config.js'; +import { getEntityName, getNameAsString, hasSourceFile } from './reflection-ast.js'; +import { Resolver } from './resolver.js'; + +export interface ExternalLibraryImport { + declaration: Node; + name: EntityName; + sourceFile: SourceFile; + module: Required; +} + +export class External { + protected sourceFileNames = new Set(); + + public compileExternalLibraryImports = new Map>(); + + protected processedEntities = new Set(); + + public embeddedLibraryVariables = new Set(); + + public knownGlobalTypeNames = new Set(); + + public sourceFile?: SourceFile; + + protected embeddingExternalLibraryImport?: ExternalLibraryImport; + + constructor(protected resolver: Resolver) {} + + startEmbeddingExternalLibraryImport(value: ExternalLibraryImport): void { + if (this.embeddingExternalLibraryImport) { + throw new Error('Already embedding external library import'); + } + this.embeddingExternalLibraryImport = value; + } + + getEmbeddingExternalLibraryImport(): ExternalLibraryImport { + if (!this.embeddingExternalLibraryImport) { + throw new Error('Not embedding external library import'); + } + return this.embeddingExternalLibraryImport; + } + + isEmbeddingExternalLibraryImport(): boolean { + return !!this.embeddingExternalLibraryImport; + } + + finishEmbeddingExternalLibraryImport(): void { + delete this.embeddingExternalLibraryImport; + } + + public addSourceFile(sourceFile: SourceFile): void { + this.sourceFileNames.add(sourceFile.fileName); + } + + public hasSourceFile(sourceFile: SourceFile): boolean { + return this.sourceFileNames.has(sourceFile.fileName); + } + + public shouldInlineExternalLibraryImport( + importDeclaration: ImportDeclaration, + entityName: EntityName, + config: ReflectionConfig, + ): boolean { + if (!isStringLiteral(importDeclaration.moduleSpecifier)) return false; + if (!hasSourceFile(importDeclaration)) return false; + let resolvedModule; + try { + // throws an error if import is not an external library + resolvedModule = this.resolver.resolveExternalLibraryImport(importDeclaration); + } catch { + return false; + } + if (config.inlineExternalLibraryImports === true) return true; + const imports = config.inlineExternalLibraryImports?.[resolvedModule.packageId.name]; + if (!imports) return false; + if (imports === true) return true; + if (!importDeclaration.moduleSpecifier.text.startsWith(resolvedModule.packageId.name)) { + return true; + } + const typeName = getEntityName(entityName); + return imports.includes(typeName); + } + + public hasProcessedEntity(typeName: EntityName): boolean { + return this.processedEntities.has(getNameAsString(typeName)); + } + + public processExternalLibraryImport( + typeName: EntityName, + declaration: Node, + sourceFile: SourceFile, + importDeclaration?: ImportDeclaration, + ): ExternalLibraryImport { + const module = importDeclaration + ? this.resolver.resolveExternalLibraryImport(importDeclaration) + : this.getEmbeddingExternalLibraryImport().module; + + const entityName = getNameAsString(typeName); + if (this.processedEntities.has(entityName)) { + return { + name: typeName, + declaration, + sourceFile, + module, + }; + } + + this.processedEntities.add(entityName); + + const imports = + this.compileExternalLibraryImports.get(module.packageId.name) || new Map(); + const externalLibraryImport: ExternalLibraryImport = { + name: typeName, + declaration, + sourceFile, + module, + }; + this.compileExternalLibraryImports.set(module.packageId.name, imports.set(entityName, externalLibraryImport)); + + if (sourceFile.fileName !== this.sourceFile?.fileName) { + this.addSourceFile(sourceFile); + } + + return externalLibraryImport!; + } +} diff --git a/packages/type-compiler/src/reflection-ast.ts b/packages/type-compiler/src/reflection-ast.ts index ab33b3663..0ba4df124 100644 --- a/packages/type-compiler/src/reflection-ast.ts +++ b/packages/type-compiler/src/reflection-ast.ts @@ -7,8 +7,8 @@ * * You should have received a copy of the MIT License along with this program. */ - -import ts, { +import { CloneNodeHook, cloneNode as tsNodeClone } from '@marcj/ts-clone-node'; +import type { ArrowFunction, BigIntLiteral, BinaryExpression, @@ -30,7 +30,9 @@ import ts, { StringLiteralLike, SymbolTable, } from 'typescript'; -import { cloneNode as tsNodeClone, CloneNodeHook } from '@marcj/ts-clone-node'; +import ts from 'typescript'; + +import { External, ExternalLibraryImport } from './external.js'; import { SourceFile } from './ts-types.js'; const { @@ -55,6 +57,18 @@ export function getIdentifierName(node: Identifier | PrivateIdentifier): string return ts.unescapeLeadingUnderscores(node.escapedText); } +export function getExternalRuntimeTypeName(importPath: string): string { + return `__ɵΩ${importPath.replace(/[^a-zA-Z0-9]+/g, '_')}`; +} + +export function getRuntimeTypeName(typeName: string): string { + return `__Ω${typeName}`; +} + +export function hasSourceFile(node: Node): boolean { + return typeof node.getSourceFile === 'function'; +} + export function findSourceFile(node: Node): SourceFile | undefined { if (node.kind === SyntaxKind.SourceFile) return node as SourceFile; let current = node.parent; @@ -86,7 +100,13 @@ export function parseJSDocAttributeFromText(comment: string, attribute: string): if (withoutContent === -1) return undefined; //make sure next character is space or end of comment const nextCharacter = comment[withoutContent + attribute.length + 1]; - if (!nextCharacter || nextCharacter === ' ' || nextCharacter === '\n' || nextCharacter === '\r' || nextCharacter === '\t') { + if ( + !nextCharacter || + nextCharacter === ' ' || + nextCharacter === '\n' || + nextCharacter === '\r' || + nextCharacter === '\t' + ) { return ''; } start = withoutContent + attribute.length + 1; @@ -102,14 +122,21 @@ export function parseJSDocAttributeFromText(comment: string, attribute: string): const content = comment.substring(start, end).trim(); // make sure multiline comments are supported, and each line is trimmed and `\s\s\s\*` removed - return content.split('\n').map(v => { - const indexOfStar = v.indexOf('*'); - if (indexOfStar === -1) return v.trim(); - return v.substring(indexOfStar + 1).trim(); - }).join('\n'); + return content + .split('\n') + .map(v => { + const indexOfStar = v.indexOf('*'); + if (indexOfStar === -1) return v.trim(); + return v.substring(indexOfStar + 1).trim(); + }) + .join('\n'); } -export function extractJSDocAttribute(sourceFile: SourceFile, node: Node | undefined, attribute: string): string | undefined { +export function extractJSDocAttribute( + sourceFile: SourceFile, + node: Node | undefined, + attribute: string, +): string | undefined { // in TypeScript 5.3 they made JSDoc parsing optional and disabled by default. // we need to read the comments manually and then parse @{attribute} {value} manually. // we need reference to SourceFile, since Node.getSourceFile() although available in types, @@ -143,7 +170,8 @@ export function getNameAsString(node?: PropertyName | QualifiedName): string { if (isNumericLiteral(node)) return node.text; if (isNoSubstitutionTemplateLiteral(node)) return node.text; if (isComputedPropertyName(node)) { - if (isStringLiteralLike(node) || isNumericLiteral(node)) return (node as StringLiteralLike | NumericLiteral).text; + if (isStringLiteralLike(node) || isNumericLiteral(node)) + return (node as StringLiteralLike | NumericLiteral).text; return ''; } if (isPrivateIdentifier(node)) return getIdentifierName(node); @@ -169,39 +197,83 @@ const cloneHook = (node: T, payload: { depth: number }): CloneNo }; export class NodeConverter { - constructor(protected f: NodeFactory) { + constructor( + protected f: NodeFactory, + protected external: External, + ) {} + + createExternalRuntimeTypePropertyAccessExpression( + name: string, + externalLibraryImport?: ExternalLibraryImport, + ): PropertyAccessExpression { + const { module } = externalLibraryImport || this.external.getEmbeddingExternalLibraryImport(); + return this.f.createPropertyAccessExpression( + this.f.createIdentifier(getExternalRuntimeTypeName(module.packageId.name)), + name, + ); } toExpression(node?: T): Expression { if (node === undefined) return this.f.createIdentifier('undefined'); if (Array.isArray(node)) { - return this.f.createArrayLiteralExpression(this.f.createNodeArray(node.map(v => this.toExpression(v))) as NodeArray); + return this.f.createArrayLiteralExpression( + this.f.createNodeArray(node.map(v => this.toExpression(v))) as NodeArray, + ); + } + + if ('string' === typeof node) { + return this.f.createStringLiteral(node, true); } - if ('string' === typeof node) return this.f.createStringLiteral(node, true); if ('number' === typeof node) return this.f.createNumericLiteral(node); - if ('bigint' === typeof node) return this.f.createBigIntLiteral(String(node)); - if ('boolean' === typeof node) return node ? this.f.createTrue() : this.f.createFalse(); + + if ('bigint' === typeof node) { + return this.f.createBigIntLiteral(String(node)); + } + + if ('boolean' === typeof node) { + return node ? this.f.createTrue() : this.f.createFalse(); + } if (node.pos === -1 && node.end === -1 && node.parent === undefined) { if (isArrowFunction(node)) { - if (node.body.pos === -1 && node.body.end === -1 && node.body.parent === undefined) return node; - return this.f.createArrowFunction(node.modifiers, node.typeParameters, node.parameters, node.type, node.equalsGreaterThanToken, this.toExpression(node.body as Expression)); + if (node.body.pos === -1 && node.body.end === -1 && node.body.parent === undefined) { + return node; + } + + return this.f.createArrowFunction( + node.modifiers, + node.typeParameters, + node.parameters, + node.type, + node.equalsGreaterThanToken, + this.toExpression(node.body as Expression), + ); } + return node; } + switch (node.kind) { case SyntaxKind.Identifier: - return finish(node, this.f.createIdentifier(getIdentifierName(node as Identifier))); + const name = getIdentifierName(node as Identifier); + return this.external.isEmbeddingExternalLibraryImport() && !this.external.knownGlobalTypeNames.has(name) + ? this.createExternalRuntimeTypePropertyAccessExpression(name) + : finish(node, this.f.createIdentifier(name)); + case SyntaxKind.StringLiteral: return finish(node, this.f.createStringLiteral((node as StringLiteral).text)); + case SyntaxKind.NumericLiteral: return finish(node, this.f.createNumericLiteral((node as NumericLiteral).text)); + case SyntaxKind.BigIntLiteral: return finish(node, this.f.createBigIntLiteral((node as BigIntLiteral).text)); + case SyntaxKind.TrueKeyword: return finish(node, this.f.createTrue()); + case SyntaxKind.FalseKeyword: return finish(node, this.f.createFalse()); } @@ -224,7 +296,6 @@ export class NodeConverter { console.error('could not clone node', node); throw error; } - } } @@ -233,10 +304,18 @@ function isExternalOrCommonJsModule(file: SourceFile): boolean { return (file.externalModuleIndicator || file.commonJsModuleIndicator) !== undefined; } -export function isNodeWithLocals(node: Node): node is (Node & { locals: SymbolTable | undefined }) { +export function isBuiltType(typeVar: Identifier, sourceFile: SourceFile): boolean { + return isNodeWithLocals(sourceFile) && !!sourceFile.locals?.has(typeVar.escapedText); +} + +export function isNodeWithLocals(node: Node): node is Node & { locals: SymbolTable | undefined } { return 'locals' in node; } +export function getEntityName(typeName: EntityName): string { + return isIdentifier(typeName) ? getIdentifierName(typeName) : getIdentifierName(typeName.right); +} + //logic copied from typescript export function getGlobalsOfSourceFile(file: SourceFile): SymbolTable | void { if (file.redirectInfo) return; @@ -266,7 +345,6 @@ export function ensureImportIsEmitted(importDeclaration: ImportDeclaration, spec (importDeclaration.flags as any) |= NodeFlags.Synthesized; } - /** * Serializes an entity name as an expression for decorator type metadata. * @@ -313,4 +391,3 @@ function finish(oldNode: MetaNode, newNode: T): T { newNode.symbol = newNode._symbol; return newNode; } - diff --git a/packages/type-compiler/src/resolver.ts b/packages/type-compiler/src/resolver.ts index 591ad9d1b..f78dc2cf5 100644 --- a/packages/type-compiler/src/resolver.ts +++ b/packages/type-compiler/src/resolver.ts @@ -6,6 +6,7 @@ import type { Expression, ImportDeclaration, ResolvedModule, + ResolvedModuleFull, SourceFile, StringLiteral, } from 'typescript'; @@ -43,7 +44,40 @@ export class Resolver { return this.resolveSourceFile(from, moduleSpecifier); } - protected resolveImpl(modulePath: StringLiteral, sourceFile: SourceFile): ResolvedModule | undefined { + resolveExternalLibraryImport(importDeclaration: ImportDeclaration): Required { + const resolvedModule = this.resolveImport(importDeclaration); + if (!resolvedModule.isExternalLibraryImport) { + throw new Error('Resolved module is not an external library import'); + } + if (!resolvedModule.packageId) { + // packageId will be undefined when importing from sub-paths such as `rxjs/operators` + resolvedModule.packageId = { + name: (importDeclaration.moduleSpecifier as StringLiteral).text, + subModuleName: 'unknown', + version: 'unknown', + }; + } + return resolvedModule as Required; + } + + resolveImport(importDeclaration: ImportDeclaration): ResolvedModuleFull { + if (!isStringLiteral(importDeclaration.moduleSpecifier)) { + throw new Error('Invalid import declaration module specifier'); + } + const resolvedModule = this.resolveImpl( + importDeclaration.moduleSpecifier, + importDeclaration.getSourceFile(), + ) as ResolvedModuleFull; + if (!resolvedModule) { + throw new Error('Cannot resolve module'); + } + return resolvedModule; + } + + protected resolveImpl( + modulePath: StringLiteral, + sourceFile: SourceFile, + ): ResolvedModuleFull | ResolvedModule | undefined { if (this.host.resolveModuleNameLiterals !== undefined) { const results = this.host.resolveModuleNameLiterals( [modulePath], diff --git a/packages/type-compiler/tests/inline-external-library-imports.spec.ts b/packages/type-compiler/tests/inline-external-library-imports.spec.ts new file mode 100644 index 000000000..2ce5cd6d5 --- /dev/null +++ b/packages/type-compiler/tests/inline-external-library-imports.spec.ts @@ -0,0 +1,404 @@ +import { expect, test } from '@jest/globals'; +import { Observable } from 'rxjs'; + +import { TypeClass, TypeFunction } from '@deepkit/type'; + +import { transpile, transpileAndRun } from './utils'; + +test('string type alias', () => { + const res = transpile( + { + app: `import { NIL } from 'uuid'; + + type T = typeof NIL; + `, + }, + undefined, + { + inlineExternalLibraryImports: { + uuid: ['NIL'], + }, + }, + ); + + expect(res.app).not.toContain('const __ΩNIL = ['); + expect(res.app).not.toContain('() => __assignType(uuid_1.NIL, __ΩNIL)'); + expect(res.app).toContain('() => uuid_1.NIL'); +}); + +test('typeOf string type alias', () => { + const res = transpileAndRun({ + app: `import { typeOf } from '@deepkit/type'; + import { NIL } from 'uuid'; + + typeOf(); + `, + }); + + expect(res).toMatchInlineSnapshot(` + { + "kind": 13, + "literal": "00000000-0000-0000-0000-000000000000", + "typeName": undefined, + } + `); +}); + +test('object type alias', () => { + const res = transpile( + { + app: `import { config } from 'rxjs'; + + type A = typeof config; + `, + }, + undefined, + { + inlineExternalLibraryImports: { + rxjs: ['config'], + }, + }, + ); + + expect(res.app).not.toContain('const __Ωconfig = ['); + expect(res.app).not.toContain('() => __assignType(rxjs_1.config, __Ωconfig)'); + expect(res.app).toContain('() => rxjs_1.config'); +}); + +test('typeOf object type alias', () => { + const res = transpileAndRun({ + app: `import { typeOf } from '@deepkit/type'; + import { config } from 'rxjs'; + + typeOf(); + `, + }); + + expect(res).toMatchInlineSnapshot(` + { + "annotations": {}, + "id": 2, + "kind": 30, + "typeName": undefined, + "types": [ + { + "kind": 32, + "name": "onUnhandledError", + "parent": [Circular], + "type": { + "kind": 10, + "parent": [Circular], + }, + }, + { + "kind": 32, + "name": "onStoppedNotification", + "parent": [Circular], + "type": { + "kind": 10, + "parent": [Circular], + }, + }, + { + "kind": 32, + "name": "Promise", + "parent": [Circular], + "type": { + "kind": 11, + "parent": [Circular], + }, + }, + { + "kind": 32, + "name": "useDeprecatedSynchronousErrorHandling", + "parent": [Circular], + "type": { + "jit": {}, + "kind": 7, + "origin": { + "kind": 13, + "literal": false, + }, + "parent": [Circular], + }, + }, + { + "kind": 32, + "name": "useDeprecatedNextContext", + "parent": [Circular], + "type": { + "jit": {}, + "kind": 7, + "origin": { + "kind": 13, + "literal": false, + }, + "parent": [Circular], + }, + }, + ], + } + `); +}); + +test('declares scoped variable', () => { + const res = transpile( + { + app: `import { map } from 'rxjs/operators'; + + type A = typeof map; + `, + }, + undefined, + { + inlineExternalLibraryImports: { + 'rxjs/operators': ['map'], + }, + }, + ); + + expect(res.app).toContain('__ɵΩrxjs_operators = {}'); +}); + +test('function type alias', () => { + const res = transpile( + { + app: `import { map } from 'rxjs/operators'; + + type A = typeof map; + `, + }, + undefined, + { + inlineExternalLibraryImports: { + 'rxjs/operators': ['map'], + }, + }, + ); + + expect(res.app).toContain('__ɵΩrxjs_operators.map = ['); + expect(res.app).toContain('() => __ɵΩrxjs_operators.map'); +}); + +test('typeOf function type alias', () => { + const res = transpileAndRun( + { + app: `import { map } from 'rxjs/operators'; + import { typeOf } from '@deepkit/type'; + + typeOf(); + `, + }, + undefined, + { + inlineExternalLibraryImports: { + 'rxjs/operators': ['map'], + }, + }, + ) as TypeFunction; + + expect(res).toMatchObject({ + kind: 25, + typeName: undefined, + type: { + jit: {}, + kind: 5, + origin: { + kind: 13, + literal: 'value', + }, + }, + }); +}); + +test('class type var', () => { + const res = transpile( + { + app: `import { Observable } from 'rxjs'; + + type A = Observable; + `, + }, + undefined, + { + inlineExternalLibraryImports: { + rxjs: ['Observable'], + }, + }, + ); + + expect(res.app).toMatchInlineSnapshot(` + ""use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + const __ɵΩrxjs = {}; + __ɵΩrxjs.Observable = ['T', () => __ɵΩrxjs.Observable, 'source', () => __ɵΩrxjs.Operator, 'operator', () => __ɵΩrxjs.Observable, 'this', () => __ɵΩrxjs.Subscriber, 'subscriber', () => __ɵΩrxjs.TeardownLogic, '', 'subscribe', 'constructor', 'args', 'create', () => __ɵΩrxjs.Operator, () => __ɵΩrxjs.Observable, 'lift', () => __ΩPartial, () => __ɵΩrxjs.Observer, 'value', 'observerOrNext', () => __ɵΩrxjs.Subscription, 'next', 'forEach', () => __ɵΩrxjs.Observable, 'pipe', 'toPromise', () => __ɵΩrxjs.Subscribable, 'Observable', 'b!PP"7"-J3#P"e"!o$#-J3%PPPe$!7&2\\'Pe$!7(2)n*/+2,8"0-P"@2."/+3/sPe"!"o0#2%8P"7102PPe#!o4"o3"Pe$!25$/+J268P770,PPe#!25$/+28$\`09PPe#!7:0;PPe#!-J\`0<5e!!o="x"w>y']; + const __ΩPartial = ['T', 'l+e#!e"!fRb!Pde"!gN#"y']; + __ɵΩrxjs.Operator = ['T', 'R', () => __ɵΩrxjs.Subscriber, 'subscriber', 'source', () => __ɵΩrxjs.TeardownLogic, 'call', 'Operator', 'b!b"PPPe$"7#2$"2%n&1\\'Mw(y']; + __ɵΩrxjs.Subscriber = ['T', () => __ɵΩrxjs.Subscription, 'x', '', 'next', 'e', 'error', 'complete', () => __ɵΩrxjs.Subscriber, 'create', 'isStopped', () => __ɵΩrxjs.Subscriber, () => __ɵΩrxjs.Observer, 'destination', () => __ɵΩrxjs.Subscriber, () => __ɵΩrxjs.Observer, 'constructor', 'value', 'err', 'unsubscribe', '_next', '_error', '_complete', () => __ɵΩrxjs.Observer, 'Subscriber', 'b!P7"PPe#!2#8$/$2%8P"2&8$/$2\\'8P$/$2(8Pe#!7)0*s)3+ __ɵΩrxjs.Subscription, () => __ɵΩrxjs.Unsubscribable, '', 'PP7!n"P$/#$Jy']; + __ɵΩrxjs.Observer = ['T', 'value', '', 'next', 'err', 'error', 'complete', 'Observer', 'b!PPe#!2"$/#4$P"2%$/#4&P$/#4\\'Mw(y']; + __ɵΩrxjs.Subscription = [() => __ɵΩrxjs.Subscription, 'EMPTY', 'closed', '', 'initialTeardown', 'constructor', 'unsubscribe', () => __ɵΩrxjs.TeardownLogic, 'teardown', 'add', () => __ΩExclude, () => __ɵΩrxjs.TeardownLogic, 'remove', () => __ɵΩrxjs.SubscriptionLike, 'Subscription', 'P7!3"s)3#PPP$/$-J2%8"0&P$0\\'Pn(2)$0*Pn,$o+#2)$0-5n.x"w/y']; + __ɵΩrxjs.Subscribable = ['T', () => __ΩPartial, () => __ɵΩrxjs.Observer, 'observer', () => __ɵΩrxjs.Unsubscribable, 'subscribe', 'Subscribable', 'b!PPe#!o#"o""2$n%1&Mw\\'y']; + const __ΩExclude = ['T', 'U', 'l6!Re$!RPe#!e$"qk#%QRb!b"Pde"!p)y']; + __ɵΩrxjs.Unsubscribable = ['unsubscribe', 'Unsubscribable', 'PP$1!Mw"y']; + __ɵΩrxjs.SubscriptionLike = [() => __ɵΩrxjs.Unsubscribable, 'unsubscribe', 'closed', 'SubscriptionLike', 'Pn!P$1")4#9Mw$y']; + const rxjs_1 = require("rxjs"); + const __ΩA = [() => __ɵΩrxjs.Observable, 'P#7!y']; + " + `); +}); + +test('runtime type name clashing', () => { + const res = transpile( + { + app: `import { Observable } from 'rxjs'; + + type Subscribable = any; + + type A = Observable; + `, + }, + undefined, + { + inlineExternalLibraryImports: { + rxjs: ['Observable'], + }, + }, + ); + + expect(res.app).toContain('__ɵΩrxjs.Subscribable = ['); + expect(res.app).toContain('const __ΩSubscribable = ['); +}); + +test('class typeOf', () => { + const res = transpileAndRun( + { + app: `import { Observable } from 'rxjs'; + import { typeOf } from '@deepkit/type'; + + typeOf>(); + `, + }, + undefined, + { + inlineExternalLibraryImports: { + rxjs: ['Observable'], + }, + }, + ) as TypeClass; + + console.log(res); +}); + +test('only a single type is transformed', () => { + const res = transpile( + { + app: `import { ConfigEnv, CorsOrigin } from 'vite'; + + type A = ConfigEnv; + + type B = CorsOrigin; + `, + }, + undefined, + { + inlineExternalLibraryImports: { + vite: ['ConfigEnv'], + }, + }, + ); + + expect(res.app).toContain('__ɵΩvite.ConfigEnv = ['); + expect(res.app).not.toContain('__ɵΩvite.CorsOrigin = ['); +}); + +test('interface typeOf', () => { + const res = transpileAndRun( + { + app: `import { ConfigEnv, CorsOrigin } from 'vite'; + import { typeOf } from '@deepkit/type'; + + typeOf(); + `, + }, + undefined, + { + inlineExternalLibraryImports: { + vite: ['ConfigEnv'], + }, + }, + ); + + expect(res).toMatchInlineSnapshot(` + { + "annotations": {}, + "id": 2, + "kind": 30, + "typeName": "ConfigEnv", + "types": [ + { + "kind": 32, + "name": "command", + "parent": [Circular], + "type": { + "kind": 23, + "parent": [Circular], + "types": [ + { + "kind": 13, + "literal": "build", + "parent": [Circular], + }, + { + "kind": 13, + "literal": "serve", + "parent": [Circular], + }, + ], + }, + }, + { + "kind": 32, + "name": "mode", + "parent": [Circular], + "type": { + "kind": 5, + "parent": [Circular], + }, + }, + { + "kind": 32, + "name": "ssrBuild", + "optional": true, + "parent": [Circular], + "type": { + "kind": 7, + "parent": [Circular], + }, + }, + ], + } + `); +}); + +test('inline all external type imports for package', () => { + const res = transpile( + { + app: `import { ConfigEnv, CorsOrigin } from 'vite'; + import { typeOf } from '@deepkit/type'; + + type A = ConfigEnv; + type B = CorsOrigin; + `, + }, + undefined, + { + inlineExternalLibraryImports: { + vite: true, + }, + }, + ); + + expect(res.app).toContain('__ɵΩvite.ConfigEnv = ['); + expect(res.app).toContain('__ɵΩvite.CorsOrigin = ['); +}); diff --git a/packages/type-compiler/tests/utils.ts b/packages/type-compiler/tests/utils.ts index 4eaf827ae..08df56f39 100644 --- a/packages/type-compiler/tests/utils.ts +++ b/packages/type-compiler/tests/utils.ts @@ -1,9 +1,11 @@ -import * as ts from 'typescript'; -import { createSourceFile, getPreEmitDiagnostics, ScriptTarget, ScriptKind, TransformationContext } from 'typescript'; import { createFSBackedSystem, createVirtualCompilerHost, knownLibFilesForCompilerOptions } from '@typescript/vfs'; -import { ReflectionTransformer } from '../src/compiler.js'; import { readFileSync } from 'fs'; import { dirname, join } from 'path'; +import * as ts from 'typescript'; +import { ScriptKind, ScriptTarget, TransformationContext, createSourceFile, getPreEmitDiagnostics } from 'typescript'; + +import { ReflectionTransformer } from '../src/compiler.js'; +import { ReflectionConfig } from '../src/config.js'; const defaultLibLocation = dirname(require.resolve('typescript')) + '/'; //node_modules/typescript/lib/ @@ -22,11 +24,14 @@ function readLibs(compilerOptions: ts.CompilerOptions, files: Map, options: ts.CompilerOptions = {}): Record { - const compilerOptions: ts.CompilerOptions = { +export function transform( + files: Record, + compilerOptions: ts.CompilerOptions = {}, + reflectionOptions?: ReflectionConfig, +): Record { + compilerOptions = { ...defaultCompilerOptions, target: ts.ScriptTarget.ES2016, allowNonTsExtensions: true, @@ -34,7 +39,7 @@ export function transform(files: Record, options: ts.CompilerOpt moduleResolution: ts.ModuleResolutionKind.NodeJs, experimentalDecorators: true, esModuleInterop: true, - ...options + ...compilerOptions, }; const fsMap = new Map(); @@ -45,14 +50,29 @@ export function transform(files: Record, options: ts.CompilerOpt const res: Record = {}; for (const [fileName, source] of Object.entries(files)) { - const sourceFile = createSourceFile(fullPath(fileName), source, compilerOptions.target || ScriptTarget.ES2018, true, ScriptKind.TS); + const sourceFile = createSourceFile( + fullPath(fileName), + source, + compilerOptions.target || ScriptTarget.ES2018, + true, + ScriptKind.TS, + ); host.updateFile(sourceFile); } for (const fileName of Object.keys(files)) { const sourceFile = host.compilerHost.getSourceFile(fullPath(fileName), ScriptTarget.ES2022); if (!sourceFile) continue; - const transform = ts.transform(sourceFile, [(context) => (node) => new ReflectionTransformer(context).forHost(host.compilerHost).withReflection({reflection: 'default'}).transformSourceFile(node)]); + const transform = ts.transform(sourceFile, [ + context => node => + new ReflectionTransformer(context) + .forHost(host.compilerHost) + .withReflection({ + reflection: 'default', + ...reflectionOptions, + }) + .transformSourceFile(node), + ]); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const code = printer.printNode(ts.EmitHint.SourceFile, transform.transformed[0], transform.transformed[0]); res[fileName] = code; @@ -64,16 +84,24 @@ export function transform(files: Record, options: ts.CompilerOpt /** * The first entry in files is executed as main script */ -export function transpileAndRun(files: Record, options: ts.CompilerOptions = {}): any { - const source = transpile(files, options); +export function transpileAndRun( + files: Record, + compilerOptions: ts.CompilerOptions = {}, + reflectionOptions?: ReflectionConfig, +): any { + const source = transpile(files, compilerOptions, reflectionOptions); console.log('transpiled', source); const first = Object.keys(files)[0]; return eval(source[first]); } -export function transpile(files: Record, options: ts.CompilerOptions = {}): Record { - const compilerOptions: ts.CompilerOptions = { +export function transpile( + files: Record, + compilerOptions: ts.CompilerOptions = {}, + reflectionOptions?: ReflectionConfig, +): any { + compilerOptions = { ...defaultCompilerOptions, target: ts.ScriptTarget.ES2015, allowNonTsExtensions: true, @@ -82,7 +110,7 @@ export function transpile(files: Record, options: ts.CompilerOpt experimentalDecorators: true, esModuleInterop: true, skipLibCheck: true, - ...options + ...compilerOptions, }; const fsMap = new Map(); @@ -105,11 +133,23 @@ export function transpile(files: Record, options: ts.CompilerOpt } const res: Record = {}; - program.emit(undefined, (fileName, data) => { - res[fileName.slice(__dirname.length + 1).replace(/\.js$/, '')] = data; - }, undefined, undefined, { - before: [(context: TransformationContext) => new ReflectionTransformer(context).forHost(host.compilerHost).withReflection({reflection: 'default'})], - }); + program.emit( + undefined, + (fileName, data) => { + res[fileName.slice(__dirname.length + 1).replace(/\.js$/, '')] = data; + }, + undefined, + undefined, + { + before: [ + (context: TransformationContext) => + new ReflectionTransformer(context).forHost(host.compilerHost).withReflection({ + reflection: 'default', + ...reflectionOptions, + }), + ], + }, + ); return res; } diff --git a/packages/type/src/reflection/processor.ts b/packages/type/src/reflection/processor.ts index 193625366..5fc282e1c 100644 --- a/packages/type/src/reflection/processor.ts +++ b/packages/type/src/reflection/processor.ts @@ -7,33 +7,22 @@ * * You should have received a copy of the MIT License along with this program. */ +import { ClassType, isArray, isClass, isFunction, stringifyValueWithType } from '@deepkit/core'; +import { MappedModifier, ReflectionOp } from '@deepkit/type-spec'; +import { isWithDeferredDecorators } from '../decorator.js'; +import { isExtendable } from './extends.js'; +import { ReflectionClass, TData } from './reflection.js'; +import { state } from './state.js'; import { Annotations, - applyScheduledAnnotations, CartesianProduct, - copyAndSetParent, - defaultAnnotation, - flattenUnionTypes, - getAnnotations, - getMember, - indexAccess, - isMember, - isPrimitive, - isSameType, - isType, - isTypeIncluded, - isWithAnnotations, - merge, - narrowOriginalLiteral, ReflectionKind, ReflectionVisibility, - stringifyType, Type, TypeBaseMember, TypeCallSignature, TypeClass, - typeDecorators, TypeEnumMember, TypeFunction, TypeIndexSignature, @@ -49,27 +38,39 @@ import { TypeTemplateLiteral, TypeTupleMember, TypeUnion, + applyScheduledAnnotations, + copyAndSetParent, + defaultAnnotation, + flattenUnionTypes, + getAnnotations, + getMember, + indexAccess, + isMember, + isPrimitive, + isSameType, + isType, + isTypeIncluded, + isWithAnnotations, + merge, + narrowOriginalLiteral, + stringifyType, + typeDecorators, unboxUnion, validationAnnotation, widenLiteral, } from './type.js'; -import { MappedModifier, ReflectionOp } from '@deepkit/type-spec'; -import { isExtendable } from './extends.js'; -import { ClassType, isArray, isClass, isFunction, stringifyValueWithType } from '@deepkit/core'; -import { isWithDeferredDecorators } from '../decorator.js'; -import { ReflectionClass, TData } from './reflection.js'; -import { state } from './state.js'; export type RuntimeStackEntry = Type | Object | (() => ClassType | Object) | string | number | boolean | bigint; -export type Packed = (RuntimeStackEntry | string)[] & { __is?: (data: any) => boolean } & { __type?: Type } & { __unpack?: PackStruct }; +export type Packed = (RuntimeStackEntry | string)[] & { + __is?: (data: any) => boolean; +} & { __type?: Type } & { __unpack?: PackStruct }; export class PackStruct { constructor( public ops: ReflectionOp[] = [], public stack: RuntimeStackEntry[] = [], - ) { - } + ) {} } function unpackOps(decodedOps: ReflectionOp[], encodedOPs: string): void { @@ -91,7 +92,7 @@ export function pack(packOrOps: PackStruct | ReflectionOp[]): Packed { if (!isArray(packOrOps)) { if (packOrOps.stack.length) { - return [...packOrOps.stack as RuntimeStackEntry[], encodedOps]; + return [...(packOrOps.stack as RuntimeStackEntry[]), encodedOps]; } } @@ -124,7 +125,11 @@ function isPack(o: any): o is Packed { * This is the slow path, using the full type virtual machine to resolve the type. * If you want to handle some fast paths (including cache), try using resolveReceiveType() instead. */ -export function resolveRuntimeType(o: ClassType | Function | Packed | any, args: any[] = [], options?: ReflectOptions): Type { +export function resolveRuntimeType( + o: ClassType | Function | Packed | any, + args: any[] = [], + options?: ReflectOptions, +): Type { const type = Processor.get().reflect(o, args, options || { reuseCached: true }); if (isType(type)) { @@ -191,7 +196,11 @@ function assignResult(ref: Type, result: T, assignParents: boole // for (const member of ref.arguments) member.parent = ref; // } - if (ref.kind === ReflectionKind.function || ref.kind === ReflectionKind.method || ref.kind === ReflectionKind.methodSignature) { + if ( + ref.kind === ReflectionKind.function || + ref.kind === ReflectionKind.method || + ref.kind === ReflectionKind.methodSignature + ) { ref.return.parent = ref; for (const member of ref.parameters) member.parent = ref as any; } @@ -214,7 +223,13 @@ function isConditionTruthy(condition: Type | number): boolean { function createProgram(options: Partial, inputs?: RuntimeStackEntry[]): Program { const program: Program = { active: true, - frame: { index: 0, startIndex: -1, inputs: inputs || [], variables: 0, previous: undefined }, + frame: { + index: 0, + startIndex: -1, + inputs: inputs || [], + variables: 0, + previous: undefined, + }, stack: options.stack || [], stackPointer: options.stackPointer ?? -1, program: 0, @@ -231,13 +246,14 @@ function createProgram(options: Partial, inputs?: RuntimeStackEntry[]): object: options.object, }; - if (options.initialStack) for (let i = 0; i < options.initialStack.length; i++) { - if (i < program.stack.length) { - program.stack[i] = options.initialStack[i]; - } else { - program.stack.push(options.initialStack[i]); + if (options.initialStack) + for (let i = 0; i < options.initialStack.length; i++) { + if (i < program.stack.length) { + program.stack[i] = options.initialStack[i]; + } else { + program.stack.push(options.initialStack[i]); + } } - } program.stackPointer = options.initialStack ? options.initialStack.length - 1 : -1; program.frame.startIndex = program.stackPointer; @@ -245,7 +261,11 @@ function createProgram(options: Partial, inputs?: RuntimeStackEntry[]): return program; } -function isValidCacheEntry(current: Program, object: ClassType | Function | Packed | any, inputs: RuntimeStackEntry[] = []): Program | undefined { +function isValidCacheEntry( + current: Program, + object: ClassType | Function | Packed | any, + inputs: RuntimeStackEntry[] = [], +): Program | undefined { if (current.object === object) { //issue a new reference if inputs are the same //todo: when a function has a default, this is not set in current.inputs, and could fail when it differs to given inputs @@ -265,7 +285,11 @@ function isValidCacheEntry(current: Program, object: ClassType | Function | Pack return; } -function findExistingProgram(current: Program | undefined, object: ClassType | Function | Packed | any, inputs: RuntimeStackEntry[] = []) { +function findExistingProgram( + current: Program | undefined, + object: ClassType | Function | Packed | any, + inputs: RuntimeStackEntry[] = [], +) { let checks = 0; while (current) { if (current.object === object) { @@ -318,7 +342,7 @@ export class Processor { static typeProcessor?: Processor; static get(): Processor { - return Processor.typeProcessor ||= new Processor(); + return (Processor.typeProcessor ||= new Processor()); } private cache: Program[] = []; @@ -344,7 +368,11 @@ export class Processor { // object: undefined, }; - reflect(object: ClassType | Function | Packed | any, inputs: RuntimeStackEntry[] = [], options: ReflectOptions = {}): Type { + reflect( + object: ClassType | Function | Packed | any, + inputs: RuntimeStackEntry[] = [], + options: ReflectOptions = {}, + ): Type { const start = Date.now(); const result = this._reflect(object, inputs, options); @@ -355,18 +383,26 @@ export class Processor { return result; } - _reflect(object: ClassType | Function | Packed | any, inputs: RuntimeStackEntry[] = [], options: ReflectOptions = {}): Type { + _reflect( + object: ClassType | Function | Packed | any, + inputs: RuntimeStackEntry[] = [], + options: ReflectOptions = {}, + ): Type { const packed: Packed | undefined = isPack(object) ? object : object.__type; if (!packed) { if (isFunction(object) && object.length === 0) { //functions without any type annotations do not have the overhead of an assigned __type return { kind: ReflectionKind.function, - function: object, name: object.name, - parameters: [], return: { kind: ReflectionKind.any } + function: object, + name: object.name, + parameters: [], + return: { kind: ReflectionKind.any }, }; } - throw new Error(`No valid runtime type for ${stringifyValueWithType(object)} given. Is @deepkit/type-compiler correctly installed? Execute deepkit-type-install to check`); + throw new Error( + `No valid runtime type for ${stringifyValueWithType(object)} given. Is @deepkit/type-compiler correctly installed? Execute deepkit-type-install to check`, + ); } for (let i = 0; i < inputs.length; i++) { @@ -415,7 +451,7 @@ export class Processor { } // process.stdout.write(`${options.reuseCached} Cache miss ${stringifyValueWithType(object)}(...${inputs.length})\n`); - const pack = packed.__unpack ||= unpack(packed); + const pack = (packed.__unpack ||= unpack(packed)); const program = createProgram({ ops: pack.ops, initialStack: pack.stack, object }, inputs); const type = this.runProgram(program); type.typeName ||= options.typeName; @@ -436,7 +472,12 @@ export class Processor { return type; } - run(ops: ReflectionOp[], initialStack: RuntimeStackEntry[], inputs: RuntimeStackEntry[] = [], object?: ClassType | Function | Packed | any): Type { + run( + ops: ReflectionOp[], + initialStack: RuntimeStackEntry[], + inputs: RuntimeStackEntry[] = [], + object?: ClassType | Function | Packed | any, + ): Type { return this.runProgram(createProgram({ ops, initialStack, object }, inputs)); } @@ -472,615 +513,946 @@ export class Processor { protected loop(until?: Program): Type | RuntimeStackEntry { let result = this.program.stack[0]; - programLoop: - while (this.program.active) { - const program = this.program; - // process.stdout.write(`jump to program: ${stringifyValueWithType(program.object)}\n`); - for (; program.program < program.end; program.program++) { - const op = program.ops[program.program]; - - // process.stdout.write(`[${program.depth}:${program.frame.index}] step ${program.program} ${ReflectionOp[op]}\n`); - switch (op) { - case ReflectionOp.string: - this.pushType({ kind: ReflectionKind.string }); - break; - case ReflectionOp.number: - this.pushType({ kind: ReflectionKind.number }); - break; - case ReflectionOp.numberBrand: - const ref = this.eatParameter() as number; - this.pushType({ kind: ReflectionKind.number, brand: ref }); - break; - case ReflectionOp.boolean: - this.pushType({ kind: ReflectionKind.boolean }); - break; - case ReflectionOp.void: - this.pushType({ kind: ReflectionKind.void }); - break; - case ReflectionOp.unknown: - this.pushType({ kind: ReflectionKind.unknown }); - break; - case ReflectionOp.object: - this.pushType({ kind: ReflectionKind.object }); - break; - case ReflectionOp.never: - this.pushType({ kind: ReflectionKind.never }); - break; - case ReflectionOp.undefined: - this.pushType({ kind: ReflectionKind.undefined }); - break; - case ReflectionOp.bigint: - this.pushType({ kind: ReflectionKind.bigint }); - break; - case ReflectionOp.symbol: - this.pushType({ kind: ReflectionKind.symbol }); - break; - case ReflectionOp.null: - this.pushType({ kind: ReflectionKind.null }); - break; - case ReflectionOp.any: - this.pushType({ kind: ReflectionKind.any }); - break; - case ReflectionOp.literal: { - const ref = this.eatParameter() as number; - this.pushType({ kind: ReflectionKind.literal, literal: program.stack[ref] as string | number | boolean | bigint }); - break; - } - case ReflectionOp.templateLiteral: { - this.handleTemplateLiteral(); - break; - } - case ReflectionOp.date: - this.pushType({ kind: ReflectionKind.class, classType: Date, types: [] }); - break; - case ReflectionOp.uint8Array: - this.pushType({ kind: ReflectionKind.class, classType: Uint8Array, types: [] }); - break; - case ReflectionOp.int8Array: - this.pushType({ kind: ReflectionKind.class, classType: Int8Array, types: [] }); - break; - case ReflectionOp.uint8ClampedArray: - this.pushType({ kind: ReflectionKind.class, classType: Uint8ClampedArray, types: [] }); - break; - case ReflectionOp.uint16Array: - this.pushType({ kind: ReflectionKind.class, classType: Uint16Array, types: [] }); - break; - case ReflectionOp.int16Array: - this.pushType({ kind: ReflectionKind.class, classType: Int16Array, types: [] }); - break; - case ReflectionOp.uint32Array: - this.pushType({ kind: ReflectionKind.class, classType: Uint32Array, types: [] }); - break; - case ReflectionOp.int32Array: - this.pushType({ kind: ReflectionKind.class, classType: Int32Array, types: [] }); - break; - case ReflectionOp.float32Array: - this.pushType({ kind: ReflectionKind.class, classType: Float32Array, types: [] }); - break; - case ReflectionOp.float64Array: - this.pushType({ kind: ReflectionKind.class, classType: Float64Array, types: [] }); - break; - case ReflectionOp.bigInt64Array: - this.pushType({ - kind: ReflectionKind.class, - classType: 'undefined' !== typeof BigInt64Array ? BigInt64Array : class BigInt64ArrayNotAvailable { - }, - types: [] - }); - break; - case ReflectionOp.arrayBuffer: - this.pushType({ kind: ReflectionKind.class, classType: ArrayBuffer, types: [] }); - break; - case ReflectionOp.class: { - const types = this.popFrame() as Type[]; - let t = { kind: ReflectionKind.class, id: state.nominalId++, classType: Object, types: [] } as TypeClass; - - function add(member: Type) { - if (member.kind === ReflectionKind.propertySignature) { - member = { - ...member, - parent: t, - visibility: ReflectionVisibility.public, - kind: ReflectionKind.property - } as TypeProperty; - } else if (member.kind === ReflectionKind.methodSignature) { - member = { - ...member, - parent: t, - visibility: ReflectionVisibility.public, - kind: ReflectionKind.method - } as TypeMethod; - } + programLoop: while (this.program.active) { + const program = this.program; + // process.stdout.write(`jump to program: ${stringifyValueWithType(program.object)}\n`); + for (; program.program < program.end; program.program++) { + const op = program.ops[program.program]; - switch (member.kind) { - case ReflectionKind.indexSignature: { - //todo, replace the old one? - t.types.push(member); - break; + // process.stdout.write(`[${program.depth}:${program.frame.index}] step ${program.program} ${ReflectionOp[op]}\n`); + switch (op) { + case ReflectionOp.string: + this.pushType({ kind: ReflectionKind.string }); + break; + case ReflectionOp.number: + this.pushType({ kind: ReflectionKind.number }); + break; + case ReflectionOp.numberBrand: + const ref = this.eatParameter() as number; + this.pushType({ + kind: ReflectionKind.number, + brand: ref, + }); + break; + case ReflectionOp.boolean: + this.pushType({ kind: ReflectionKind.boolean }); + break; + case ReflectionOp.void: + this.pushType({ kind: ReflectionKind.void }); + break; + case ReflectionOp.unknown: + this.pushType({ kind: ReflectionKind.unknown }); + break; + case ReflectionOp.object: + this.pushType({ kind: ReflectionKind.object }); + break; + case ReflectionOp.never: + this.pushType({ kind: ReflectionKind.never }); + break; + case ReflectionOp.undefined: + this.pushType({ kind: ReflectionKind.undefined }); + break; + case ReflectionOp.bigint: + this.pushType({ kind: ReflectionKind.bigint }); + break; + case ReflectionOp.symbol: + this.pushType({ kind: ReflectionKind.symbol }); + break; + case ReflectionOp.null: + this.pushType({ kind: ReflectionKind.null }); + break; + case ReflectionOp.any: + this.pushType({ kind: ReflectionKind.any }); + break; + case ReflectionOp.literal: { + const ref = this.eatParameter() as number; + this.pushType({ + kind: ReflectionKind.literal, + literal: program.stack[ref] as string | number | boolean | bigint, + }); + break; + } + case ReflectionOp.templateLiteral: { + this.handleTemplateLiteral(); + break; + } + case ReflectionOp.date: + this.pushType({ + kind: ReflectionKind.class, + classType: Date, + types: [], + }); + break; + case ReflectionOp.uint8Array: + this.pushType({ + kind: ReflectionKind.class, + classType: Uint8Array, + types: [], + }); + break; + case ReflectionOp.int8Array: + this.pushType({ + kind: ReflectionKind.class, + classType: Int8Array, + types: [], + }); + break; + case ReflectionOp.uint8ClampedArray: + this.pushType({ + kind: ReflectionKind.class, + classType: Uint8ClampedArray, + types: [], + }); + break; + case ReflectionOp.uint16Array: + this.pushType({ + kind: ReflectionKind.class, + classType: Uint16Array, + types: [], + }); + break; + case ReflectionOp.int16Array: + this.pushType({ + kind: ReflectionKind.class, + classType: Int16Array, + types: [], + }); + break; + case ReflectionOp.uint32Array: + this.pushType({ + kind: ReflectionKind.class, + classType: Uint32Array, + types: [], + }); + break; + case ReflectionOp.int32Array: + this.pushType({ + kind: ReflectionKind.class, + classType: Int32Array, + types: [], + }); + break; + case ReflectionOp.float32Array: + this.pushType({ + kind: ReflectionKind.class, + classType: Float32Array, + types: [], + }); + break; + case ReflectionOp.float64Array: + this.pushType({ + kind: ReflectionKind.class, + classType: Float64Array, + types: [], + }); + break; + case ReflectionOp.bigInt64Array: + this.pushType({ + kind: ReflectionKind.class, + classType: + 'undefined' !== typeof BigInt64Array + ? BigInt64Array + : class BigInt64ArrayNotAvailable {}, + types: [], + }); + break; + case ReflectionOp.arrayBuffer: + this.pushType({ + kind: ReflectionKind.class, + classType: ArrayBuffer, + types: [], + }); + break; + case ReflectionOp.class: { + const types = this.popFrame() as Type[]; + let t = { + kind: ReflectionKind.class, + id: state.nominalId++, + classType: Object, + types: [], + } as TypeClass; + + function add(member: Type) { + if (member.kind === ReflectionKind.propertySignature) { + member = { + ...member, + parent: t, + visibility: ReflectionVisibility.public, + kind: ReflectionKind.property, + } as TypeProperty; + } else if (member.kind === ReflectionKind.methodSignature) { + member = { + ...member, + parent: t, + visibility: ReflectionVisibility.public, + kind: ReflectionKind.method, + } as TypeMethod; + } + + switch (member.kind) { + case ReflectionKind.indexSignature: { + //todo, replace the old one? + t.types.push(member); + break; + } + case ReflectionKind.property: + case ReflectionKind.method: { + const existing = t.types.findIndex( + v => + (v.kind === ReflectionKind.property || v.kind === ReflectionKind.method) && + v.name === (member as TypeProperty | TypeMethod).name, + ); + if (existing !== -1) { + //remove entry, since we replace it + t.types.splice(existing, 1); } - case ReflectionKind.property: - case ReflectionKind.method: { - const existing = t.types.findIndex(v => (v.kind === ReflectionKind.property || v.kind === ReflectionKind.method) && v.name === (member as TypeProperty | TypeMethod).name); - if (existing !== -1) { - //remove entry, since we replace it - t.types.splice(existing, 1); - } - t.types.push(member); - - if (member.kind === ReflectionKind.method && member.name === 'constructor') { - for (const parameter of member.parameters) { - if (parameter.visibility !== undefined || parameter.readonly) { - const property = { - kind: ReflectionKind.property, - name: parameter.name, - visibility: parameter.visibility, - default: parameter.default, - type: parameter.type, - } as TypeProperty; - if (parameter.optional) property.optional = true; - if (parameter.readonly) property.readonly = true; - parameter.type.parent = property; - add(property); - } + t.types.push(member); + + if (member.kind === ReflectionKind.method && member.name === 'constructor') { + for (const parameter of member.parameters) { + if (parameter.visibility !== undefined || parameter.readonly) { + const property = { + kind: ReflectionKind.property, + name: parameter.name, + visibility: parameter.visibility, + default: parameter.default, + type: parameter.type, + } as TypeProperty; + if (parameter.optional) property.optional = true; + if (parameter.readonly) property.readonly = true; + parameter.type.parent = property; + add(property); } } - break; } + break; } } + } - for (const member of types) { - switch (member.kind) { - case ReflectionKind.objectLiteral: - case ReflectionKind.class: { - for (const sub of member.types) add(sub); - break; - } - case ReflectionKind.indexSignature: - case ReflectionKind.property: - case ReflectionKind.method: { - add(member); - } + for (const member of types) { + switch (member.kind) { + case ReflectionKind.objectLiteral: + case ReflectionKind.class: { + for (const sub of member.types) add(sub); + break; + } + case ReflectionKind.indexSignature: + case ReflectionKind.property: + case ReflectionKind.method: { + add(member); } - // if (member.kind === ReflectionKind.property) member.type = widenLiteral(member.type); } - const args = program.frame.inputs.filter(isType); + // if (member.kind === ReflectionKind.property) member.type = widenLiteral(member.type); + } + const args = program.frame.inputs.filter(isType); - for (const member of t.types) member.parent = t; - if (t.arguments) for (const member of t.arguments) member.parent = t; + for (const member of t.types) member.parent = t; + if (t.arguments) for (const member of t.arguments) member.parent = t; - if (args.length) t.arguments = args; - t.typeArguments = program.typeParameters; + if (args.length) t.arguments = args; + t.typeArguments = program.typeParameters; - this.pushType(t); - break; + this.pushType(t); + break; + } + case ReflectionOp.widen: { + const current = program.stack[program.stackPointer] as Type; + if (current.kind === ReflectionKind.literal) { + this.pushType(widenLiteral(this.pop() as TypeLiteral)); } - case ReflectionOp.widen: { - const current = (program.stack[program.stackPointer] as Type); - if (current.kind === ReflectionKind.literal) { - this.pushType(widenLiteral(this.pop() as TypeLiteral)); - } - break; + break; + } + case ReflectionOp.classExtends: { + const argsNumber = this.eatParameter() as number; + const typeArguments: Type[] = []; + for (let i = 0; i < argsNumber; i++) { + typeArguments.push(this.pop() as Type); } - case ReflectionOp.classExtends: { - const argsNumber = this.eatParameter() as number; - const typeArguments: Type[] = []; - for (let i = 0; i < argsNumber; i++) { - typeArguments.push(this.pop() as Type); - } - (program.stack[program.stackPointer] as TypeClass).extendsArguments = typeArguments; + (program.stack[program.stackPointer] as TypeClass).extendsArguments = typeArguments; - break; + break; + } + case ReflectionOp.implements: { + const argsNumber = this.eatParameter() as number; + const types: Type[] = []; + for (let i = 0; i < argsNumber; i++) { + types.push(this.pop() as Type); } - case ReflectionOp.implements: { - const argsNumber = this.eatParameter() as number; - const types: Type[] = []; - for (let i = 0; i < argsNumber; i++) { - types.push(this.pop() as Type); - } - (program.stack[program.stackPointer] as TypeClass).implements = types; + (program.stack[program.stackPointer] as TypeClass).implements = types; + break; + } + case ReflectionOp.parameter: { + const ref = this.eatParameter() as number; + const t: Type = { + kind: ReflectionKind.parameter, + parent: undefined as any, + name: program.stack[ref] as string, + type: this.pop() as Type, + }; + t.type.parent = t; + this.pushType(t); + break; + } + case ReflectionOp.functionReference: + case ReflectionOp.classReference: { + const ref = this.eatParameter() as number; + const classOrFunction = resolveFunction(program.stack[ref] as Function, program.object); + // will be packed if external library import + const packed = isPack(classOrFunction); + const inputs = this.popFrame() as Type[]; + if (!classOrFunction) { + this.pushType({ kind: ReflectionKind.unknown }); break; } - case ReflectionOp.parameter: { - const ref = this.eatParameter() as number; - const t: Type = { kind: ReflectionKind.parameter, parent: undefined as any, name: program.stack[ref] as string, type: this.pop() as Type }; - t.type.parent = t; - this.pushType(t); - break; - } - case ReflectionOp.functionReference: - case ReflectionOp.classReference: { - const ref = this.eatParameter() as number; - const classOrFunction = resolveFunction(program.stack[ref] as Function, program.object); - const inputs = this.popFrame() as Type[]; - if (!classOrFunction) { - this.pushType({ kind: ReflectionKind.unknown }); - break; + + if (!packed && !classOrFunction.__type) { + if (op === ReflectionOp.classReference) { + this.pushType({ + kind: ReflectionKind.class, + classType: classOrFunction, + typeArguments: inputs, + types: [], + }); + } else if (op === ReflectionOp.functionReference) { + this.pushType({ + kind: ReflectionKind.function, + function: classOrFunction, + parameters: [], + return: { kind: ReflectionKind.unknown }, + }); } + } else { + //when it's just a simple reference resolution like typeOf() then enable cache re-use (so always the same type is returned) + const directReference = !!( + this.isEnded() && + program.previous && + program.previous.end === 0 + ); + const result = this.reflect(classOrFunction, inputs, { + inline: !directReference, + reuseCached: directReference, + }); + if (directReference) program.directReturn = true; + this.push(result, program); - if (!classOrFunction.__type) { - if (op === ReflectionOp.classReference) { - this.pushType({ kind: ReflectionKind.class, classType: classOrFunction, typeArguments: inputs, types: [] }); - } else if (op === ReflectionOp.functionReference) { - this.pushType({ kind: ReflectionKind.function, function: classOrFunction, parameters: [], return: { kind: ReflectionKind.unknown } }); - } - } else { - //when it's just a simple reference resolution like typeOf() then enable cache re-use (so always the same type is returned) - const directReference = !!(this.isEnded() && program.previous && program.previous.end === 0); - const result = this.reflect(classOrFunction, inputs, { inline: !directReference, reuseCached: directReference }); - if (directReference) program.directReturn = true; - this.push(result, program); - - if (isWithAnnotations(result) && inputs.length) { - result.typeArguments = result.typeArguments || []; - for (let i = 0; i < inputs.length; i++) { - result.typeArguments[i] = inputs[i]; - } + if (isWithAnnotations(result) && inputs.length) { + result.typeArguments = result.typeArguments || []; + for (let i = 0; i < inputs.length; i++) { + result.typeArguments[i] = inputs[i]; } + } - //this.reflect/run might create another program onto the stack. switch to it if so - if (this.program !== program) { - //continue to next this.program. - program.program++; //manual increment as the for loop would normally do that - continue programLoop; - } + //this.reflect/run might create another program onto the stack. switch to it if so + if (this.program !== program) { + //continue to next this.program. + program.program++; //manual increment as the for loop would normally do that + continue programLoop; } - break; } - case ReflectionOp.enum: { - const types = this.popFrame() as TypeEnumMember[]; - const enumType: { [name: string]: string | number } = {}; - - let i = 0; - for (const type of types) { - if (type.default) { - const v = type.default(); - enumType[type.name] = v; - if ('number' === typeof v) { - i = v + 1; - } - } else { - enumType[type.name] = i++; + break; + } + case ReflectionOp.enum: { + const types = this.popFrame() as TypeEnumMember[]; + const enumType: { [name: string]: string | number } = {}; + + let i = 0; + for (const type of types) { + if (type.default) { + const v = type.default(); + enumType[type.name] = v; + if ('number' === typeof v) { + i = v + 1; } + } else { + enumType[type.name] = i++; } - const values = Object.values(enumType); - const t: Type = { kind: ReflectionKind.enum, enum: enumType, values, indexType: getEnumType(values) }; - this.pushType(t); - break; - } - case ReflectionOp.enumMember: { - const name = program.stack[this.eatParameter() as number] as string | (() => string); - this.pushType({ - kind: ReflectionKind.enumMember, - parent: undefined as any, - name: isFunction(name) ? name() : name - }); - break; - } - case ReflectionOp.tuple: { - this.handleTuple(); - break; } - case ReflectionOp.tupleMember: { - const t: TypeTupleMember = { - kind: ReflectionKind.tupleMember, type: this.pop() as Type, - parent: undefined as any, - }; - t.type.parent = t; - this.pushType(t); - break; - } - case ReflectionOp.namedTupleMember: { - const name = program.stack[this.eatParameter() as number] as string; - const t: Type = { - kind: ReflectionKind.tupleMember, type: this.pop() as Type, - parent: undefined as any, - name: isFunction(name) ? name() : name - }; - t.type.parent = t; - this.pushType(t); - break; - } - case ReflectionOp.rest: { - const t: Type = { - kind: ReflectionKind.rest, - parent: undefined as any, - type: this.pop() as Type, - }; - t.type.parent = t; - this.pushType(t); - break; - } - case ReflectionOp.regexp: { - this.pushType({ kind: ReflectionKind.regexp }); - break; - } - case ReflectionOp.typeParameter: - case ReflectionOp.typeParameterDefault: { - const nameRef = this.eatParameter() as number; - program.typeParameters = program.typeParameters || []; - let type = program.frame.inputs[program.frame.variables++]; - - if (op === ReflectionOp.typeParameterDefault) { - const defaultValue = this.pop(); - if (type === undefined) { - type = defaultValue; - } - } - + const values = Object.values(enumType); + const t: Type = { + kind: ReflectionKind.enum, + enum: enumType, + values, + indexType: getEnumType(values), + }; + this.pushType(t); + break; + } + case ReflectionOp.enumMember: { + const name = program.stack[this.eatParameter() as number] as string | (() => string); + this.pushType({ + kind: ReflectionKind.enumMember, + parent: undefined as any, + name: isFunction(name) ? name() : name, + }); + break; + } + case ReflectionOp.tuple: { + this.handleTuple(); + break; + } + case ReflectionOp.tupleMember: { + const t: TypeTupleMember = { + kind: ReflectionKind.tupleMember, + type: this.pop() as Type, + parent: undefined as any, + }; + t.type.parent = t; + this.pushType(t); + break; + } + case ReflectionOp.namedTupleMember: { + const name = program.stack[this.eatParameter() as number] as string; + const t: Type = { + kind: ReflectionKind.tupleMember, + type: this.pop() as Type, + parent: undefined as any, + name: isFunction(name) ? name() : name, + }; + t.type.parent = t; + this.pushType(t); + break; + } + case ReflectionOp.rest: { + const t: Type = { + kind: ReflectionKind.rest, + parent: undefined as any, + type: this.pop() as Type, + }; + t.type.parent = t; + this.pushType(t); + break; + } + case ReflectionOp.regexp: { + this.pushType({ kind: ReflectionKind.regexp }); + break; + } + case ReflectionOp.typeParameter: + case ReflectionOp.typeParameterDefault: { + const nameRef = this.eatParameter() as number; + program.typeParameters = program.typeParameters || []; + let type = program.frame.inputs[program.frame.variables++]; + + if (op === ReflectionOp.typeParameterDefault) { + const defaultValue = this.pop(); if (type === undefined) { - //generic not instantiated - program.typeParameters.push({ kind: ReflectionKind.any, typeParameter: true } as any); - this.pushType({ kind: ReflectionKind.typeParameter, name: program.stack[nameRef] as string }); - } else { - program.typeParameters.push(type as Type); - this.pushType(type as Type); + type = defaultValue; } - break; - } - case ReflectionOp.set: { - const t: Type = { kind: ReflectionKind.class, classType: Set, arguments: [this.pop() as Type], types: [] }; - t.arguments![0].parent = t; - this.pushType(t); - break; } - case ReflectionOp.map: { - const value = this.pop() as Type; - const key = this.pop() as Type; - const t: TypeClass = { kind: ReflectionKind.class, classType: Map, arguments: [key, value], types: [] }; - t.arguments![0].parent = t; - t.arguments![1].parent = t; - this.pushType(t); - break; - } - case ReflectionOp.promise: { - const type = this.pop() as Type; - const t: TypePromise = { kind: ReflectionKind.promise, type }; - t.type.parent = t; - this.pushType(t); - break; + + if (type === undefined) { + //generic not instantiated + program.typeParameters.push({ + kind: ReflectionKind.any, + typeParameter: true, + } as any); + this.pushType({ + kind: ReflectionKind.typeParameter, + name: program.stack[nameRef] as string, + }); + } else { + program.typeParameters.push(type as Type); + this.pushType(type as Type); } - case ReflectionOp.union: { - const types = this.popFrame() as Type[]; - const flattened = flattenUnionTypes(types); - const t: Type = unboxUnion({ kind: ReflectionKind.union, types: flattened }); - if (t.kind === ReflectionKind.union) { - for (const member of t.types) { - member.parent = t; - } + break; + } + case ReflectionOp.set: { + const t: Type = { + kind: ReflectionKind.class, + classType: Set, + arguments: [this.pop() as Type], + types: [], + }; + t.arguments![0].parent = t; + this.pushType(t); + break; + } + case ReflectionOp.map: { + const value = this.pop() as Type; + const key = this.pop() as Type; + const t: TypeClass = { + kind: ReflectionKind.class, + classType: Map, + arguments: [key, value], + types: [], + }; + t.arguments![0].parent = t; + t.arguments![1].parent = t; + this.pushType(t); + break; + } + case ReflectionOp.promise: { + const type = this.pop() as Type; + const t: TypePromise = { + kind: ReflectionKind.promise, + type, + }; + t.type.parent = t; + this.pushType(t); + break; + } + case ReflectionOp.union: { + const types = this.popFrame() as Type[]; + const flattened = flattenUnionTypes(types); + const t: Type = unboxUnion({ + kind: ReflectionKind.union, + types: flattened, + }); + if (t.kind === ReflectionKind.union) { + for (const member of t.types) { + member.parent = t; } - this.pushType(t); - break; - } - case ReflectionOp.intersection: { - let t = this.handleIntersection(); - if (t) this.pushType(t); - break; - } - case ReflectionOp.callSignature: - case ReflectionOp.function: { - const types = this.popFrame() as Type[]; - const name = program.stack[this.eatParameter() as number] as string; - - const returnType = types.length > 0 ? types[types.length - 1] as Type : { kind: ReflectionKind.any } as Type; - const parameters = types.length > 1 ? types.slice(0, -1) as TypeParameter[] : []; - - let t = op === ReflectionOp.callSignature ? { - kind: ReflectionKind.callSignature, - return: returnType, - parameters - } as TypeCallSignature : { - kind: ReflectionKind.function, - name: name || undefined, - return: returnType, - parameters - } as TypeFunction; - t.return.parent = t; - for (const member of t.parameters) member.parent = t; - this.pushType(t); - break; - } - case ReflectionOp.array: { - const t: Type = { kind: ReflectionKind.array, type: this.pop() as Type }; - t.type.parent = t; - this.pushType(t); - break; } - case ReflectionOp.property: - case ReflectionOp.propertySignature: { - const name = program.stack[this.eatParameter() as number] as number | string | symbol | (() => symbol); - let type = this.pop() as Type; - let isOptional = false; - - if (type.kind === ReflectionKind.union && type.types.length === 2) { - const undefinedType = type.types.find(v => v.kind === ReflectionKind.undefined); - const restType = type.types.find(v => v.kind !== ReflectionKind.null && v.kind !== ReflectionKind.undefined); - if (restType && undefinedType) { - type = restType; - isOptional = true; - } - } - - const property = { - kind: op === ReflectionOp.propertySignature ? ReflectionKind.propertySignature : ReflectionKind.property, - type, - name: isFunction(name) ? name() : name - } as TypeProperty | TypePropertySignature; - - if (isOptional) { - property.optional = true; + this.pushType(t); + break; + } + case ReflectionOp.intersection: { + let t = this.handleIntersection(); + if (t) this.pushType(t); + break; + } + case ReflectionOp.callSignature: + case ReflectionOp.function: { + const types = this.popFrame() as Type[]; + const name = program.stack[this.eatParameter() as number] as string; + + const returnType = + types.length > 0 + ? (types[types.length - 1] as Type) + : ({ kind: ReflectionKind.any } as Type); + const parameters = types.length > 1 ? (types.slice(0, -1) as TypeParameter[]) : []; + + let t = + op === ReflectionOp.callSignature + ? ({ + kind: ReflectionKind.callSignature, + return: returnType, + parameters, + } as TypeCallSignature) + : ({ + kind: ReflectionKind.function, + name: name || undefined, + return: returnType, + parameters, + } as TypeFunction); + t.return.parent = t; + for (const member of t.parameters) member.parent = t; + this.pushType(t); + break; + } + case ReflectionOp.array: { + const t: Type = { + kind: ReflectionKind.array, + type: this.pop() as Type, + }; + t.type.parent = t; + this.pushType(t); + break; + } + case ReflectionOp.property: + case ReflectionOp.propertySignature: { + const name = program.stack[this.eatParameter() as number] as + | number + | string + | symbol + | (() => symbol); + let type = this.pop() as Type; + let isOptional = false; + + if (type.kind === ReflectionKind.union && type.types.length === 2) { + const undefinedType = type.types.find(v => v.kind === ReflectionKind.undefined); + const restType = type.types.find( + v => v.kind !== ReflectionKind.null && v.kind !== ReflectionKind.undefined, + ); + if (restType && undefinedType) { + type = restType; + isOptional = true; } + } - if (op === ReflectionOp.property) { - (property as TypeProperty).visibility = ReflectionVisibility.public; - } + const property = { + kind: + op === ReflectionOp.propertySignature + ? ReflectionKind.propertySignature + : ReflectionKind.property, + type, + name: isFunction(name) ? name() : name, + } as TypeProperty | TypePropertySignature; - property.type.parent = property; - this.pushType(property); - break; - } - case ReflectionOp.method: - case ReflectionOp.methodSignature: { - const name = program.stack[this.eatParameter() as number] as number | string | symbol; - const types = this.popFrame() as Type[]; - const returnType = types.length > 0 ? types[types.length - 1] as Type : { kind: ReflectionKind.any } as Type; - const parameters: TypeParameter[] = types.length > 1 ? types.slice(0, -1) as TypeParameter[] : []; - - let t: TypeMethod | TypeMethodSignature = op === ReflectionOp.method - ? { kind: ReflectionKind.method, parent: undefined as any, visibility: ReflectionVisibility.public, name, return: returnType, parameters } - : { kind: ReflectionKind.methodSignature, parent: undefined as any, name, return: returnType, parameters }; - t.return.parent = t; - for (const member of t.parameters) member.parent = t; - this.pushType(t); - break; - } - case ReflectionOp.optional: - (program.stack[program.stackPointer] as TypeBaseMember | TypeTupleMember).optional = true; - break; - case ReflectionOp.readonly: - (program.stack[program.stackPointer] as TypeBaseMember).readonly = true; - break; - case ReflectionOp.public: - (program.stack[program.stackPointer] as TypeBaseMember).visibility = ReflectionVisibility.public; - break; - case ReflectionOp.protected: - (program.stack[program.stackPointer] as TypeBaseMember).visibility = ReflectionVisibility.protected; - break; - case ReflectionOp.private: - (program.stack[program.stackPointer] as TypeBaseMember).visibility = ReflectionVisibility.private; - break; - case ReflectionOp.abstract: - (program.stack[program.stackPointer] as TypeBaseMember).abstract = true; - break; - case ReflectionOp.static: - (program.stack[program.stackPointer] as TypeBaseMember).static = true; - break; - case ReflectionOp.defaultValue: - (program.stack[program.stackPointer] as TypeProperty | TypeEnumMember | TypeParameter).default = program.stack[this.eatParameter() as number] as () => any; - break; - case ReflectionOp.description: - (program.stack[program.stackPointer] as TypeProperty).description = program.stack[this.eatParameter() as number] as string; - break; - case ReflectionOp.typeName: { - const type = (program.stack[program.stackPointer] as Type); - const name = program.stack[this.eatParameter() as number] as string; - if (type.typeName) { - type.originTypes = [{ typeName: type.typeName, typeArguments: type.typeArguments }, ...(type.originTypes || [])]; - type.typeArguments = undefined; - } - type.typeName = name; - break; - } - case ReflectionOp.indexSignature: { - const type = this.pop() as Type; - const index = this.pop() as Type; - const t: Type = { kind: ReflectionKind.indexSignature, parent: undefined as any, index, type }; - t.type.parent = t; - t.index.parent = t; - this.pushType(t); - break; - } - case ReflectionOp.objectLiteral: { - let t = { - kind: ReflectionKind.objectLiteral, - id: state.nominalId++, - types: [] - } as TypeObjectLiteral; - - const frameTypes = this.popFrame() as (TypeIndexSignature | TypePropertySignature | TypeMethodSignature | TypeObjectLiteral | TypeCallSignature)[]; - pushObjectLiteralTypes(t, frameTypes); - for (const member of t.types) member.parent = t; - this.pushType(t); - break; - } - // case ReflectionOp.pointer: { - // this.push(program.stack[this.eatParameter() as number]); - // break; - // } - case ReflectionOp.distribute: { - this.handleDistribute(program); - break; - } - case ReflectionOp.condition: { - const right = this.pop() as Type; - const left = this.pop() as Type; - const condition = this.pop() as Type | number; - this.popFrame(); - isConditionTruthy(condition) ? this.pushType(left) : this.pushType(right); - break; + if (isOptional) { + property.optional = true; } - case ReflectionOp.jumpCondition: { - const leftProgram = this.eatParameter() as number; - const rightProgram = this.eatParameter() as number; - const condition = this.pop() as Type | number; - const truthy = isConditionTruthy(condition); - this.call(truthy ? leftProgram : rightProgram); - break; + + if (op === ReflectionOp.property) { + (property as TypeProperty).visibility = ReflectionVisibility.public; } - case ReflectionOp.infer: { - const frameOffset = this.eatParameter() as number; - const stackEntryIndex = this.eatParameter() as number; - const frame = program.frame; - let last: Type = { kind: ReflectionKind.unknown }; - this.push({ - kind: ReflectionKind.infer, set: (type: Type) => { - if (last.kind !== ReflectionKind.unknown) { - if (last.kind === ReflectionKind.union || last.kind === ReflectionKind.intersection) { - if (!isTypeIncluded(last.types, type)) { - last.types.push(type); - } - } else { - if (type.parent && type.parent.kind === ReflectionKind.parameter) { - last = { kind: ReflectionKind.intersection, types: [last, type] }; - } else { - last = { kind: ReflectionKind.union, types: [last, type] }; - } + property.type.parent = property; + this.pushType(property); + break; + } + case ReflectionOp.method: + case ReflectionOp.methodSignature: { + const name = program.stack[this.eatParameter() as number] as number | string | symbol; + const types = this.popFrame() as Type[]; + const returnType = + types.length > 0 + ? (types[types.length - 1] as Type) + : ({ kind: ReflectionKind.any } as Type); + const parameters: TypeParameter[] = + types.length > 1 ? (types.slice(0, -1) as TypeParameter[]) : []; + + let t: TypeMethod | TypeMethodSignature = + op === ReflectionOp.method + ? { + kind: ReflectionKind.method, + parent: undefined as any, + visibility: ReflectionVisibility.public, + name, + return: returnType, + parameters, + } + : { + kind: ReflectionKind.methodSignature, + parent: undefined as any, + name, + return: returnType, + parameters, + }; + t.return.parent = t; + for (const member of t.parameters) member.parent = t; + this.pushType(t); + break; + } + case ReflectionOp.optional: + (program.stack[program.stackPointer] as TypeBaseMember | TypeTupleMember).optional = true; + break; + case ReflectionOp.readonly: + (program.stack[program.stackPointer] as TypeBaseMember).readonly = true; + break; + case ReflectionOp.public: + (program.stack[program.stackPointer] as TypeBaseMember).visibility = + ReflectionVisibility.public; + break; + case ReflectionOp.protected: + (program.stack[program.stackPointer] as TypeBaseMember).visibility = + ReflectionVisibility.protected; + break; + case ReflectionOp.private: + (program.stack[program.stackPointer] as TypeBaseMember).visibility = + ReflectionVisibility.private; + break; + case ReflectionOp.abstract: + (program.stack[program.stackPointer] as TypeBaseMember).abstract = true; + break; + case ReflectionOp.static: + (program.stack[program.stackPointer] as TypeBaseMember).static = true; + break; + case ReflectionOp.defaultValue: + (program.stack[program.stackPointer] as TypeProperty | TypeEnumMember | TypeParameter).default = + program.stack[this.eatParameter() as number] as () => any; + break; + case ReflectionOp.description: + (program.stack[program.stackPointer] as TypeProperty).description = program.stack[ + this.eatParameter() as number + ] as string; + break; + case ReflectionOp.typeName: { + const type = program.stack[program.stackPointer] as Type; + const name = program.stack[this.eatParameter() as number] as string; + if (type.typeName) { + type.originTypes = [ + { + typeName: type.typeName, + typeArguments: type.typeArguments, + }, + ...(type.originTypes || []), + ]; + type.typeArguments = undefined; + } + type.typeName = name; + break; + } + case ReflectionOp.indexSignature: { + const type = this.pop() as Type; + const index = this.pop() as Type; + const t: Type = { + kind: ReflectionKind.indexSignature, + parent: undefined as any, + index, + type, + }; + t.type.parent = t; + t.index.parent = t; + this.pushType(t); + break; + } + case ReflectionOp.objectLiteral: { + let t = { + kind: ReflectionKind.objectLiteral, + id: state.nominalId++, + types: [], + } as TypeObjectLiteral; + + const frameTypes = this.popFrame() as ( + | TypeIndexSignature + | TypePropertySignature + | TypeMethodSignature + | TypeObjectLiteral + | TypeCallSignature + )[]; + pushObjectLiteralTypes(t, frameTypes); + for (const member of t.types) member.parent = t; + this.pushType(t); + break; + } + // case ReflectionOp.pointer: { + // this.push(program.stack[this.eatParameter() as number]); + // break; + // } + case ReflectionOp.distribute: { + this.handleDistribute(program); + break; + } + case ReflectionOp.condition: { + const right = this.pop() as Type; + const left = this.pop() as Type; + const condition = this.pop() as Type | number; + this.popFrame(); + isConditionTruthy(condition) ? this.pushType(left) : this.pushType(right); + break; + } + case ReflectionOp.jumpCondition: { + const leftProgram = this.eatParameter() as number; + const rightProgram = this.eatParameter() as number; + const condition = this.pop() as Type | number; + const truthy = isConditionTruthy(condition); + this.call(truthy ? leftProgram : rightProgram); + break; + } + case ReflectionOp.infer: { + const frameOffset = this.eatParameter() as number; + const stackEntryIndex = this.eatParameter() as number; + const frame = program.frame; + + let last: Type = { kind: ReflectionKind.unknown }; + this.push({ + kind: ReflectionKind.infer, + set: (type: Type) => { + if (last.kind !== ReflectionKind.unknown) { + if ( + last.kind === ReflectionKind.union || + last.kind === ReflectionKind.intersection + ) { + if (!isTypeIncluded(last.types, type)) { + last.types.push(type); } } else { - last = type; + if (type.parent && type.parent.kind === ReflectionKind.parameter) { + last = { + kind: ReflectionKind.intersection, + types: [last, type], + }; + } else { + last = { + kind: ReflectionKind.union, + types: [last, type], + }; + } } + } else { + last = type; + } - if (frameOffset === 0) { - program.stack[frame.startIndex + 1 + stackEntryIndex] = last; - } else if (frameOffset === 1) { - program.stack[frame.previous!.startIndex + 1 + stackEntryIndex] = last; - } else if (frameOffset === 2) { - program.stack[frame.previous!.previous!.startIndex + 1 + stackEntryIndex] = last; - } else if (frameOffset === 3) { - program.stack[frame.previous!.previous!.previous!.startIndex + 1 + stackEntryIndex] = last; - } else if (frameOffset === 4) { - program.stack[frame.previous!.previous!.previous!.previous!.startIndex + 1 + stackEntryIndex] = last; - } else { - let current = frame; - for (let i = 0; i < frameOffset; i++) current = current.previous!; - program.stack[current.startIndex + 1 + stackEntryIndex] = last; - } + if (frameOffset === 0) { + program.stack[frame.startIndex + 1 + stackEntryIndex] = last; + } else if (frameOffset === 1) { + program.stack[frame.previous!.startIndex + 1 + stackEntryIndex] = last; + } else if (frameOffset === 2) { + program.stack[frame.previous!.previous!.startIndex + 1 + stackEntryIndex] = last; + } else if (frameOffset === 3) { + program.stack[ + frame.previous!.previous!.previous!.startIndex + 1 + stackEntryIndex + ] = last; + } else if (frameOffset === 4) { + program.stack[ + frame.previous!.previous!.previous!.previous!.startIndex + 1 + stackEntryIndex + ] = last; + } else { + let current = frame; + for (let i = 0; i < frameOffset; i++) current = current.previous!; + program.stack[current.startIndex + 1 + stackEntryIndex] = last; } - } as TypeInfer); - break; + }, + } as TypeInfer); + break; + } + case ReflectionOp.extends: { + const right = this.pop() as string | number | boolean | Type; + const left = this.pop() as string | number | boolean | Type; + const result = isExtendable(left, right); + this.pushType({ + kind: ReflectionKind.literal, + literal: result, + }); + break; + } + case ReflectionOp.indexAccess: { + this.handleIndexAccess(); + break; + } + case ReflectionOp.typeof: { + const param1 = this.eatParameter() as number; + const fn = program.stack[param1] as () => any; + const value = fn(); + + //typeInfer calls Processor.run() and changes this.program, so handle it correctly + const result = typeInfer(value); + this.push(result, program); + + //this.reflect/run might create another program onto the stack. switch to it if so + if (this.program !== program) { + //continue to next this.program. + program.program++; //manual increment as the for loop would normally do that + continue programLoop; } - case ReflectionOp.extends: { - const right = this.pop() as string | number | boolean | Type; - const left = this.pop() as string | number | boolean | Type; - const result = isExtendable(left, right); - this.pushType({ kind: ReflectionKind.literal, literal: result }); - break; + break; + } + case ReflectionOp.keyof: { + this.handleKeyOf(); + break; + } + case ReflectionOp.var: { + this.push({ kind: ReflectionKind.unknown, var: true }); + program.frame.variables++; + break; + } + case ReflectionOp.mappedType2: { + this.handleMappedType(program, true); + break; + } + case ReflectionOp.mappedType: { + this.handleMappedType(program); + break; + } + case ReflectionOp.loads: { + const frameOffset = this.eatParameter() as number; + const stackEntryIndex = this.eatParameter() as number; + if (frameOffset === 0) { + this.push(program.stack[program.frame.startIndex + 1 + stackEntryIndex]); + } else if (frameOffset === 1) { + this.push(program.stack[program.frame.previous!.startIndex + 1 + stackEntryIndex]); + } else if (frameOffset === 2) { + this.push( + program.stack[program.frame.previous!.previous!.startIndex + 1 + stackEntryIndex], + ); + } else if (frameOffset === 3) { + this.push( + program.stack[ + program.frame.previous!.previous!.previous!.startIndex + 1 + stackEntryIndex + ], + ); + } else if (frameOffset === 4) { + this.push( + program.stack[ + program.frame.previous!.previous!.previous!.previous!.startIndex + + 1 + + stackEntryIndex + ], + ); + } else { + let current = program.frame; + for (let i = 0; i < frameOffset; i++) current = current.previous!; + this.push(program.stack[current.startIndex + 1 + stackEntryIndex]); } - case ReflectionOp.indexAccess: { - this.handleIndexAccess(); - break; + break; + } + case ReflectionOp.arg: { + //used by InlineRuntimeType too + const arg = this.eatParameter() as number; + const t = program.stack[arg] as + | Type + | ReflectionClass + | number + | string + | boolean + | bigint; + if (t instanceof ReflectionClass) { + this.push({ + ...t.type, + typeName: t.getClassName(), + }); + } else if ( + 'string' === typeof t || + 'number' === typeof t || + 'boolean' === typeof t || + 'bigint' === typeof t + ) { + this.push({ + kind: ReflectionKind.literal, + literal: t, + }); + } else { + this.push(t); } - case ReflectionOp.typeof: { - const param1 = this.eatParameter() as number; - const fn = program.stack[param1] as () => any; - const value = fn(); - - //typeInfer calls Processor.run() and changes this.program, so handle it correctly - const result = typeInfer(value); + break; + } + case ReflectionOp.return: { + this.returnFrame(); + break; + } + case ReflectionOp.frame: { + this.pushFrame(); + break; + } + case ReflectionOp.moveFrame: { + const type = this.pop(); + this.popFrame(); + if (type) this.push(type); + break; + } + case ReflectionOp.jump: { + const arg = this.eatParameter() as number; + program.program = arg - 1; //-1 because next iteration does program++ + break; + } + case ReflectionOp.call: { + const programPointer = this.eatParameter() as number; + this.call(programPointer); + break; + } + case ReflectionOp.nominal: { + const t = program.stack[program.stackPointer] as Type; + //program ended, so assign new nominal id to objectLiteral or class + t.id = state.nominalId++; + break; + } + case ReflectionOp.inline: { + const pPosition = this.eatParameter() as number; + const pOrFn = program.stack[pPosition] as number | Packed | (() => Packed); + const p = isFunction(pOrFn) ? pOrFn() : pOrFn; + // process.stdout.write(`inline ${pOrFn.toString()}\n`); + if (p === undefined) { + // console.log('inline with invalid reference', pOrFn.toString()); + this.push({ kind: ReflectionKind.unknown }); + } else if ('number' === typeof p) { + //self circular reference, usually a 0, which indicates we put the result of the current program as the type on the stack. + this.push(program.resultType); + } else { + //when it's just a simple reference resolution like typeOf() then don't issue a new reference (no inline: true) + const directReference = !!( + this.isEnded() && + program.previous && + program.previous.end === 0 + ); + const result = this.reflect(p, [], { + inline: !directReference, + reuseCached: directReference, + }); + if (directReference) program.directReturn = true; this.push(result, program); //this.reflect/run might create another program onto the stack. switch to it if so @@ -1089,206 +1461,112 @@ export class Processor { program.program++; //manual increment as the for loop would normally do that continue programLoop; } - break; - } - case ReflectionOp.keyof: { - this.handleKeyOf(); - break; - } - case ReflectionOp.var: { - this.push({ kind: ReflectionKind.unknown, var: true }); - program.frame.variables++; - break; - } - case ReflectionOp.mappedType2: { - this.handleMappedType(program, true); - break; } - case ReflectionOp.mappedType: { - this.handleMappedType(program); - break; - } - case ReflectionOp.loads: { - const frameOffset = this.eatParameter() as number; - const stackEntryIndex = this.eatParameter() as number; - if (frameOffset === 0) { - this.push(program.stack[program.frame.startIndex + 1 + stackEntryIndex]); - } else if (frameOffset === 1) { - this.push(program.stack[program.frame.previous!.startIndex + 1 + stackEntryIndex]); - } else if (frameOffset === 2) { - this.push(program.stack[program.frame.previous!.previous!.startIndex + 1 + stackEntryIndex]); - } else if (frameOffset === 3) { - this.push(program.stack[program.frame.previous!.previous!.previous!.startIndex + 1 + stackEntryIndex]); - } else if (frameOffset === 4) { - this.push(program.stack[program.frame.previous!.previous!.previous!.previous!.startIndex + 1 + stackEntryIndex]); - } else { - let current = program.frame; - for (let i = 0; i < frameOffset; i++) current = current.previous!; - this.push(program.stack[current.startIndex + 1 + stackEntryIndex]); - } - break; - } - case ReflectionOp.arg: { - //used by InlineRuntimeType too - const arg = this.eatParameter() as number; - const t = program.stack[arg] as Type | ReflectionClass | number | string | boolean | bigint; - if (t instanceof ReflectionClass) { - this.push({ ...t.type, typeName: t.getClassName() }); - } else if ('string' === typeof t || 'number' === typeof t || 'boolean' === typeof t || 'bigint' === typeof t) { - this.push({ kind: ReflectionKind.literal, literal: t }); - } else { - this.push(t); - } - break; - } - case ReflectionOp.return: { - this.returnFrame(); - break; - } - case ReflectionOp.frame: { - this.pushFrame(); - break; - } - case ReflectionOp.moveFrame: { - const type = this.pop(); - this.popFrame(); - if (type) this.push(type); - break; - } - case ReflectionOp.jump: { - const arg = this.eatParameter() as number; - program.program = arg - 1; //-1 because next iteration does program++ - break; - } - case ReflectionOp.call: { - const programPointer = this.eatParameter() as number; - this.call(programPointer); - break; - } - case ReflectionOp.nominal: { - const t = program.stack[program.stackPointer] as Type; - //program ended, so assign new nominal id to objectLiteral or class - t.id = state.nominalId++; - break; + break; + } + case ReflectionOp.inlineCall: { + const pPosition = this.eatParameter() as number; + const argumentSize = this.eatParameter() as number; + const inputs: Type[] = []; + for (let i = 0; i < argumentSize; i++) { + let input = this.pop() as Type; + if ( + (input.kind === ReflectionKind.never || input.kind === ReflectionKind.unknown) && + program.inputs[i] + ) + input = program.inputs[i] as Type; + inputs.unshift(input); } - case ReflectionOp.inline: { - const pPosition = this.eatParameter() as number; - const pOrFn = program.stack[pPosition] as number | Packed | (() => Packed); - const p = isFunction(pOrFn) ? pOrFn() : pOrFn; - // process.stdout.write(`inline ${pOrFn.toString()}\n`); - if (p === undefined) { - // console.log('inline with invalid reference', pOrFn.toString()); - this.push({ kind: ReflectionKind.unknown }); - } else if ('number' === typeof p) { + const pOrFn = program.stack[pPosition] as number | Packed | (() => Packed); + const p = isFunction(pOrFn) ? pOrFn() : pOrFn; + if (p === undefined) { + // console.log('inline with invalid reference', pOrFn.toString()); + this.push({ kind: ReflectionKind.unknown }); + } else if ('number' === typeof p) { + if (argumentSize === 0) { //self circular reference, usually a 0, which indicates we put the result of the current program as the type on the stack. this.push(program.resultType); } else { - //when it's just a simple reference resolution like typeOf() then don't issue a new reference (no inline: true) - const directReference = !!(this.isEnded() && program.previous && program.previous.end === 0); - const result = this.reflect(p, [], { inline: !directReference, reuseCached: directReference }); - if (directReference) program.directReturn = true; - this.push(result, program); - - //this.reflect/run might create another program onto the stack. switch to it if so - if (this.program !== program) { - //continue to next this.program. - program.program++; //manual increment as the for loop would normally do that - continue programLoop; - } - } - break; - } - case ReflectionOp.inlineCall: { - const pPosition = this.eatParameter() as number; - const argumentSize = this.eatParameter() as number; - const inputs: Type[] = []; - for (let i = 0; i < argumentSize; i++) { - let input = this.pop() as Type; - if ((input.kind === ReflectionKind.never || input.kind === ReflectionKind.unknown) && program.inputs[i]) input = program.inputs[i] as Type; - inputs.unshift(input); - } - const pOrFn = program.stack[pPosition] as number | Packed | (() => Packed); - const p = isFunction(pOrFn) ? pOrFn() : pOrFn; - if (p === undefined) { - // console.log('inline with invalid reference', pOrFn.toString()); - this.push({ kind: ReflectionKind.unknown }); - } else if ('number' === typeof p) { - if (argumentSize === 0) { - //self circular reference, usually a 0, which indicates we put the result of the current program as the type on the stack. - this.push(program.resultType); + const found = findExistingProgram(this.program, program.object, inputs); + if (found) { + this.push(createRef(found), program); } else { - const found = findExistingProgram(this.program, program.object, inputs); - if (found) { - this.push(createRef(found), program); - } else { - // process.stdout.write(`Cache miss ${pOrFn.toString()}(...${inputs.length})\n`); - //execute again the current program - const nextProgram = createProgram({ + // process.stdout.write(`Cache miss ${pOrFn.toString()}(...${inputs.length})\n`); + //execute again the current program + const nextProgram = createProgram( + { ops: program.ops, initialStack: program.initialStack, - }, inputs); - this.push(this.runProgram(nextProgram), program); + }, + inputs, + ); + this.push(this.runProgram(nextProgram), program); - //continue to next this.program that was assigned by runProgram() - program.program++; //manual increment as the for loop would normally do that - continue programLoop; - } + //continue to next this.program that was assigned by runProgram() + program.program++; //manual increment as the for loop would normally do that + continue programLoop; } - } else { - const result = this.reflect(p, inputs); + } + } else { + const result = this.reflect(p, inputs); - if (isWithAnnotations(result) && inputs.length) { - result.typeArguments = result.typeArguments || []; - for (let i = 0; i < inputs.length; i++) { - result.typeArguments[i] = inputs[i]; - } + if (isWithAnnotations(result) && inputs.length) { + result.typeArguments = result.typeArguments || []; + for (let i = 0; i < inputs.length; i++) { + result.typeArguments[i] = inputs[i]; } + } - this.push(result, program); + this.push(result, program); - //this.reflect/run might create another program onto the stack. switch to it if so - if (this.program !== program) { - //continue to next this.program. - program.program++; //manual increment as the for loop would normally do that - continue programLoop; - } + //this.reflect/run might create another program onto the stack. switch to it if so + if (this.program !== program) { + //continue to next this.program. + program.program++; //manual increment as the for loop would normally do that + continue programLoop; } - break; } + break; } } + } - result = narrowOriginalLiteral(program.stack[program.stackPointer] as Type); - // process.stdout.write(`Done ${program.depth} in ${Date.now() - program.started}ms with ${stringifyValueWithType(program.object)} -> ${stringifyShortResolvedType(result as Type)}\n`); - - if (isType(result)) { - if (program.object) { - if (result.kind === ReflectionKind.class && result.classType === Object) { - result.classType = program.object; - applyClassDecorators(result); - } - if (result.kind === ReflectionKind.function && !result.function) { - result.function = program.object; - } + result = narrowOriginalLiteral(program.stack[program.stackPointer] as Type); + // process.stdout.write(`Done ${program.depth} in ${Date.now() - program.started}ms with ${stringifyValueWithType(program.object)} -> ${stringifyShortResolvedType(result as Type)}\n`); + + if (isType(result)) { + if ( + program.object && + // will be packed if external library import + !isPack(program.object) + ) { + if (result.kind === ReflectionKind.class && result.classType === Object) { + result.classType = program.object; + applyClassDecorators(result); } - if (!program.directReturn) { - result = assignResult(program.resultType, result as Type, !result.inlined); - applyScheduledAnnotations(program.resultType); + if (result.kind === ReflectionKind.function && !result.function) { + result.function = program.object; } } + if (!program.directReturn) { + result = assignResult(program.resultType, result as Type, !result.inlined); + applyScheduledAnnotations(program.resultType); + } + } - program.active = false; - if (program.previous) this.program = program.previous; + program.active = false; + if (program.previous) this.program = program.previous; - if (program.resultTypes) for (const ref of program.resultTypes) { + if (program.resultTypes) + for (const ref of program.resultTypes) { assignResult(ref, result as Type, false); applyScheduledAnnotations(ref); } - if (until === program) { - this.cache = []; - return result; - } + if (until === program) { + this.cache = []; + return result; } + } this.cache = []; return result; @@ -1298,11 +1576,14 @@ export class Processor { const types: TypeTupleMember[] = []; const stackTypes = this.popFrame() as Type[]; for (const type of stackTypes) { - const resolved: TypeTupleMember = type.kind === ReflectionKind.tupleMember ? type : { - kind: ReflectionKind.tupleMember, - parent: undefined as any, - type - }; + const resolved: TypeTupleMember = + type.kind === ReflectionKind.tupleMember + ? type + : { + kind: ReflectionKind.tupleMember, + parent: undefined as any, + type, + }; type.parent = resolved; if (resolved.type.kind === ReflectionKind.rest) { if (resolved.type.type.kind === ReflectionKind.tuple) { @@ -1335,11 +1616,14 @@ export class Processor { function appendAnnotations(a: Type) { if (a.annotations === annotations) return; if (a.annotations) Object.assign(annotations, a.annotations); - if (a.decorators) decorators.push(...a.decorators as TypeObjectLiteral[]); + if (a.decorators) decorators.push(...(a.decorators as TypeObjectLiteral[])); } function handleUnion(a: Type, unionType: TypeUnion): Type { - return unboxUnion({ kind: ReflectionKind.union, types: unionType.types.filter(v => isExtendable(v, a)) }); + return unboxUnion({ + kind: ReflectionKind.union, + types: unionType.types.filter(v => isExtendable(v, a)), + }); } function handleAndObject(a: Type, objectType: TypeObjectLiteral): Type { @@ -1363,7 +1647,10 @@ export class Processor { return handleUnion(a, b); } - if ((a.kind === ReflectionKind.objectLiteral || a.kind === ReflectionKind.class) && (b.kind === ReflectionKind.objectLiteral || b.kind === ReflectionKind.class)) { + if ( + (a.kind === ReflectionKind.objectLiteral || a.kind === ReflectionKind.class) && + (b.kind === ReflectionKind.objectLiteral || b.kind === ReflectionKind.class) + ) { appendAnnotations(a); appendAnnotations(b); return merge([a, b]); @@ -1400,7 +1687,13 @@ export class Processor { return isExtendable(a, b) ? a : { kind: ReflectionKind.never }; } - if (a.kind === ReflectionKind.objectLiteral || a.kind === ReflectionKind.class || a.kind === ReflectionKind.never || a.kind === ReflectionKind.unknown) return b; + if ( + a.kind === ReflectionKind.objectLiteral || + a.kind === ReflectionKind.class || + a.kind === ReflectionKind.never || + a.kind === ReflectionKind.unknown + ) + return b; if (b.annotations) { Object.assign(annotations, b.annotations); @@ -1409,27 +1702,26 @@ export class Processor { return a; } - outer: - for (const type of types) { - if (type.kind === ReflectionKind.never) continue; - if (type.kind === ReflectionKind.objectLiteral) { - for (const decorator of typeDecorators) { - if (decorator(annotations, type)) { - decorators.push(type); - continue outer; - } + outer: for (const type of types) { + if (type.kind === ReflectionKind.never) continue; + if (type.kind === ReflectionKind.objectLiteral) { + for (const decorator of typeDecorators) { + if (decorator(annotations, type)) { + decorators.push(type); + continue outer; } } - if (result.kind === ReflectionKind.never) { - result = { kind: ReflectionKind.never }; - break; - } else if (result.kind === ReflectionKind.unknown) { - result = type; - appendAnnotations(type); - } else { - result = collapse(result, type); - } } + if (result.kind === ReflectionKind.never) { + result = { kind: ReflectionKind.never }; + break; + } else if (result.kind === ReflectionKind.unknown) { + result = type; + appendAnnotations(type); + } else { + result = collapse(result, type); + } + } if (result.kind === ReflectionKind.unknown) { //type not calculated yet, so schedule annotations. Those will be applied once the type is fully computed. @@ -1465,7 +1757,10 @@ export class Processor { if (next === undefined) { //end const types = this.popFrame() as Type[]; - const result: TypeUnion = { kind: ReflectionKind.union, types: flattenUnionTypes(types) }; + const result: TypeUnion = { + kind: ReflectionKind.union, + types: flattenUnionTypes(types), + }; const t: Type = unboxUnion(result); if (t.kind === ReflectionKind.union) for (const member of t.types) member.parent = t; this.push(t); @@ -1484,7 +1779,10 @@ export class Processor { } else { const t: Type = indexAccess(left, right); if (isWithAnnotations(t)) { - t.indexAccessOrigin = { container: left as TypeObjectLiteral, index: right as Type }; + t.indexAccessOrigin = { + container: left as TypeObjectLiteral, + index: right as Type, + }; } t.parent = undefined; @@ -1495,23 +1793,58 @@ export class Processor { private handleKeyOf() { const type = this.pop() as Type; if (type.kind === ReflectionKind.objectLiteral || type.kind === ReflectionKind.class) { - const union = { kind: ReflectionKind.union, origin: type, types: [] } as TypeUnion; + const union = { + kind: ReflectionKind.union, + origin: type, + types: [], + } as TypeUnion; for (const member of type.types) { - if ((member.kind === ReflectionKind.propertySignature || member.kind === ReflectionKind.property) && member.name !== 'new') { - union.types.push({ kind: ReflectionKind.literal, literal: member.name, parent: union, origin: member } as TypeLiteral); - } else if ((member.kind === ReflectionKind.methodSignature || member.kind === ReflectionKind.method) && member.name !== 'constructor') { - union.types.push({ kind: ReflectionKind.literal, literal: member.name, parent: union, origin: member } as TypeLiteral); + if ( + (member.kind === ReflectionKind.propertySignature || member.kind === ReflectionKind.property) && + member.name !== 'new' + ) { + union.types.push({ + kind: ReflectionKind.literal, + literal: member.name, + parent: union, + origin: member, + } as TypeLiteral); + } else if ( + (member.kind === ReflectionKind.methodSignature || member.kind === ReflectionKind.method) && + member.name !== 'constructor' + ) { + union.types.push({ + kind: ReflectionKind.literal, + literal: member.name, + parent: union, + origin: member, + } as TypeLiteral); } } this.push(union); } else if (type.kind === ReflectionKind.tuple) { - const union = { kind: ReflectionKind.union, origin: type, types: [] } as TypeUnion; + const union = { + kind: ReflectionKind.union, + origin: type, + types: [], + } as TypeUnion; for (let i = 0; i < type.types.length; i++) { - union.types.push({ kind: ReflectionKind.literal, literal: i, parent: union } as TypeLiteral); + union.types.push({ + kind: ReflectionKind.literal, + literal: i, + parent: union, + } as TypeLiteral); } this.push(union); } else if (type.kind === ReflectionKind.any) { - this.push({ kind: ReflectionKind.union, types: [{ kind: ReflectionKind.string }, { kind: ReflectionKind.number }, { kind: ReflectionKind.symbol }] }); + this.push({ + kind: ReflectionKind.union, + types: [ + { kind: ReflectionKind.string }, + { kind: ReflectionKind.number }, + { kind: ReflectionKind.symbol }, + ], + }); } else { this.push({ kind: ReflectionKind.never }); } @@ -1522,7 +1855,12 @@ export class Processor { const modifier = this.eatParameter() as number; function isSimpleIndex(index: Type): boolean { - if (index.kind === ReflectionKind.string || index.kind === ReflectionKind.number || index.kind === ReflectionKind.symbol) return true; + if ( + index.kind === ReflectionKind.string || + index.kind === ReflectionKind.number || + index.kind === ReflectionKind.symbol + ) + return true; if (index.kind === ReflectionKind.union) { const types = index.types.filter(v => isSimpleIndex(v)); return types.length === 0; @@ -1532,7 +1870,9 @@ export class Processor { if (program.frame.mappedType) { let type = this.pop() as Type; - let index: Type | string | boolean | symbol | number | bigint = program.stack[program.frame.startIndex + 1] as Type; + let index: Type | string | boolean | symbol | number | bigint = program.stack[ + program.frame.startIndex + 1 + ] as Type; if (withName) { if (type.kind === ReflectionKind.tuple) { index = type.types[1].type; @@ -1547,7 +1887,12 @@ export class Processor { if (index.kind === ReflectionKind.never) { //ignore } else if (index.kind === ReflectionKind.any || isSimpleIndex(index)) { - const t: TypeIndexSignature = { kind: ReflectionKind.indexSignature, type, index, parent: undefined as any }; + const t: TypeIndexSignature = { + kind: ReflectionKind.indexSignature, + type, + index, + parent: undefined as any, + }; t.type.parent = t; t.index.parent = t; this.push(t); @@ -1558,9 +1903,17 @@ export class Processor { index = index.literal; } - const property: TypeProperty | TypePropertySignature | TypeTupleMember = type.kind === ReflectionKind.propertySignature || type.kind === ReflectionKind.property || type.kind === ReflectionKind.tupleMember - ? type - : { kind: isTuple ? ReflectionKind.tupleMember : ReflectionKind.propertySignature, name: index, type, optional } as TypePropertySignature; + const property: TypeProperty | TypePropertySignature | TypeTupleMember = + type.kind === ReflectionKind.propertySignature || + type.kind === ReflectionKind.property || + type.kind === ReflectionKind.tupleMember + ? type + : ({ + kind: isTuple ? ReflectionKind.tupleMember : ReflectionKind.propertySignature, + name: index, + type, + optional, + } as TypePropertySignature); if (property !== type) type.parent = property; if (property.type.kind !== ReflectionKind.never) { @@ -1598,7 +1951,11 @@ export class Processor { if (fromType.origin && fromType.origin.kind === ReflectionKind.tuple) { t = { kind: ReflectionKind.tuple, types: members as any[] }; } else { - t = { kind: ReflectionKind.objectLiteral, id: state.nominalId++, types: members as any[] }; + t = { + kind: ReflectionKind.objectLiteral, + id: state.nominalId++, + types: members as any[], + }; } this.push(t); @@ -1620,46 +1977,58 @@ export class Processor { } const product = cartesian.calculate(); - outer: - for (const combination of product) { - const template: TypeTemplateLiteral = { kind: ReflectionKind.templateLiteral, types: [] }; - let hasPlaceholder = false; - let lastLiteral: { kind: ReflectionKind.literal, literal: string, parent?: Type } | undefined = undefined; - //merge a combination of types, e.g. [string, 'abc', '3'] as template literal => `${string}abc3`. - for (const item of combination) { - if (item.kind === ReflectionKind.never) { - //template literals that contain a never like `prefix.${never}` are completely ignored - continue outer; - } + outer: for (const combination of product) { + const template: TypeTemplateLiteral = { + kind: ReflectionKind.templateLiteral, + types: [], + }; + let hasPlaceholder = false; + let lastLiteral: + | { + kind: ReflectionKind.literal; + literal: string; + parent?: Type; + } + | undefined = undefined; + //merge a combination of types, e.g. [string, 'abc', '3'] as template literal => `${string}abc3`. + for (const item of combination) { + if (item.kind === ReflectionKind.never) { + //template literals that contain a never like `prefix.${never}` are completely ignored + continue outer; + } - if (item.kind === ReflectionKind.literal) { - if (lastLiteral) { - lastLiteral.literal += item.literal as string + ''; - } else { - lastLiteral = { kind: ReflectionKind.literal, literal: item.literal as string + '', parent: template }; - template.types.push(lastLiteral); - } + if (item.kind === ReflectionKind.literal) { + if (lastLiteral) { + lastLiteral.literal += (item.literal as string) + ''; } else { - hasPlaceholder = true; - lastLiteral = undefined; - item.parent = template; - template.types.push(item as TypeTemplateLiteral['types'][number]); + lastLiteral = { + kind: ReflectionKind.literal, + literal: (item.literal as string) + '', + parent: template, + }; + template.types.push(lastLiteral); } + } else { + hasPlaceholder = true; + lastLiteral = undefined; + item.parent = template; + template.types.push(item as TypeTemplateLiteral['types'][number]); } + } - if (hasPlaceholder) { - if (template.types.length === 1 && template.types[0].kind === ReflectionKind.string) { - template.types[0].parent = result; - result.types.push(template.types[0]); - } else { - template.parent = result; - result.types.push(template); - } - } else if (lastLiteral) { - lastLiteral.parent = result; - result.types.push(lastLiteral); + if (hasPlaceholder) { + if (template.types.length === 1 && template.types[0].kind === ReflectionKind.string) { + template.types[0].parent = result; + result.types.push(template.types[0]); + } else { + template.parent = result; + result.types.push(template); } + } else if (lastLiteral) { + lastLiteral.parent = result; + result.types.push(lastLiteral); } + } const t: Type = unboxUnion(result); if (t.kind === ReflectionKind.union) for (const member of t.types) member.parent = t; this.pushType(t); @@ -1692,7 +2061,10 @@ export class Processor { } protected popFrame(): RuntimeStackEntry[] { - const result = this.program.stack.slice(this.program.frame.startIndex + this.program.frame.variables + 1, this.program.stackPointer + 1); + const result = this.program.stack.slice( + this.program.frame.startIndex + this.program.frame.variables + 1, + this.program.stackPointer + 1, + ); this.program.stackPointer = this.program.frame.startIndex; if (this.program.frame.previous) this.program.frame = this.program.frame.previous; return result; @@ -1741,7 +2113,13 @@ function typeInferFromContainer(container: Iterable): Type { } export function typeInfer(value: any): Type { - if ('string' === typeof value || 'number' === typeof value || 'boolean' === typeof value || 'bigint' === typeof value || 'symbol' === typeof value) { + if ( + 'string' === typeof value || + 'number' === typeof value || + 'boolean' === typeof value || + 'bigint' === typeof value || + 'symbol' === typeof value + ) { return { kind: ReflectionKind.literal, literal: value }; } else if (null === value) { return { kind: ReflectionKind.null }; @@ -1754,43 +2132,74 @@ export function typeInfer(value: any): Type { //with emitted types: function or class //don't use resolveRuntimeType since we don't allow cache here // console.log('typeInfer of', value.name); - return Processor.get().reflect(value, undefined, { inline: true }) as Type; + return Processor.get().reflect(value, undefined, { + inline: true, + }) as Type; } if (isClass(value)) { //unknown class - return { kind: ReflectionKind.class, classType: value as ClassType, types: [] }; + return { + kind: ReflectionKind.class, + classType: value as ClassType, + types: [], + }; } - return { kind: ReflectionKind.function, function: value, name: value.name, return: { kind: ReflectionKind.any }, parameters: [] }; + return { + kind: ReflectionKind.function, + function: value, + name: value.name, + return: { kind: ReflectionKind.any }, + parameters: [], + }; } else if (isArray(value)) { - return { kind: ReflectionKind.array, type: typeInferFromContainer(value) }; + return { + kind: ReflectionKind.array, + type: typeInferFromContainer(value), + }; } else if ('object' === typeof value) { const constructor = value.constructor; if ('function' === typeof constructor && constructor !== Object && isArray(constructor.__type)) { //with emitted types //don't use resolveRuntimeType since we don't allow cache here - return Processor.get().reflect(constructor, undefined, { inline: true }) as Type; + return Processor.get().reflect(constructor, undefined, { + inline: true, + }) as Type; } if (constructor === RegExp) return { kind: ReflectionKind.regexp }; if (constructor === Date) return { kind: ReflectionKind.class, classType: Date, types: [] }; if (constructor === Set) { const type = typeInferFromContainer(value); - return { kind: ReflectionKind.class, classType: Set, arguments: [type], types: [] }; + return { + kind: ReflectionKind.class, + classType: Set, + arguments: [type], + types: [], + }; } if (constructor === Map) { const keyType = typeInferFromContainer((value as Map).keys()); const valueType = typeInferFromContainer((value as Map).values()); - return { kind: ReflectionKind.class, classType: Map, arguments: [keyType, valueType], types: [] }; + return { + kind: ReflectionKind.class, + classType: Map, + arguments: [keyType, valueType], + types: [], + }; } //generate a new program that builds a objectLiteral. This is necessary since typeInfer() with its Processor.reflect() calls might return immediately TypeAny if //the execution was scheduled (if we are in an executing program) so we can not depend on the result directly. //each part of the program of a value[i] is executed after the current OP, so we have to schedule new OPs doing the same as //in this loop here and construct the objectLiteral in the VM. - const resultType: TypeObjectLiteral = { kind: ReflectionKind.objectLiteral, id: state.nominalId++, types: [] }; + const resultType: TypeObjectLiteral = { + kind: ReflectionKind.objectLiteral, + id: state.nominalId++, + types: [], + }; const ops: ReflectionOp[] = []; const stack: RuntimeStackEntry[] = []; @@ -1825,7 +2234,10 @@ function applyClassDecorators(type: TypeClass) { applyPropertyDecorator(member.type, data); } - if ('number' === typeof parameterIndexOrDescriptor && (member.kind === ReflectionKind.method || member.kind === ReflectionKind.methodSignature)) { + if ( + 'number' === typeof parameterIndexOrDescriptor && + (member.kind === ReflectionKind.method || member.kind === ReflectionKind.methodSignature) + ) { const param = member.parameters[parameterIndexOrDescriptor]; if (param) { applyPropertyDecorator(param.type, data); @@ -1842,7 +2254,14 @@ function applyPropertyDecorator(type: Type, data: TData) { for (const validator of data.validators) { validationAnnotation.register(annotations, { name: 'function', - args: [{ kind: ReflectionKind.function, function: validator, parameters: [], return: { kind: ReflectionKind.any } }] + args: [ + { + kind: ReflectionKind.function, + function: validator, + parameters: [], + return: { kind: ReflectionKind.any }, + }, + ], }); } } @@ -1855,57 +2274,63 @@ function pushObjectLiteralTypes( let annotations: Annotations = {}; const decorators: Type[] = []; - outer: - for (const member of types) { - if (member.kind === ReflectionKind.propertySignature && member.type.kind === ReflectionKind.never) continue; + outer: for (const member of types) { + if (member.kind === ReflectionKind.propertySignature && member.type.kind === ReflectionKind.never) continue; - if (member.kind === ReflectionKind.objectLiteral) { - //all `extends T` expression land at the beginning of the stack frame, and are always an objectLiteral. - //we use it as base and move its types first into types + if (member.kind === ReflectionKind.objectLiteral) { + //all `extends T` expression land at the beginning of the stack frame, and are always an objectLiteral. + //we use it as base and move its types first into types - //it might be a decorator - for (const decorator of typeDecorators) { - if (decorator(annotations, member)) { - decorators.push(member); - continue outer; - } + //it might be a decorator + for (const decorator of typeDecorators) { + if (decorator(annotations, member)) { + decorators.push(member); + continue outer; } + } - type.implements ||= []; - type.implements.push(member); + type.implements ||= []; + type.implements.push(member); - pushObjectLiteralTypes(type, member.types); + pushObjectLiteralTypes(type, member.types); - //redirect decorators - if (member.decorators) { - decorators.push(...member.decorators); - } - if (member.annotations) { - annotations = Object.assign(member.annotations, annotations); - } - } else if (member.kind === ReflectionKind.indexSignature) { - //note: is it possible to overwrite an index signature? - type.types.push(member); - } else if (member.kind === ReflectionKind.propertySignature || member.kind === ReflectionKind.methodSignature) { - const toAdd = member.kind === ReflectionKind.propertySignature && member.type.kind === ReflectionKind.function ? { - kind: ReflectionKind.methodSignature, - name: member.name, - optional: member.optional, - parameters: member.type.parameters, - return: member.type.return, - } as TypeMethodSignature : member; - - const existing = type.types.findIndex(v => (v.kind === ReflectionKind.propertySignature || v.kind === ReflectionKind.methodSignature) && v.name === toAdd.name); - if (existing !== -1) { - //remove entry, since we replace it - type.types.splice(existing, 1, toAdd); - } else { - type.types.push(toAdd); - } - } else if (member.kind === ReflectionKind.callSignature) { - type.types.push(member); + //redirect decorators + if (member.decorators) { + decorators.push(...member.decorators); + } + if (member.annotations) { + annotations = Object.assign(member.annotations, annotations); } + } else if (member.kind === ReflectionKind.indexSignature) { + //note: is it possible to overwrite an index signature? + type.types.push(member); + } else if (member.kind === ReflectionKind.propertySignature || member.kind === ReflectionKind.methodSignature) { + const toAdd = + member.kind === ReflectionKind.propertySignature && member.type.kind === ReflectionKind.function + ? ({ + kind: ReflectionKind.methodSignature, + name: member.name, + optional: member.optional, + parameters: member.type.parameters, + return: member.type.return, + } as TypeMethodSignature) + : member; + + const existing = type.types.findIndex( + v => + (v.kind === ReflectionKind.propertySignature || v.kind === ReflectionKind.methodSignature) && + v.name === toAdd.name, + ); + if (existing !== -1) { + //remove entry, since we replace it + type.types.splice(existing, 1, toAdd); + } else { + type.types.push(toAdd); + } + } else if (member.kind === ReflectionKind.callSignature) { + type.types.push(member); } + } type.annotations = type.annotations || {}; if (decorators.length) type.decorators = decorators;