@eliasku/ts-transformers 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@eliasku/ts-transformers",
3
+ "description": "",
4
+ "version": "0.0.1",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "bun test",
9
+ "format": "prettier --write **/*.{ts,json,md,yml}",
10
+ "check": "tsc -p .",
11
+ "lint": "eslint ."
12
+ },
13
+ "dependencies": {
14
+ "typescript": "^5"
15
+ },
16
+ "optionalDependencies": {
17
+ "prettier": "latest",
18
+ "eslint": "latest",
19
+ "typescript-eslint": "latest",
20
+ "@types/bun": "latest",
21
+ "rollup": "latest",
22
+ "@rollup/plugin-typescript": "latest",
23
+ "tslib": "latest"
24
+ },
25
+ "exports": {
26
+ "./mangler": "./src/mangler/index.ts",
27
+ "./const-enum": "./src/const-enum/index.ts"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/eliasku/ts-transformers.git"
32
+ },
33
+ "author": "Ilya Kuzmichev",
34
+ "keywords": [
35
+ "typescript",
36
+ "ts",
37
+ "plugin",
38
+ "transformer",
39
+ "minify",
40
+ "const-enum",
41
+ "treeshake",
42
+ "mangle"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "files": [
48
+ "src"
49
+ ]
50
+ }
package/src/config.ts ADDED
@@ -0,0 +1 @@
1
+ export const LOGS = false;
@@ -0,0 +1,165 @@
1
+ import ts from "typescript";
2
+
3
+ export type EnumValue = string | number;
4
+
5
+ export interface EvaluationContext {
6
+ localMembers: Map<string, EnumValue>;
7
+ allowEnumReferences: boolean;
8
+ }
9
+
10
+ export class EnumEvaluator {
11
+ private lastImplicitValue = -1;
12
+ private enumType: "numeric" | "string" | "mixed" = "numeric";
13
+
14
+ constructor(private readonly typeChecker: ts.TypeChecker) {}
15
+
16
+ reset(): void {
17
+ this.lastImplicitValue = -1;
18
+ this.enumType = "numeric";
19
+ }
20
+
21
+ evaluate(expr: ts.Expression, context: EvaluationContext): EnumValue {
22
+ if (ts.isPrefixUnaryExpression(expr)) {
23
+ return this.evaluateUnary(expr, context);
24
+ } else if (ts.isBinaryExpression(expr)) {
25
+ return this.evaluateBinary(expr, context);
26
+ } else if (ts.isStringLiteralLike(expr)) {
27
+ return expr.text;
28
+ } else if (ts.isNumericLiteral(expr)) {
29
+ return +expr.text;
30
+ } else if (ts.isParenthesizedExpression(expr)) {
31
+ return this.evaluate(expr.expression, context);
32
+ } else if (ts.isIdentifier(expr)) {
33
+ return this.evaluateIdentifier(expr, context);
34
+ } else if (
35
+ expr.kind === ts.SyntaxKind.TrueKeyword ||
36
+ expr.kind === ts.SyntaxKind.FalseKeyword ||
37
+ expr.kind === ts.SyntaxKind.NullKeyword
38
+ ) {
39
+ throw this.createError(expr, `Unsupported literal: ${ts.SyntaxKind[expr.kind]}`);
40
+ }
41
+
42
+ throw this.createError(expr, `Cannot evaluate expression: ${expr.getText()}`);
43
+ }
44
+
45
+ evaluateEnumMember(member: ts.EnumMember, context: EvaluationContext): EnumValue {
46
+ if (!member.initializer) {
47
+ return this.evaluateImplicitMember(member);
48
+ }
49
+
50
+ const value = this.evaluate(member.initializer, context);
51
+
52
+ if (typeof value === "number") {
53
+ this.lastImplicitValue = value;
54
+ }
55
+
56
+ return value;
57
+ }
58
+
59
+ private evaluateImplicitMember(member: ts.EnumMember): EnumValue {
60
+ const name = ts.isIdentifier(member.name) ? member.name.text : `<computed>`;
61
+ // unused
62
+ void name;
63
+
64
+ if (this.lastImplicitValue === -1) {
65
+ this.lastImplicitValue = 0;
66
+ return 0;
67
+ }
68
+
69
+ const nextValue = this.lastImplicitValue + 1;
70
+ this.lastImplicitValue = nextValue;
71
+ return nextValue;
72
+ }
73
+
74
+ createLiteral(value: EnumValue): ts.Expression {
75
+ if (typeof value === "string") {
76
+ return ts.factory.createStringLiteral(value);
77
+ } else {
78
+ if (value < 0) {
79
+ return ts.factory.createPrefixMinus(ts.factory.createNumericLiteral(Math.abs(value).toString()));
80
+ }
81
+ return ts.factory.createNumericLiteral(value.toString());
82
+ }
83
+ }
84
+
85
+ private evaluateUnary(expr: ts.PrefixUnaryExpression, context: EvaluationContext): number {
86
+ const value = this.evaluate(expr.operand, context);
87
+ if (typeof value !== "number") {
88
+ throw this.createError(expr, `Unary operator requires numeric value, got ${typeof value}`);
89
+ }
90
+
91
+ switch (expr.operator) {
92
+ case ts.SyntaxKind.PlusToken:
93
+ return value;
94
+ case ts.SyntaxKind.MinusToken:
95
+ return -value;
96
+ case ts.SyntaxKind.TildeToken:
97
+ return ~value;
98
+ default:
99
+ throw this.createError(expr, `Unsupported unary operator: ${ts.SyntaxKind[expr.operator]}`);
100
+ }
101
+ }
102
+
103
+ private evaluateBinary(expr: ts.BinaryExpression, context: EvaluationContext): EnumValue {
104
+ const left = this.evaluate(expr.left, context);
105
+ const right = this.evaluate(expr.right, context);
106
+
107
+ // String concatenation
108
+ if (typeof left === "string" && typeof right === "string" && expr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
109
+ return left + right;
110
+ }
111
+
112
+ // Numeric operations
113
+ if (typeof left === "number" && typeof right === "number") {
114
+ switch (expr.operatorToken.kind) {
115
+ case ts.SyntaxKind.BarToken:
116
+ return left | right;
117
+ case ts.SyntaxKind.AmpersandToken:
118
+ return left & right;
119
+ case ts.SyntaxKind.GreaterThanGreaterThanToken:
120
+ return left >> right;
121
+ case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
122
+ return left >>> right;
123
+ case ts.SyntaxKind.LessThanLessThanToken:
124
+ return left << right;
125
+ case ts.SyntaxKind.CaretToken:
126
+ return left ^ right;
127
+ case ts.SyntaxKind.AsteriskToken:
128
+ return left * right;
129
+ case ts.SyntaxKind.SlashToken:
130
+ return left / right;
131
+ case ts.SyntaxKind.PlusToken:
132
+ return left + right;
133
+ case ts.SyntaxKind.MinusToken:
134
+ return left - right;
135
+ case ts.SyntaxKind.PercentToken:
136
+ return left % right;
137
+ case ts.SyntaxKind.AsteriskAsteriskToken:
138
+ return left ** right;
139
+ default:
140
+ throw this.createError(expr, `Unsupported binary operator: ${ts.SyntaxKind[expr.operatorToken.kind]}`);
141
+ }
142
+ }
143
+
144
+ throw this.createError(expr, `Cannot evaluate binary expression with types ${typeof left} and ${typeof right}`);
145
+ }
146
+
147
+ private evaluateIdentifier(expr: ts.Identifier, context: EvaluationContext): EnumValue {
148
+ if (!context.allowEnumReferences) {
149
+ throw this.createError(expr, `Cannot reference enum member here`);
150
+ }
151
+
152
+ const value = context.localMembers.get(expr.text);
153
+ if (value === undefined) {
154
+ throw this.createError(expr, `Undefined enum member: ${expr.text}`);
155
+ }
156
+
157
+ return value;
158
+ }
159
+
160
+ private createError(node: ts.Node, message: string): Error {
161
+ const sourceFile = node.getSourceFile();
162
+ const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart());
163
+ return new Error(`${sourceFile.fileName}:${pos.line + 1}:${pos.character}: ${message}`);
164
+ }
165
+ }
@@ -0,0 +1,181 @@
1
+ import ts from "typescript";
2
+ import { ConstEnumRegistry } from "./registry";
3
+ import { EnumEvaluator } from "./evaluator";
4
+ import { hasModifier, isConstEnumType } from "./utils";
5
+ import { LOGS } from "../config";
6
+
7
+ export const tsTransformConstEnums = (
8
+ program: ts.Program,
9
+ entrySourceFiles?: readonly string[],
10
+ ): ts.TransformerFactory<ts.SourceFile> => {
11
+ if (LOGS) {
12
+ console.log("[const-enum] tsTransformConstEnums called!");
13
+ }
14
+ const startTime = performance.now();
15
+ const registry = new ConstEnumRegistry(program, entrySourceFiles);
16
+ const typeChecker = program.getTypeChecker();
17
+ const evaluator = new EnumEvaluator(typeChecker);
18
+ if (LOGS) {
19
+ console.log(
20
+ `[const-enum] Found ${registry.getEnumCount()} const enum declarations in ${performance.now() - startTime}ms`,
21
+ );
22
+ }
23
+
24
+ return (context: ts.TransformationContext) => {
25
+ function transformNodeAndChildren(
26
+ node: ts.Node,
27
+ ctx: ts.TransformationContext,
28
+ sourceFile: ts.SourceFile,
29
+ ): ts.Node {
30
+ return ts.visitEachChild(
31
+ transformNode(node, sourceFile, ctx, registry, evaluator, typeChecker),
32
+ (childNode: ts.Node) => transformNodeAndChildren(childNode, ctx, sourceFile),
33
+ ctx,
34
+ );
35
+ }
36
+ return (sourceFile: ts.SourceFile) => transformNodeAndChildren(sourceFile, context, sourceFile) as ts.SourceFile;
37
+ };
38
+ };
39
+
40
+ function transformNode(
41
+ node: ts.Node,
42
+ sourceFile: ts.SourceFile,
43
+ ctx: ts.TransformationContext,
44
+ registry: ConstEnumRegistry,
45
+ evaluator: EnumEvaluator,
46
+ typeChecker: ts.TypeChecker,
47
+ ): ts.Node {
48
+ if (ts.isPropertyAccessExpression(node)) {
49
+ return transformPropertyAccess(node, ctx, registry, evaluator, typeChecker);
50
+ }
51
+
52
+ if (ts.isEnumDeclaration(node)) {
53
+ return transformEnumDeclaration(node, sourceFile, ctx);
54
+ }
55
+
56
+ if (ts.isImportSpecifier(node)) {
57
+ return transformImportSpecifier(node, ctx, registry, typeChecker);
58
+ }
59
+
60
+ if (ts.isImportClause(node)) {
61
+ return transformImportClause(node, ctx, registry, typeChecker);
62
+ }
63
+
64
+ return ts.visitEachChild(
65
+ node,
66
+ (child) => transformNode(child, sourceFile, ctx, registry, evaluator, typeChecker),
67
+ ctx,
68
+ );
69
+ }
70
+
71
+ function transformPropertyAccess(
72
+ node: ts.PropertyAccessExpression,
73
+ ctx: ts.TransformationContext,
74
+ registry: ConstEnumRegistry,
75
+ evaluator: EnumEvaluator,
76
+ typeChecker: ts.TypeChecker,
77
+ ): ts.Expression | ts.PropertyAccessExpression {
78
+ const expressionType = typeChecker.getTypeAtLocation(node.expression);
79
+
80
+ if (!isConstEnumType(expressionType)) {
81
+ return node;
82
+ }
83
+
84
+ const enumSymbol = expressionType.symbol || expressionType.aliasSymbol;
85
+ if (!enumSymbol) {
86
+ return node;
87
+ }
88
+
89
+ const enumInfo = registry.getEnumInfo(enumSymbol);
90
+ if (!enumInfo) {
91
+ if (LOGS) {
92
+ console.warn(`[const-enum] Could not find const enum ${enumSymbol.name}`);
93
+ }
94
+ return node;
95
+ }
96
+
97
+ const memberValue = enumInfo.members.get(node.name.text)?.value;
98
+ if (memberValue === undefined || memberValue === null) {
99
+ if (LOGS) {
100
+ console.warn(`[const-enum] Could not find member ${enumSymbol.name}.${node.name.text}`);
101
+ }
102
+ return node;
103
+ }
104
+
105
+ const literal = evaluator.createLiteral(memberValue);
106
+ if (LOGS) {
107
+ console.log(`[const-enum] Inline ${enumSymbol.name}.${node.name.text} → ${JSON.stringify(memberValue)}`);
108
+ }
109
+
110
+ return literal;
111
+ }
112
+
113
+ function transformEnumDeclaration(
114
+ node: ts.EnumDeclaration,
115
+ sourceFile: ts.SourceFile,
116
+ ctx: ts.TransformationContext,
117
+ ): ts.EnumDeclaration | undefined {
118
+ // unused
119
+ void ctx;
120
+
121
+ if (!hasModifier(node, ts.SyntaxKind.ConstKeyword)) {
122
+ return node;
123
+ }
124
+
125
+ if (sourceFile.isDeclarationFile) {
126
+ if (LOGS) {
127
+ console.log(`[const-enum] Strip 'const' from ${node.name.text} in ${sourceFile.fileName}`);
128
+ }
129
+ return ts.factory.updateEnumDeclaration(
130
+ node,
131
+ node.modifiers?.filter((m) => m.kind !== ts.SyntaxKind.ConstKeyword),
132
+ node.name,
133
+ node.members,
134
+ );
135
+ }
136
+
137
+ if (LOGS) {
138
+ console.log(`[const-enum] Remove const enum declaration ${node.name.text} in ${sourceFile.fileName}`);
139
+ }
140
+ return undefined;
141
+ }
142
+
143
+ function transformImportSpecifier(
144
+ node: ts.ImportSpecifier,
145
+ ctx: ts.TransformationContext,
146
+ registry: ConstEnumRegistry,
147
+ typeChecker: ts.TypeChecker,
148
+ ): ts.ImportSpecifier | undefined {
149
+ const importedType = typeChecker.getTypeAtLocation(node);
150
+
151
+ if (isConstEnumType(importedType)) {
152
+ if (LOGS) {
153
+ console.log(`[const-enum] Remove import of const enum ${importedType.symbol?.name}`);
154
+ }
155
+ return undefined;
156
+ }
157
+
158
+ return node;
159
+ }
160
+
161
+ function transformImportClause(
162
+ node: ts.ImportClause,
163
+ ctx: ts.TransformationContext,
164
+ registry: ConstEnumRegistry,
165
+ typeChecker: ts.TypeChecker,
166
+ ): ts.ImportClause | undefined {
167
+ if (!node.name) {
168
+ return node;
169
+ }
170
+
171
+ const type = typeChecker.getTypeAtLocation(node.name);
172
+
173
+ if (isConstEnumType(type)) {
174
+ if (LOGS) {
175
+ console.log(`[const-enum] Remove import clause for const enum ${type.symbol?.name}`);
176
+ }
177
+ return undefined;
178
+ }
179
+
180
+ return node;
181
+ }
@@ -0,0 +1,169 @@
1
+ import ts from "typescript";
2
+ import { EnumValue, EvaluationContext } from "./evaluator";
3
+ import { EnumEvaluator } from "./evaluator";
4
+ import { hasModifier, isConstEnumSymbol } from "./utils";
5
+ import { LOGS } from "../config";
6
+
7
+ export interface ConstEnumInfo {
8
+ declaration: ts.EnumDeclaration;
9
+ members: Map<string, ConstEnumMemberInfo>;
10
+ isExported: boolean;
11
+ sourceFile: ts.SourceFile;
12
+ }
13
+
14
+ export interface ConstEnumMemberInfo {
15
+ declaration: ts.EnumMember;
16
+ name: string;
17
+ value: EnumValue | null;
18
+ }
19
+
20
+ export class ConstEnumRegistry {
21
+ private readonly program: ts.Program;
22
+ private readonly typeChecker: ts.TypeChecker;
23
+ private readonly entrySourceFiles: readonly string[];
24
+ private readonly enumDeclarations: Map<string, ConstEnumInfo>;
25
+
26
+ constructor(program: ts.Program, entrySourceFiles?: readonly string[]) {
27
+ this.program = program;
28
+ this.typeChecker = program.getTypeChecker();
29
+ this.entrySourceFiles = entrySourceFiles || program.getRootFileNames();
30
+ this.enumDeclarations = new Map();
31
+ this.collectConstEnumsFromEntryPoints();
32
+ }
33
+
34
+ getEnum(enumName: string): ConstEnumInfo | undefined {
35
+ return this.enumDeclarations.get(enumName);
36
+ }
37
+
38
+ getEnumInfo(symbol: ts.Symbol): ConstEnumInfo | undefined {
39
+ const name = this.getEnumSymbolName(symbol);
40
+ return this.enumDeclarations.get(name);
41
+ }
42
+
43
+ getMemberValue(enumName: string, memberName: string): EnumValue | undefined {
44
+ const enumInfo = this.enumDeclarations.get(enumName);
45
+ if (!enumInfo) return undefined;
46
+ const memberInfo = enumInfo.members.get(memberName);
47
+ return memberInfo?.value ?? undefined;
48
+ }
49
+
50
+ getAllEnums(): ConstEnumInfo[] {
51
+ return Array.from(this.enumDeclarations.values());
52
+ }
53
+
54
+ getEnumCount(): number {
55
+ return this.enumDeclarations.size;
56
+ }
57
+
58
+ private collectConstEnumsFromEntryPoints(): void {
59
+ if (LOGS) {
60
+ console.log(`[const-enum registry] Starting collection from ${this.entrySourceFiles.length} entry point(s)`);
61
+ }
62
+
63
+ // Collect all const enums from the entire program
64
+ const sourceFiles = this.program.getSourceFiles();
65
+ if (LOGS) {
66
+ console.log(`[const-enum registry] Program has ${sourceFiles.length} source files`);
67
+ }
68
+
69
+ for (const sourceFile of sourceFiles) {
70
+ // We are using typescript files from node_modules as well, so don't skip them
71
+ // but skip declaration files
72
+ if (sourceFile.isDeclarationFile) {
73
+ continue;
74
+ }
75
+
76
+ this.registerConstEnumFromSource(sourceFile);
77
+ }
78
+
79
+ if (LOGS) {
80
+ console.log(`[const-enum registry] Found ${this.enumDeclarations.size} const enum declarations`);
81
+ }
82
+ }
83
+
84
+ private registerConstEnumFromSource(sourceFile: ts.SourceFile): void {
85
+ ts.forEachChild(sourceFile, (node) => {
86
+ if (ts.isEnumDeclaration(node) && hasModifier(node, ts.SyntaxKind.ConstKeyword)) {
87
+ const symbol = this.typeChecker.getSymbolAtLocation(node.name);
88
+ if (symbol && isConstEnumSymbol(symbol)) {
89
+ this.registerEnum(symbol, node, sourceFile);
90
+ }
91
+ }
92
+ });
93
+ }
94
+
95
+ private registerEnum(symbol: ts.Symbol, declaration: ts.EnumDeclaration, sourceFile: ts.SourceFile): void {
96
+ const name = this.getEnumSymbolName(symbol);
97
+
98
+ if (this.enumDeclarations.has(name)) {
99
+ // Already registered (might be from different import)
100
+ return;
101
+ }
102
+
103
+ const isExported = this.hasExportModifier(declaration);
104
+
105
+ const enumInfo: ConstEnumInfo = {
106
+ declaration,
107
+ members: new Map(),
108
+ isExported,
109
+ sourceFile,
110
+ };
111
+
112
+ this.evaluateEnumMembers(enumInfo);
113
+
114
+ this.enumDeclarations.set(name, enumInfo);
115
+ }
116
+
117
+ private hasExportModifier(node: ts.Node): boolean {
118
+ if (!ts.canHaveModifiers(node)) {
119
+ return false;
120
+ }
121
+ const modifiers = ts.getModifiers(node);
122
+ if (!modifiers) {
123
+ return false;
124
+ }
125
+ return modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
126
+ }
127
+
128
+ private evaluateEnumMembers(enumInfo: ConstEnumInfo): void {
129
+ const evaluator = new EnumEvaluator(this.typeChecker);
130
+ evaluator.reset();
131
+ const context: EvaluationContext = {
132
+ localMembers: new Map(),
133
+ allowEnumReferences: true,
134
+ };
135
+
136
+ for (const member of enumInfo.declaration.members) {
137
+ if (!ts.isIdentifier(member.name)) continue;
138
+
139
+ try {
140
+ const value = evaluator.evaluateEnumMember(member, context);
141
+ context.localMembers.set(member.name.text, value);
142
+ enumInfo.members.set(member.name.text, {
143
+ declaration: member,
144
+ name: member.name.text,
145
+ value,
146
+ });
147
+ } catch (error) {
148
+ throw new Error(
149
+ `Failed to evaluate const enum member ${enumInfo.declaration.name.text}.${member.name.text}: ${error}`,
150
+ );
151
+ }
152
+ }
153
+ }
154
+
155
+ private getEnumDeclaration(symbol: ts.Symbol): ts.EnumDeclaration | null {
156
+ const decl = symbol.declarations?.[0];
157
+ if (decl && ts.isEnumDeclaration(decl)) {
158
+ return decl;
159
+ }
160
+ return null;
161
+ }
162
+
163
+ private getEnumSymbolName(symbol: ts.Symbol): string {
164
+ if (symbol.flags & ts.SymbolFlags.Alias) {
165
+ return this.typeChecker.getAliasedSymbol(symbol).name;
166
+ }
167
+ return symbol.name;
168
+ }
169
+ }
@@ -0,0 +1,13 @@
1
+ import ts from "typescript";
2
+
3
+ export const hasModifier = (node: ts.Node, modifier: ts.SyntaxKind) =>
4
+ ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((mod: ts.Modifier) => mod.kind === modifier);
5
+
6
+ export const isConstEnumSymbol = (symbol: ts.Symbol): boolean => (symbol.flags & ts.SymbolFlags.ConstEnum) !== 0;
7
+
8
+ export const isConstEnumType = (type: ts.Type | undefined): boolean => {
9
+ if (!type) return false;
10
+ const symbol = type.symbol || type.aliasSymbol;
11
+ if (!symbol) return false;
12
+ return (symbol.flags & ts.SymbolFlags.ConstEnum) !== 0;
13
+ };