Skip to content

Commit

Permalink
feat(type): refactor typeName embedding
Browse files Browse the repository at this point in the history
Previously Type.typeName was detected based on actual symbol names in JS (var __Ωxy). This had a several issues.

First, we used forward refs like `() => __Ωxy` and then use toString() on it to get the symbol name. This is slow as it uses fn.toString() and regexp.

Second, since we expect __Ω as prefix, this is unstable since some minifier entirely replace `__Ωxy` into something entirely different like `abc`.

Third, __Ω is not always preserved in runtimes. It was often replaced with `__\uxxx` (bun, esbuild) this would make parsing even more complex.

Runtime types should work with any bundler and runtime, whether minification is enabled or not. Thus, this commit removes this approach entirely and emits for TypeAliasDeclaration, InterfaceDeclaration, EnumDeclaration, and ClassDeclaration now always `Op.typename` with the real type name as string. This means type names will survive all minification processes.
  • Loading branch information
marcj committed Jan 22, 2024
1 parent bdf4ebd commit 48a2994
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 126 deletions.
1 change: 1 addition & 0 deletions packages/create-app/files/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"experimentalDecorators": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "node"
Expand Down
30 changes: 20 additions & 10 deletions packages/type-compiler/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1114,11 +1114,7 @@ export class ReflectionTransformer implements CustomTransformer {
}
}

if (isTypeAliasDeclaration(node)) {
this.extractPackStructOfType(node.type, typeProgram);
} else {
this.extractPackStructOfType(node, typeProgram);
}
this.extractPackStructOfType(node, typeProgram);

if (isTypeAliasDeclaration(node) || isInterfaceDeclaration(node) || isClassDeclaration(node) || isClassExpression(node)) {
typeProgram.pushOp(ReflectionOp.nominal);
Expand Down Expand Up @@ -1362,6 +1358,12 @@ export class ReflectionTransformer implements CustomTransformer {
program.popFrameImplicit();
break;
}
case SyntaxKind.TypeAliasDeclaration: {
const narrowed = node as TypeAliasDeclaration;
this.extractPackStructOfType(narrowed.type, program);
if (narrowed.name) this.resolveTypeName(getIdentifierName(narrowed.name), program);
break;
}
case SyntaxKind.TypeLiteral:
case SyntaxKind.InterfaceDeclaration: {
//TypeScript does not narrow types down
Expand Down Expand Up @@ -1389,6 +1391,10 @@ export class ReflectionTransformer implements CustomTransformer {
}
const description = descriptionNode && extractJSDocAttribute(this.sourceFile, descriptionNode, 'description');
if (description) program.pushOp(ReflectionOp.description, program.findOrAddStackEntry(description));

if (isInterfaceDeclaration(narrowed)) {
if (narrowed.name) this.resolveTypeName(getIdentifierName(narrowed.name), program);
}
program.popFrameImplicit();
break;
}
Expand Down Expand Up @@ -1704,6 +1710,7 @@ export class ReflectionTransformer implements CustomTransformer {
program.pushOp(ReflectionOp.enum);
const description = extractJSDocAttribute(this.sourceFile, narrowed, 'description');
if (description) program.pushOp(ReflectionOp.description, program.findOrAddStackEntry(description));
if (narrowed.name) this.resolveTypeName(getIdentifierName(narrowed.name), program);
program.popFrameImplicit();
break;
}
Expand Down Expand Up @@ -2153,7 +2160,7 @@ export class ReflectionTransformer implements CustomTransformer {
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
program.pushOp(ReflectionOp.any);
this.resolveTypeOnlyImport(typeName, program);
return;
}

Expand All @@ -2162,13 +2169,13 @@ export class ReflectionTransformer implements CustomTransformer {
const reflection = this.getReflectionConfig(found);
// if this is never, then its generally disabled for this file
if (reflection.mode === 'never') {
program.pushOp(ReflectionOp.any);
this.resolveTypeOnlyImport(typeName, program);
return;
}

const declarationReflection = this.isWithReflection(found, declaration);
if (!declarationReflection) {
program.pushOp(ReflectionOp.any);
this.resolveTypeOnlyImport(typeName, program);
return;
}

Expand All @@ -2179,7 +2186,7 @@ export class ReflectionTransformer implements CustomTransformer {
//it's a reference type inside the same file. Make sure its type is reflected
const reflection = this.isWithReflection(program.sourceFile, declaration);
if (!reflection) {
program.pushOp(ReflectionOp.any);
this.resolveTypeOnlyImport(typeName, program);
return;
}

Expand Down Expand Up @@ -2218,6 +2225,8 @@ export class ReflectionTransformer implements CustomTransformer {
// this.extractPackStructOfType(declaration, program);
// return;
} 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
if (resolved.typeOnly) {
this.resolveTypeOnlyImport(typeName, program);
Expand All @@ -2227,7 +2236,7 @@ export class ReflectionTransformer implements CustomTransformer {
//it's a reference type inside the same file. Make sure its type is reflected
const reflection = this.isWithReflection(program.sourceFile, declaration);
if (!reflection) {
program.pushOp(ReflectionOp.any);
this.resolveTypeOnlyImport(typeName, program);
return;
}

Expand Down Expand Up @@ -2338,6 +2347,7 @@ export class ReflectionTransformer implements CustomTransformer {
}

protected resolveTypeName(typeName: string, program: CompilerProgram) {
if (!typeName) return;
program.pushOp(ReflectionOp.typeName, program.findOrAddStackEntry(typeName));
}

Expand Down
34 changes: 33 additions & 1 deletion packages/type-compiler/tests/transform.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as ts from 'typescript';
import { createSourceFile, ScriptKind, ScriptTarget } from 'typescript';
import { expect, test } from '@jest/globals';
import { ReflectionTransformer } from '../src/compiler.js';
import { transform } from './utils.js';
import { transform, transpileAndRun } from './utils.js';

test('transform simple TS', () => {
const sourceFile = createSourceFile('app.ts', `
Expand Down Expand Up @@ -202,3 +202,35 @@ test('declaration file resolved export all', () => {

expect(res['app.ts']).toContain('import { __ΩT } from \'./module');
});

test('import typeOnly interface', () => {
const res = transform({
'app.ts': `
import type { Cache } from './module';
return typeOf<Cache>();
`,
'module.d.ts': `
export interface Cache {
}
`
});

//make sure OP.typeName with its type name is emitted
expect(res['app.ts']).toContain(`['Cache',`);
});

test('import typeOnly class', () => {
const res = transform({
'app.ts': `
import type { Cache } from './module';
typeOf<Cache>();
`,
'module.d.ts': `
export declare class Cache {
}
`
});

//make sure OP.typeName with its type name is emitted
expect(res['app.ts']).toContain(`['Cache',`);
});
8 changes: 8 additions & 0 deletions packages/type-spec/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,14 @@ export enum ReflectionOp {

callSignature, //Same as function but for call signatures (in object literals)

/**
* Assign for Enum, Interface, Class, and TypeAlias declaration at the very end
* of the program the typeName. This is so that we have type names available even
* if the JS code is minified.
*
* his operator also assigns originTypes to the type, as it acts as the finalization
* step of a type.
*/
typeName, //has one parameter, the index of the stack entry that contains the type name. Uses current head of the stack as type and assigns typeName to it.

/**
Expand Down
37 changes: 11 additions & 26 deletions packages/type/src/reflection/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ import {
TypeUnion,
unboxUnion,
validationAnnotation,
widenLiteral
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 { extractTypeNameFromFunction, ReflectionClass, TData } from './reflection.js';
import { ReflectionClass, TData } from './reflection.js';
import { state } from './state.js';

export type RuntimeStackEntry = Type | Object | (() => ClassType | Object) | string | number | boolean | bigint;
Expand Down Expand Up @@ -184,19 +184,7 @@ interface Program {
}

function assignResult<T extends Type>(ref: Type, result: T, assignParents: boolean): T {
if (ref.typeName) {
if (result.typeName && ref.typeName !== result.typeName) {
Object.assign(ref, result, {
typeName: ref.typeName,
typeArguments: ref.typeArguments,
originTypes: [{ typeName: result.typeName, typeArguments: result.typeArguments }, ...(result.originTypes || [])],
});
} else {
Object.assign(ref, result, { typeName: ref.typeName, typeArguments: ref.typeArguments });
}
} else {
Object.assign(ref, result);
}
Object.assign(ref, result);

if (assignParents) {
// if (ref.kind === ReflectionKind.class && ref.arguments) {
Expand Down Expand Up @@ -977,7 +965,13 @@ export class Processor {
(program.stack[program.stackPointer] as TypeProperty).description = program.stack[this.eatParameter() as number] as string;
break;
case ReflectionOp.typeName: {
(program.stack[program.stackPointer] as Type).typeName = program.stack[this.eatParameter() as number] as string;
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: {
Expand Down Expand Up @@ -1191,13 +1185,8 @@ export class Processor {
} else {
//when it's just a simple reference resolution like typeOf<Class>() then don't issue a new reference (no inline: true)
const directReference = !!(this.isEnded() && program.previous && program.previous.end === 0);
const typeName = (isFunction(pOrFn) ? extractTypeNameFromFunction(pOrFn) : '');
const result = this.reflect(p, [], { inline: !directReference, reuseCached: directReference, typeName });
const result = this.reflect(p, [], { inline: !directReference, reuseCached: directReference });
if (directReference) program.directReturn = true;

if (isWithAnnotations(result)) {
result.typeName = (isFunction(pOrFn) ? extractTypeNameFromFunction(pOrFn) : '') || result.typeName;
}
this.push(result, program);

//this.reflect/run might create another program onto the stack. switch to it if so
Expand Down Expand Up @@ -1248,10 +1237,6 @@ export class Processor {
} else {
const result = this.reflect(p, inputs);

if (isWithAnnotations(result)) {
result.typeName = (isFunction(pOrFn) ? extractTypeNameFromFunction(pOrFn) : '') || result.typeName;
}

if (isWithAnnotations(result) && inputs.length) {
result.typeArguments = result.typeArguments || [];
for (let i = 0; i < inputs.length; i++) {
Expand Down
13 changes: 1 addition & 12 deletions packages/type/src/reflection/reflection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,7 @@ export function resolveReceiveType(type?: Packed | Type | ClassType | AbstractCl
}
return resolveRuntimeType(type) as Type;
}
const typeName = typeFn ? extractTypeNameFromFunction(typeFn) : undefined;
return resolvePacked(type, undefined, { reuseCached: true, typeName });
return resolvePacked(type, undefined, { reuseCached: true });
}

export function reflect(o: any, ...args: any[]): Type {
Expand Down Expand Up @@ -1452,16 +1451,6 @@ export class ReflectionClass<T> {
}
}

export function extractTypeNameFromFunction(fn: Function): string {
const str = fn.toString();
//either it starts with __Ω* or __\u{3a9}* (bun.js)
const match = str.match(/(?:__Ω|__\\u\{3a9\})([\w]+)/);
if (match) {
return match[1];
}
return 'UnknownTypeName:' + str;
}


// old function to decorate an interface
// export function decorate<T>(decorate: { [P in keyof T]?: FreeDecoratorFn<any> }, p?: ReceiveType<T>): ReflectionClass<T> {
Expand Down
2 changes: 1 addition & 1 deletion packages/type/src/reflection/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export interface TypeAnnotations {
* typeOf<UserCreate>().originTypes[0].typeName = 'Pick'
* typeOf<UserCreate>().originTypes[0].typeArguments = [User, 'user']
*/
originTypes?: { typeName: string, typeArguments: Type[] }[];
originTypes?: { typeName: string, typeArguments?: Type[] }[];

annotations?: Annotations; //parsed decorator types as annotations
decorators?: Type[]; //original decorator type
Expand Down
6 changes: 3 additions & 3 deletions packages/type/tests/compiler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ test('type emitted at the right place', () => {

const js = transpile(code);
console.log('js', js);
expect(js).toContain(`() => {\n const __Ωo = ['a', '${packRaw([ReflectionOp.frame])}`);
expect(js).toContain(`() => {\n const __Ωo = ['a', 'o', '${packRaw([ReflectionOp.frame])}`);
const type = transpileAndReturn(code);
console.log(type);
});
Expand Down Expand Up @@ -569,8 +569,8 @@ test('no global clash', () => {

const js = transpile(code);
console.log('js', js);
expect(js).toContain(`const __Ωo = ['a', '${packRaw([ReflectionOp.frame])}`);
expect(js).toContain(`const __Ωo = ['a', 'b', '${packRaw([ReflectionOp.frame])}`);
expect(js).toContain(`const __Ωo = ['a', 'o', '${packRaw([ReflectionOp.frame])}`);
expect(js).toContain(`const __Ωo = ['a', 'b', 'o', '${packRaw([ReflectionOp.frame])}`);
// const clazz = transpileAndReturn(code);
});

Expand Down
Loading

0 comments on commit 48a2994

Please sign in to comment.