@eliasku/ts-transformers 0.0.2 → 0.0.3

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,357 @@
1
1
  # @eliasku/ts-transformers
2
2
 
3
- TypeScript transformer for code optimization.
3
+ TypeScript transformer for code optimization and preparation for aggressive minification.
4
+
5
+ ## ⚠️ Important Requirement
6
+
7
+ **You must compile ALL your code from TypeScript files.**
8
+
9
+ - No pre-transpiled JavaScript files (`.js`) in your source code
10
+ - Transformer requires TypeScript type information to analyze visibility
11
+ - Any `.js` files will be included as-is without optimization
12
+ - Mix of `.ts` and `.js` sources → partial optimization, unexpected results
13
+
14
+ Applicable for only for application build, not libraries. For libraries it's better to use buildless approach, and provide `*.ts` files.
15
+
16
+ ## Approach
17
+
18
+ This transformer prepares your code for aggressive minification by analyzing TypeScript types and applying two main optimizations:
19
+
20
+ ### 1. Property Renaming with Detectable Prefixes
21
+
22
+ Based on type analysis, properties are categorized into three visibility levels:
23
+
24
+ - **External (Public)**: Exported from entry points → **no prefix** (preserved by minifiers)
25
+ - **Internal**: Used internally but not exported → prefixed with `$i$` (e.g., `$i$internalProperty`)
26
+ - **Private**: Private class members → prefixed with `$p$` (e.g., `$p$privateMethod`)
27
+
28
+ The special prefixes make these properties easily detectable by downstream minifiers (like **esbuild** or **terser**) for aggressive mangling, while preserving your public API surface.
29
+
30
+ **Example:**
31
+ ```typescript
32
+ // Before
33
+ class MyClass {
34
+ /** @public <- annotation in JSDoc to keep symbol name: property, method, field */
35
+ publicApi() {} // `publicApi` is not renamed because it's marked by annotation
36
+
37
+ public method() {} // Internal → renamed to $i$method
38
+ internalHelper() {} // Internal → renamed to $i$internalHelper
39
+ private secret = 1; // Private → renamed to $p$secret
40
+ }
41
+
42
+ // After transformer (before minifier)
43
+ class MyClass {
44
+ publicApi() {}
45
+ $i$apiMethod() {}
46
+ $i$internalHelper() {}
47
+ $p$secret = 1;
48
+ }
49
+
50
+ // After esbuild minifier (aggressive property mangling)
51
+ class A{publicApi(){},a(){},b(){},c=1}
52
+ ```
53
+
54
+ ### 2. Const Enum Inlining
55
+
56
+ Const enums are compile-time constants that should never exist at runtime. This transformer:
57
+
58
+ - Replaces all const enum member accesses with their literal values
59
+ - Removes const enum declarations from output
60
+ - Strips `const` modifier from declarations in `.d.ts` files
61
+ - Removes unused const enum imports
62
+
63
+ **Example:**
64
+ ```typescript
65
+ // Before
66
+ const enum Status {
67
+ Active = 1,
68
+ Inactive = 0
69
+ }
70
+
71
+ const status = Status.Active; // Access
72
+
73
+ // After transformer (and minifier)
74
+ const status = 1;
75
+ // Status declaration removed, import removed
76
+ ```
77
+
78
+ ## Workflow
79
+
80
+ This transformer is **Phase 1** of a two-phase optimization pipeline:
81
+
82
+ ### Phase 1: Type-Aware Preparation (This Transformer)
83
+ - **Input**: TypeScript source files
84
+ - **Process**: Analyze types, detect visibility, apply prefixes, inline const enums
85
+ - **Output**: ES modules with detectable prefixes
86
+ - **Tools**: `@eliasku/ts-transformers` + Rollup + TypeScript compiler
87
+
88
+ ### Phase 2: Aggressive Minification (esbuild / terser)
89
+ - **Input**: ES modules from Phase 1
90
+ - **Process**: Detect prefixes, mangle aggressively, apply all minification techniques
91
+ - **Output**: Minified bundle with preserved public API
92
+ - **Tools**: esbuild with property mangling
93
+
94
+ **Why Two Phases?**
95
+ - TypeScript types are only available during compilation (Phase 1)
96
+ - Production minifiers (Phase 2) are faster and more sophisticated
97
+ - Prefixes bridge the gap: Phase 1 marks what's safe to mangle, Phase 2 performs the mangling
4
98
 
5
99
  ## Usage
6
100
 
101
+ [Example Code](./example/build.ts)
102
+
7
103
  ```typescript
8
104
  import { optimizer } from "@eliasku/ts-transformers";
105
+ import typescript from "@rollup/plugin-typescript";
106
+ import { rollup } from "rollup";
107
+ import { build } from "esbuild";
9
108
 
10
- transformers: (program) => ({
11
- before: [
12
- optimizer(program, {
13
- entrySourceFiles: ["./src/index.ts"],
14
- inlineConstEnums: true,
109
+ // Phase 1: Type-aware optimization with Rollup
110
+ const bundle = await rollup({
111
+ /// ...
112
+ input: "./src/index.ts",
113
+ plugins: [
114
+ typescript({
115
+ transformers: (program) => ({
116
+ before: [
117
+ optimizer(program, {
118
+ entrySourceFiles: ["./src/index.ts"],
119
+ inlineConstEnums: true,
120
+ }),
121
+ ],
122
+ }),
15
123
  }),
16
124
  ],
17
125
  });
126
+
127
+ await bundle.write({
128
+ /// ...
129
+ file: "./dist/bundle.js",
130
+ format: "es",
131
+ });
132
+
133
+ // Phase 2: Aggressive minification with esbuild
134
+ await build({
135
+ entryPoints: ["./dist/bundle.js"],
136
+ outfile: "./dist/bundle.min.js",
137
+ minify: true,
138
+ mangleProps: /^\$[ip]\$/, // <- Match your custom prefixes here
139
+ mangleQuoted: false,
140
+ keepNames: false,
141
+ /// ...
142
+ });
143
+ ```
144
+
145
+ ### Customizing esbuild to Match Your Prefixes
146
+
147
+ If you customize the prefix options, update esbuild config to match:
148
+
149
+ ```typescript
150
+ optimizer(program, {
151
+ entrySourceFiles: ["./src/index.ts"],
152
+ internalPrefix: "_int_", // Custom internal prefix
153
+ privatePrefix: "_priv_", // Custom private prefix
154
+ });
155
+
156
+ // Then in esbuild:
157
+ mangleProps: /^(_int_|_priv_)/, // Match custom prefixes
18
158
  ```
19
159
 
20
160
  ## Options
21
161
 
22
162
  ### entrySourceFiles (required)
23
163
 
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.
164
+ An array of entry source files used to detect exported and external fields. This determines your public API surface.
25
165
 
26
- ### inlineConstEnums (optional, default: true)
166
+ ```typescript
167
+ entrySourceFiles: ["./src/index.ts"]
168
+ ```
27
169
 
28
- Whether to inline const enum values and remove const enum declarations.
170
+ ### internalPrefix (optional, default: "$i$")
29
171
 
30
- ### privatePrefix (optional, default: "$p$")
172
+ Prefix for internal properties (not exported, but used across your codebase). These will be aggressively mangled by esbuild.
173
+
174
+ ```typescript
175
+ internalPrefix: "$i$" // default: myFunction → $i$myFunction
176
+ ```
31
177
 
32
- Prefix of generated names for private fields.
178
+ ### privatePrefix (optional, default: "$p$")
33
179
 
34
- ### internalPrefix (optional, default: "$i$")
180
+ Prefix for private class members. These will be aggressively mangled by esbuild.
35
181
 
36
- Prefix of generated names for internal fields.
182
+ ```typescript
183
+ privatePrefix: "$p$" // default: this.private → this.$p$private
184
+ ```
37
185
 
38
186
  ### publicJSDocTag (optional, default: "public")
39
187
 
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".
188
+ JSDoc tag that marks a class/interface/property and all its children as public/external. Set to empty string to disable.
189
+
190
+ ```typescript
191
+ publicJSDocTag: "public" // default
192
+
193
+ class MyClass {
194
+ /** @public */
195
+ apiMethod() {} // Treated as external, no prefix applied
196
+
197
+ internalHelper() {} // Treated as internal, gets $i$ prefix
198
+ }
199
+ ```
41
200
 
42
201
  ### ignoreDecorated (optional, default: false)
43
202
 
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.
203
+ Whether decorated fields should be renamed. A field is "decorated" if itself or any parent (on type level) has a decorator.
204
+
205
+ ```typescript
206
+ ignoreDecorated: true // Don't rename decorated fields
207
+
208
+ @Component({
209
+ selector: "app-root"
210
+ })
211
+ class AppComponent {
212
+ @Input() data: any; // Decorated → not renamed with ignoreDecorated: true
213
+ private internal = 1; // Still renamed to $p$internal
214
+ }
215
+ ```
216
+
217
+ ### inlineConstEnums (optional, default: true)
218
+
219
+ Whether to inline const enum values and remove const enum declarations.
220
+
221
+ ## Examples
222
+
223
+ ### Example 1: Simple Property Renaming
224
+
225
+ ```typescript
226
+ // src/index.ts (before)
227
+ class Calculator {
228
+ add(a: number, b: number): number {
229
+ return a + b;
230
+ }
231
+ logResult(value: number): void {
232
+ console.log(value);
233
+ }
234
+ }
235
+
236
+ export const calc = new Calculator();
237
+ export { Calculator };
238
+
239
+ // After transformer (before minifier)
240
+ class Calculator {
241
+ add(a, b) { return a + b; } // Exported method → no prefix
242
+ $i$logResult(value) { // Internal method → $i$ prefix
243
+ console.log(value);
244
+ }
245
+ }
246
+ ```
247
+
248
+ ### Example 2: JSDoc Public Annotation
249
+
250
+ ```typescript
251
+ // src/index.ts (before)
252
+ class API {
253
+ /** @public */
254
+ fetchData(url: string): Promise<Data> {
255
+ return fetch(url);
256
+ }
257
+
258
+ private cache = new Map();
259
+ internalTransform(data: Data): Processed {
260
+ // transformation logic
261
+ }
262
+ }
263
+
264
+ export const api = new API();
265
+
266
+ // After transformer (before minifier)
267
+ class API {
268
+ fetchData(url) { return fetch(url); } // @public → no prefix
269
+ $p$cache = new Map(); // Private → $p$ prefix
270
+ $i$internalTransform(data) { // Internal → $i$ prefix
271
+ // transformation logic
272
+ }
273
+ }
274
+ ```
275
+
276
+ ### Example 3: Const Enum Inlining
277
+
278
+ ```typescript
279
+ // src/index.ts (before)
280
+ const enum LogLevel {
281
+ Debug = 0,
282
+ Info = 1,
283
+ Error = 2
284
+ }
285
+
286
+ function log(level: LogLevel, message: string): void {
287
+ console.log(`[${LogLevel[level]}] ${message}`);
288
+ }
289
+
290
+ export { log, LogLevel };
291
+
292
+ // After transformer (before minifier)
293
+ function log(level, message) {
294
+ console.log(`[${level}] ${message}`);
295
+ }
296
+ export { log };
297
+
298
+ // After esbuild minifier
299
+ function log(n,e){console.log(`[${n}]${e}`)}export{log};
300
+ // LogLevel values inlined: LogLevel.Debug → 0, LogLevel.Info → 1, etc.
301
+ // LogLevel enum declaration removed entirely
302
+ ```
303
+
304
+ ### Example 4: Complete Transformation + Minification
305
+
306
+ ```typescript
307
+ // src/index.ts (before)
308
+ const enum HttpStatus {
309
+ OK = 200,
310
+ NotFound = 404
311
+ }
312
+
313
+ class API {
314
+ private baseUrl = "https://api.example.com";
315
+ /** @public */
316
+ async get(path: string): Promise<Response> {
317
+ const url = `${this.baseUrl}${path}`;
318
+ const response = await fetch(url);
319
+ return this.handleResponse(response);
320
+ }
321
+
322
+ private async handleResponse(response: Response): Promise<Response> {
323
+ return response;
324
+ }
325
+
326
+ logStatus(status: HttpStatus): void {
327
+ console.log(status);
328
+ }
329
+ }
330
+
331
+ export const api = new API();
332
+
333
+ // After transformer (before minifier)
334
+ class API {
335
+ $p$baseUrl = "https://api.example.com";
336
+ async get(path) {
337
+ const url = `${this.$p$baseUrl}${path}`;
338
+ const response = await fetch(url);
339
+ return this.$i$handleResponse(response);
340
+ }
341
+
342
+ $i$handleResponse(response) {
343
+ return response;
344
+ }
345
+
346
+ $i$logStatus(status) {
347
+ console.log(status);
348
+ }
349
+ }
350
+
351
+ // After esbuild minifier
352
+ class t{a="https://api.example.com";async get(t){const n=`${this.a}${t}`;return await fetch(n)}b(t){return t}c(t){console.log(t)}}const s=new t;export{s};
353
+ ```
354
+
355
+ ## License
356
+
357
+ MIT
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.3",
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:
@@ -548,7 +552,6 @@ function createTransformerFactory(
548
552
  }
549
553
 
550
554
  if (nodeSymbol.escapedName === "prototype") {
551
- // accessing to prototype
552
555
  return putToCache(nodeSymbol, VisibilityType.External);
553
556
  }
554
557
 
@@ -696,22 +699,21 @@ function createTransformerFactory(
696
699
  return undefined;
697
700
  }
698
701
 
699
- function wrapTransformNode(node: ts.Node): ts.Node {
702
+ function wrapTransformNode(node: ts.Node): ts.Node | undefined {
700
703
  if (ts.isEnumDeclaration(node)) {
701
704
  const result = handleEnumDeclaration(node);
702
- if (result === undefined) return undefined;
703
- if (result !== node) return result;
705
+ if (result === undefined) {
706
+ return undefined;
707
+ }
708
+ if (result !== node) {
709
+ return result;
710
+ }
704
711
  }
705
712
  return transformNode(node);
706
713
  }
707
714
 
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
- }
715
+ const wrappedTransformNodeAndChildren = (node: ts.Node): ts.Node | undefined =>
716
+ ts.visitEachChild(wrapTransformNode(node), wrappedTransformNodeAndChildren, context);
715
717
 
716
718
  return wrappedTransformNodeAndChildren(sourceFile) as ts.SourceFile;
717
719
  };
@@ -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)) {