@eliasku/ts-transformers 0.0.2 → 0.0.4

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 CHANGED
@@ -1,44 +1,191 @@
1
1
  # @eliasku/ts-transformers
2
2
 
3
- TypeScript transformer for code optimization.
3
+ TypeScript transformer for aggressive code minification through type-aware property renaming and const enum inlining.
4
+
5
+ ## Important Requirement
6
+
7
+ **You must compile ALL your code from TypeScript files.**
8
+
9
+ - No pre-transpiled `.js` files in source
10
+ - Transformer requires TypeScript type information
11
+ - Applicable for application builds, not libraries
12
+
13
+ ## Core Concept
14
+
15
+ Two-phase optimization pipeline:
16
+
17
+ 1. **This Transformer**: Analyzes TypeScript types, marks renamable properties with special prefixes
18
+ 2. **Minifier (esbuild)**: Aggressively mangles prefixed properties while preserving public API
19
+
20
+ ### Visibility Levels
21
+
22
+ Based on type analysis, properties are categorized as:
23
+
24
+ - **Public (External)**: Exported from entry points → **no prefix** (preserved)
25
+ - **Private**: Everything else → prefixed with `$_` (mangled by minifier)
26
+
27
+ **Example:**
28
+ ```typescript
29
+ // Before
30
+ class MyClass {
31
+ /** @public - keeps name */
32
+ publicApi() {}
33
+
34
+ method() {} // Private → $_method
35
+ private secret = 1; // Private → $_secret
36
+ }
37
+
38
+ // After transformer (before minifier)
39
+ class MyClass {
40
+ publicApi() {}
41
+ $_method() {}
42
+ $_secret = 1;
43
+ }
44
+
45
+ // After esbuild minifier
46
+ class A{publicApi(){},a(){},b=1}
47
+ ```
48
+
49
+ ### Const Enum Inlining
50
+
51
+ Replaces const enum accesses with literal values and removes declarations.
52
+
53
+ ```typescript
54
+ // Before
55
+ const enum Status { Active = 1, Inactive = 0 }
56
+ const status = Status.Active;
57
+
58
+ // After transformer + minifier
59
+ const status = 1;
60
+ ```
4
61
 
5
62
  ## Usage
6
63
 
7
64
  ```typescript
8
65
  import { optimizer } from "@eliasku/ts-transformers";
9
-
10
- transformers: (program) => ({
11
- before: [
12
- optimizer(program, {
13
- entrySourceFiles: ["./src/index.ts"],
14
- inlineConstEnums: true,
66
+ import typescript from "@rollup/plugin-typescript";
67
+ import { rollup } from "rollup";
68
+ import { build } from "esbuild";
69
+
70
+ // Phase 1: Type-aware optimization
71
+ const bundle = await rollup({
72
+ input: "./src/index.ts",
73
+ plugins: [
74
+ typescript({
75
+ transformers: (program) => ({
76
+ before: [
77
+ optimizer(program, {
78
+ entrySourceFiles: ["./src/index.ts"],
79
+ inlineConstEnums: true,
80
+ }),
81
+ ],
82
+ }),
15
83
  }),
16
84
  ],
17
85
  });
86
+
87
+ await bundle.write({
88
+ file: "./dist/bundle.js",
89
+ format: "es",
90
+ });
91
+
92
+ // Phase 2: Aggressive minification
93
+ await build({
94
+ entryPoints: ["./dist/bundle.js"],
95
+ outfile: "./dist/bundle.min.js",
96
+ minify: true,
97
+ mangleProps: /^\$_/, // Match your privatePrefix
98
+ mangleQuoted: false,
99
+ keepNames: false,
100
+ });
18
101
  ```
19
102
 
20
103
  ## Options
21
104
 
22
105
  ### entrySourceFiles (required)
23
106
 
24
- An array of entry source files which will be used to detect exported and internal fields. Basically it should be entry point(s) of the library/project.
107
+ Entry points defining your public API surface.
25
108
 
26
- ### inlineConstEnums (optional, default: true)
109
+ ```typescript
110
+ entrySourceFiles: ["./src/index.ts"]
111
+ ```
27
112
 
28
- Whether to inline const enum values and remove const enum declarations.
113
+ ### privatePrefix (optional, default: "$_")
29
114
 
30
- ### privatePrefix (optional, default: "$p$")
115
+ Prefix for private properties that will be mangled by esbuild.
31
116
 
32
- Prefix of generated names for private fields.
117
+ ```typescript
118
+ privatePrefix: "$_" // myFunction → $_myFunction
119
+ ```
33
120
 
34
- ### internalPrefix (optional, default: "$i$")
121
+ ### publicJSDocTag (optional, default: "public")
35
122
 
36
- Prefix of generated names for internal fields.
123
+ JSDoc tag marking types/properties as public. Set to empty string to disable.
37
124
 
38
- ### publicJSDocTag (optional, default: "public")
125
+ ```typescript
126
+ publicJSDocTag: "public"
39
127
 
40
- Comment which will treat a class/interface/type/property/etc and all its children as "public". Set it to empty string to disable using JSDoc comment to detect "visibility level".
128
+ class MyClass {
129
+ /** @public */
130
+ apiMethod() {} // Public, no prefix
131
+
132
+ internalHelper() {} // Private, gets $_ prefix
133
+ }
134
+ ```
41
135
 
42
136
  ### ignoreDecorated (optional, default: false)
43
137
 
44
- Whether fields that were decorated should be renamed. A field is treated as "decorated" if itself or any its parent (on type level) has a decorator.
138
+ Skip renaming decorated fields.
139
+
140
+ ```typescript
141
+ ignoreDecorated: true
142
+
143
+ @Component({ selector: "app-root" })
144
+ class AppComponent {
145
+ @Input() data: any; // Not renamed
146
+ private internal = 1; // Renamed to $_internal
147
+ }
148
+ ```
149
+
150
+ ### inlineConstEnums (optional, default: true)
151
+
152
+ Inline const enum values and remove declarations.
153
+
154
+ ## Complete Example
155
+
156
+ ```typescript
157
+ // src/index.ts (before)
158
+ class API {
159
+ private baseUrl = "https://api.example.com";
160
+
161
+ /** @public */
162
+ async get(path: string): Promise<Response> {
163
+ const url = `${this.baseUrl}${path}`;
164
+ const response = await fetch(url);
165
+ return this.handleResponse(response);
166
+ }
167
+
168
+ private async handleResponse(response: Response): Promise<Response> {
169
+ return response;
170
+ }
171
+ }
172
+
173
+ export const api = new API();
174
+
175
+ // After transformer
176
+ class API {
177
+ $_baseUrl = "https://api.example.com";
178
+ async get(path) {
179
+ const url = `${this.$_baseUrl}${path}`;
180
+ const response = await fetch(url);
181
+ return this.$_handleResponse(response);
182
+ }
183
+
184
+ $_handleResponse(response) {
185
+ return response;
186
+ }
187
+ }
188
+
189
+ // After esbuild minifier
190
+ class A{a="https://api.example.com";async get(t){const n=`${this.a}${t}`;return await fetch(n)}b(t){return t}}const s=new A;export{s};
191
+ ```
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@eliasku/ts-transformers",
3
3
  "description": "TypeScript transformer for code optimization",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "test": "bun test",
9
9
  "format": "prettier --write **/*.{ts,json,md,yml}",
10
10
  "check": "tsc -p .",
11
- "lint": "eslint ."
11
+ "lint": "eslint .",
12
+ "build:example": "cd example && bun run build.ts"
12
13
  },
13
14
  "dependencies": {
14
15
  "typescript": "^5"
@@ -20,7 +21,8 @@
20
21
  "@types/bun": "latest",
21
22
  "rollup": "latest",
22
23
  "@rollup/plugin-typescript": "latest",
23
- "tslib": "latest"
24
+ "tslib": "latest",
25
+ "esbuild": "latest"
24
26
  },
25
27
  "module": "./src/index.ts",
26
28
  "types": "./src/index.ts",
@@ -9,13 +9,9 @@ export interface EvaluationContext {
9
9
 
10
10
  export class EnumEvaluator {
11
11
  private lastImplicitValue = -1;
12
- private enumType: "numeric" | "string" | "mixed" = "numeric";
13
-
14
- constructor(private readonly typeChecker: ts.TypeChecker) {}
15
12
 
16
13
  reset(): void {
17
14
  this.lastImplicitValue = -1;
18
- this.enumType = "numeric";
19
15
  }
20
16
 
21
17
  evaluate(expr: ts.Expression, context: EvaluationContext): EnumValue {
@@ -1,7 +1,8 @@
1
1
  import ts from "typescript";
2
2
  import { EnumValue, EvaluationContext } from "./evaluator";
3
3
  import { EnumEvaluator } from "./evaluator";
4
- import { hasModifier, isConstEnumSymbol } from "./utils";
4
+ import { isConstEnumSymbol } from "./utils";
5
+ import { hasModifier } from "../typescript-helpers";
5
6
 
6
7
  export interface ConstEnumInfo {
7
8
  declaration: ts.EnumDeclaration;
@@ -19,13 +20,11 @@ export interface ConstEnumMemberInfo {
19
20
  export class ConstEnumRegistry {
20
21
  private readonly program: ts.Program;
21
22
  private readonly typeChecker: ts.TypeChecker;
22
- private readonly entrySourceFiles: readonly string[];
23
23
  private readonly enumDeclarations: Map<string, ConstEnumInfo>;
24
24
 
25
- constructor(program: ts.Program, entrySourceFiles?: readonly string[]) {
25
+ constructor(program: ts.Program) {
26
26
  this.program = program;
27
27
  this.typeChecker = program.getTypeChecker();
28
- this.entrySourceFiles = entrySourceFiles || program.getRootFileNames();
29
28
  this.enumDeclarations = new Map();
30
29
  this.collectConstEnumsFromEntryPoints();
31
30
  }
@@ -110,7 +109,7 @@ export class ConstEnumRegistry {
110
109
  }
111
110
 
112
111
  private evaluateEnumMembers(enumInfo: ConstEnumInfo): void {
113
- const evaluator = new EnumEvaluator(this.typeChecker);
112
+ const evaluator = new EnumEvaluator();
114
113
  evaluator.reset();
115
114
  const context: EvaluationContext = {
116
115
  localMembers: new Map(),
@@ -1,13 +1,13 @@
1
1
  import ts from "typescript";
2
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
3
  export const isConstEnumSymbol = (symbol: ts.Symbol): boolean => (symbol.flags & ts.SymbolFlags.ConstEnum) !== 0;
7
4
 
8
5
  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;
6
+ if (type) {
7
+ const symbol = type.symbol || type.aliasSymbol;
8
+ if (symbol) {
9
+ return (symbol.flags & ts.SymbolFlags.ConstEnum) !== 0;
10
+ }
11
+ }
12
+ return false;
13
13
  };
@@ -21,13 +21,12 @@ export class ExportsSymbolTree {
21
21
 
22
22
  public isSymbolAccessibleFromExports(symbol: ts.Symbol): boolean {
23
23
  symbol = this.getActualSymbol(symbol);
24
-
25
- let result = false;
26
- this.exportsTree.forEach((set: Set<ts.Symbol>) => {
27
- result = result || set.has(symbol);
28
- });
29
-
30
- return result;
24
+ for (const [, set] of this.exportsTree) {
25
+ if (set.has(symbol)) {
26
+ return true;
27
+ }
28
+ }
29
+ return false;
31
30
  }
32
31
 
33
32
  private computeTreeForExports(entrySourceFiles: readonly string[]): void {
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import ts from "typescript";
2
+ export type { OptimizerOptions } from "./types";
2
3
 
3
4
  import { ExportsSymbolTree } from "./exports/tracker";
4
5
  import {
@@ -32,8 +33,8 @@ function createTransformerFactory(
32
33
  const fullOptions: OptimizerOptions = { ...defaultOptions, ...options };
33
34
  const typeChecker = program.getTypeChecker();
34
35
  const exportsSymbolTree = new ExportsSymbolTree(program, fullOptions.entrySourceFiles);
35
- const constEnumRegistry = new ConstEnumRegistry(program, fullOptions.entrySourceFiles);
36
- const enumEvaluator = new EnumEvaluator(typeChecker);
36
+ const constEnumRegistry = new ConstEnumRegistry(program);
37
+ const enumEvaluator = new EnumEvaluator();
37
38
 
38
39
  const cache = new Map<ts.Symbol, VisibilityType>();
39
40
 
@@ -43,7 +44,7 @@ function createTransformerFactory(
43
44
  }
44
45
 
45
46
  return (context: ts.TransformationContext) => {
46
- function transformNode(node: ts.Node): ts.Node {
47
+ function transformNode(node: ts.Node): ts.Node | undefined {
47
48
  if (fullOptions.inlineConstEnums !== false) {
48
49
  if (ts.isPropertyAccessExpression(node)) {
49
50
  const inlined = tryInlineConstEnum(node);
@@ -52,12 +53,16 @@ function createTransformerFactory(
52
53
 
53
54
  if (ts.isImportSpecifier(node)) {
54
55
  const removed = tryRemoveConstEnumImport(node);
55
- if (removed === undefined) return undefined;
56
+ if (removed === undefined) {
57
+ return undefined;
58
+ }
56
59
  }
57
60
 
58
61
  if (ts.isImportClause(node)) {
59
62
  const removed = tryRemoveConstEnumImportClause(node);
60
- if (removed === undefined) return undefined;
63
+ if (removed === undefined) {
64
+ return undefined;
65
+ }
61
66
  }
62
67
  }
63
68
 
@@ -70,9 +75,8 @@ function createTransformerFactory(
70
75
  if (ts.isBindingElement(node) && node.propertyName === undefined) {
71
76
  if (node.parent && ts.isObjectBindingPattern(node.parent)) {
72
77
  return handleShorthandObjectBindingElement(node);
73
- } else {
74
- console.warn("!!!", node);
75
78
  }
79
+ return node;
76
80
  }
77
81
 
78
82
  // is not supported:
@@ -156,7 +160,7 @@ function createTransformerFactory(
156
160
  return node;
157
161
  }
158
162
 
159
- return createNewNode(propertyName, VisibilityType.Internal, context.factory.createStringLiteral);
163
+ return createNewNode(propertyName, VisibilityType.Private, context.factory.createStringLiteral);
160
164
  }
161
165
 
162
166
  // obj.node
@@ -266,14 +270,12 @@ function createTransformerFactory(
266
270
  type: VisibilityType,
267
271
  createNode: (newName: string) => T,
268
272
  ): T {
269
- const newPropertyName = getNewName(oldPropertyName, type);
273
+ const newPropertyName = getNewName(oldPropertyName);
270
274
  return createNode(newPropertyName);
271
275
  }
272
276
 
273
- function getNewName(originalName: string, type: VisibilityType): string {
274
- return `${
275
- type === VisibilityType.Private ? fullOptions.privatePrefix : fullOptions.internalPrefix
276
- }${originalName}`;
277
+ function getNewName(originalName: string): string {
278
+ return `${fullOptions.privatePrefix}${originalName}`;
277
279
  }
278
280
 
279
281
  function getActualSymbol(symbol: ts.Symbol): ts.Symbol {
@@ -548,7 +550,6 @@ function createTransformerFactory(
548
550
  }
549
551
 
550
552
  if (nodeSymbol.escapedName === "prototype") {
551
- // accessing to prototype
552
553
  return putToCache(nodeSymbol, VisibilityType.External);
553
554
  }
554
555
 
@@ -600,7 +601,7 @@ function createTransformerFactory(
600
601
  }
601
602
  }
602
603
 
603
- return putToCache(nodeSymbol, VisibilityType.Internal);
604
+ return putToCache(nodeSymbol, VisibilityType.Private);
604
605
  }
605
606
 
606
607
  function getShorthandObjectBindingElementSymbol(element: ts.BindingElement): ts.Symbol | null {
@@ -696,22 +697,21 @@ function createTransformerFactory(
696
697
  return undefined;
697
698
  }
698
699
 
699
- function wrapTransformNode(node: ts.Node): ts.Node {
700
+ function wrapTransformNode(node: ts.Node): ts.Node | undefined {
700
701
  if (ts.isEnumDeclaration(node)) {
701
702
  const result = handleEnumDeclaration(node);
702
- if (result === undefined) return undefined;
703
- if (result !== node) return result;
703
+ if (result === undefined) {
704
+ return undefined;
705
+ }
706
+ if (result !== node) {
707
+ return result;
708
+ }
704
709
  }
705
710
  return transformNode(node);
706
711
  }
707
712
 
708
- function wrappedTransformNodeAndChildren(node: ts.Node): ts.Node {
709
- return ts.visitEachChild(
710
- wrapTransformNode(node),
711
- (childNode: ts.Node) => wrappedTransformNodeAndChildren(childNode),
712
- context,
713
- );
714
- }
713
+ const wrappedTransformNodeAndChildren = (node: ts.Node): ts.Node | undefined =>
714
+ ts.visitEachChild(wrapTransformNode(node), wrappedTransformNodeAndChildren, context);
715
715
 
716
716
  return wrappedTransformNodeAndChildren(sourceFile) as ts.SourceFile;
717
717
  };
package/src/types.ts CHANGED
@@ -8,18 +8,11 @@ export interface OptimizerOptions {
8
8
 
9
9
  /**
10
10
  * Prefix of generated names for private fields
11
- * @example '_private_' // default
12
- * @example '$p$'
11
+ * @example '_private_'
12
+ * @example '$_' // default
13
13
  */
14
14
  privatePrefix: string;
15
15
 
16
- /**
17
- * Prefix of generated names for internal fields
18
- * @example '_internal_' // default
19
- * @example '$i$'
20
- */
21
- internalPrefix: string;
22
-
23
16
  /**
24
17
  * Comment which will treat a class/interface/type/property/etc and all its children as "public".
25
18
  * Set it to empty string to disable using JSDoc comment to detecting "visibility level".
@@ -42,15 +35,13 @@ export interface OptimizerOptions {
42
35
  }
43
36
 
44
37
  export const enum VisibilityType {
45
- Internal = 0,
46
- Private = 1,
47
- External = 2,
38
+ Private = 0,
39
+ External = 1,
48
40
  }
49
41
 
50
42
  export const defaultOptions: OptimizerOptions = {
51
43
  entrySourceFiles: [],
52
- privatePrefix: "$p$",
53
- internalPrefix: "$i$",
44
+ privatePrefix: "$_",
54
45
  publicJSDocTag: "public",
55
46
  ignoreDecorated: false,
56
47
  inlineConstEnums: true,
@@ -139,8 +139,8 @@ function getModifiers(node: ts.Node): readonly ts.Modifier[] {
139
139
  return node.modifiers || [];
140
140
  }
141
141
 
142
- export const hasModifier = (node: ts.Node, modifier: ts.SyntaxKind) =>
143
- getModifiers(node).some((mod) => mod.kind === modifier);
142
+ export const hasModifier = (node: ts.Node, modifier: ts.SyntaxKind): boolean =>
143
+ ts.canHaveModifiers(node) && getModifiers(node)?.some((mod) => mod.kind === modifier);
144
144
 
145
145
  function getDecorators(node: ts.Node): readonly unknown[] {
146
146
  if (isBreakingTypeScriptApi(ts)) {