@eliasku/ts-transformers 0.0.1 → 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
@@ -0,0 +1,357 @@
1
+ # @eliasku/ts-transformers
2
+
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
98
+
99
+ ## Usage
100
+
101
+ [Example Code](./example/build.ts)
102
+
103
+ ```typescript
104
+ import { optimizer } from "@eliasku/ts-transformers";
105
+ import typescript from "@rollup/plugin-typescript";
106
+ import { rollup } from "rollup";
107
+ import { build } from "esbuild";
108
+
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
+ }),
123
+ }),
124
+ ],
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
158
+ ```
159
+
160
+ ## Options
161
+
162
+ ### entrySourceFiles (required)
163
+
164
+ An array of entry source files used to detect exported and external fields. This determines your public API surface.
165
+
166
+ ```typescript
167
+ entrySourceFiles: ["./src/index.ts"]
168
+ ```
169
+
170
+ ### internalPrefix (optional, default: "$i$")
171
+
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
+ ```
177
+
178
+ ### privatePrefix (optional, default: "$p$")
179
+
180
+ Prefix for private class members. These will be aggressively mangled by esbuild.
181
+
182
+ ```typescript
183
+ privatePrefix: "$p$" // default: this.private → this.$p$private
184
+ ```
185
+
186
+ ### publicJSDocTag (optional, default: "public")
187
+
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
+ ```
200
+
201
+ ### ignoreDecorated (optional, default: false)
202
+
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
- "description": "",
4
- "version": "0.0.1",
3
+ "description": "TypeScript transformer for code optimization",
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,11 +21,13 @@
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
  },
27
+ "module": "./src/index.ts",
28
+ "types": "./src/index.ts",
25
29
  "exports": {
26
- "./mangler": "./src/mangler/index.ts",
27
- "./const-enum": "./src/const-enum/index.ts"
30
+ ".": "./src/index.ts"
28
31
  },
29
32
  "repository": {
30
33
  "type": "git",
@@ -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 {
@@ -58,7 +54,6 @@ export class EnumEvaluator {
58
54
 
59
55
  private evaluateImplicitMember(member: ts.EnumMember): EnumValue {
60
56
  const name = ts.isIdentifier(member.name) ? member.name.text : `<computed>`;
61
- // unused
62
57
  void name;
63
58
 
64
59
  if (this.lastImplicitValue === -1) {
@@ -104,12 +99,10 @@ export class EnumEvaluator {
104
99
  const left = this.evaluate(expr.left, context);
105
100
  const right = this.evaluate(expr.right, context);
106
101
 
107
- // String concatenation
108
102
  if (typeof left === "string" && typeof right === "string" && expr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
109
103
  return left + right;
110
104
  }
111
105
 
112
- // Numeric operations
113
106
  if (typeof left === "number" && typeof right === "number") {
114
107
  switch (expr.operatorToken.kind) {
115
108
  case ts.SyntaxKind.BarToken:
@@ -1,8 +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";
5
- import { LOGS } from "../config";
4
+ import { isConstEnumSymbol } from "./utils";
5
+ import { hasModifier } from "../typescript-helpers";
6
6
 
7
7
  export interface ConstEnumInfo {
8
8
  declaration: ts.EnumDeclaration;
@@ -20,13 +20,11 @@ export interface ConstEnumMemberInfo {
20
20
  export class ConstEnumRegistry {
21
21
  private readonly program: ts.Program;
22
22
  private readonly typeChecker: ts.TypeChecker;
23
- private readonly entrySourceFiles: readonly string[];
24
23
  private readonly enumDeclarations: Map<string, ConstEnumInfo>;
25
24
 
26
- constructor(program: ts.Program, entrySourceFiles?: readonly string[]) {
25
+ constructor(program: ts.Program) {
27
26
  this.program = program;
28
27
  this.typeChecker = program.getTypeChecker();
29
- this.entrySourceFiles = entrySourceFiles || program.getRootFileNames();
30
28
  this.enumDeclarations = new Map();
31
29
  this.collectConstEnumsFromEntryPoints();
32
30
  }
@@ -56,29 +54,15 @@ export class ConstEnumRegistry {
56
54
  }
57
55
 
58
56
  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
57
  const sourceFiles = this.program.getSourceFiles();
65
- if (LOGS) {
66
- console.log(`[const-enum registry] Program has ${sourceFiles.length} source files`);
67
- }
68
58
 
69
59
  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
60
  if (sourceFile.isDeclarationFile) {
73
61
  continue;
74
62
  }
75
63
 
76
64
  this.registerConstEnumFromSource(sourceFile);
77
65
  }
78
-
79
- if (LOGS) {
80
- console.log(`[const-enum registry] Found ${this.enumDeclarations.size} const enum declarations`);
81
- }
82
66
  }
83
67
 
84
68
  private registerConstEnumFromSource(sourceFile: ts.SourceFile): void {
@@ -96,7 +80,6 @@ export class ConstEnumRegistry {
96
80
  const name = this.getEnumSymbolName(symbol);
97
81
 
98
82
  if (this.enumDeclarations.has(name)) {
99
- // Already registered (might be from different import)
100
83
  return;
101
84
  }
102
85
 
@@ -126,7 +109,7 @@ export class ConstEnumRegistry {
126
109
  }
127
110
 
128
111
  private evaluateEnumMembers(enumInfo: ConstEnumInfo): void {
129
- const evaluator = new EnumEvaluator(this.typeChecker);
112
+ const evaluator = new EnumEvaluator();
130
113
  evaluator.reset();
131
114
  const context: EvaluationContext = {
132
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
  };
@@ -8,7 +8,7 @@ import {
8
8
  getExportsForSourceFile,
9
9
  getDeclarationsForSymbol,
10
10
  } from "../utils/symbol-utils";
11
- import { LOGS } from "../../config";
11
+ import { LOGS } from "../config";
12
12
 
13
13
  export class ExportsSymbolTree {
14
14
  private readonly program: ts.Program;
@@ -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 {
@@ -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 {
@@ -14,23 +15,26 @@ import {
14
15
  isNodeNamedDeclaration,
15
16
  } from "./utils/symbol-utils";
16
17
  import { getNodeJSDocComment } from "./utils/ast-utils";
17
- import { RenameOptions, defaultOptions, VisibilityType } from "./types";
18
- import { LOGS } from "../config";
18
+ import { OptimizerOptions, defaultOptions, VisibilityType } from "./types";
19
+ import { ConstEnumRegistry } from "./const-enum/registry";
20
+ import { EnumEvaluator } from "./const-enum/evaluator";
21
+ import { isConstEnumType } from "./const-enum/utils";
22
+ import { LOGS } from "./config";
19
23
 
20
- export function propertiesRenameTransformer(
24
+ export const optimizer = (
21
25
  program: ts.Program,
22
- config?: Partial<RenameOptions>,
23
- ): ts.TransformerFactory<ts.SourceFile> {
24
- return createTransformerFactory(program, config);
25
- }
26
+ config?: Partial<OptimizerOptions>,
27
+ ): ts.TransformerFactory<ts.SourceFile> => createTransformerFactory(program, config);
26
28
 
27
29
  function createTransformerFactory(
28
30
  program: ts.Program,
29
- options?: Partial<RenameOptions>,
31
+ options?: Partial<OptimizerOptions>,
30
32
  ): ts.TransformerFactory<ts.SourceFile> {
31
- const fullOptions: RenameOptions = { ...defaultOptions, ...options };
33
+ const fullOptions: OptimizerOptions = { ...defaultOptions, ...options };
32
34
  const typeChecker = program.getTypeChecker();
33
35
  const exportsSymbolTree = new ExportsSymbolTree(program, fullOptions.entrySourceFiles);
36
+ const constEnumRegistry = new ConstEnumRegistry(program);
37
+ const enumEvaluator = new EnumEvaluator();
34
38
 
35
39
  const cache = new Map<ts.Symbol, VisibilityType>();
36
40
 
@@ -40,17 +44,28 @@ function createTransformerFactory(
40
44
  }
41
45
 
42
46
  return (context: ts.TransformationContext) => {
43
- function transformNodeAndChildren(node: ts.SourceFile, ctx: ts.TransformationContext): ts.SourceFile;
44
- function transformNodeAndChildren(node: ts.Node, ctx: ts.TransformationContext): ts.Node;
45
- function transformNodeAndChildren(node: ts.Node, ctx: ts.TransformationContext): ts.Node {
46
- return ts.visitEachChild(
47
- transformNode(node),
48
- (childNode: ts.Node) => transformNodeAndChildren(childNode, ctx),
49
- ctx,
50
- );
51
- }
47
+ function transformNode(node: ts.Node): ts.Node | undefined {
48
+ if (fullOptions.inlineConstEnums !== false) {
49
+ if (ts.isPropertyAccessExpression(node)) {
50
+ const inlined = tryInlineConstEnum(node);
51
+ if (inlined) return inlined;
52
+ }
53
+
54
+ if (ts.isImportSpecifier(node)) {
55
+ const removed = tryRemoveConstEnumImport(node);
56
+ if (removed === undefined) {
57
+ return undefined;
58
+ }
59
+ }
60
+
61
+ if (ts.isImportClause(node)) {
62
+ const removed = tryRemoveConstEnumImportClause(node);
63
+ if (removed === undefined) {
64
+ return undefined;
65
+ }
66
+ }
67
+ }
52
68
 
53
- function transformNode(node: ts.Node): ts.Node {
54
69
  // const a = { node }
55
70
  if (ts.isShorthandPropertyAssignment(node)) {
56
71
  return handleShorthandPropertyAssignment(node);
@@ -60,9 +75,8 @@ function createTransformerFactory(
60
75
  if (ts.isBindingElement(node) && node.propertyName === undefined) {
61
76
  if (node.parent && ts.isObjectBindingPattern(node.parent)) {
62
77
  return handleShorthandObjectBindingElement(node);
63
- } else {
64
- console.warn("!!!", node);
65
78
  }
79
+ return node;
66
80
  }
67
81
 
68
82
  // is not supported:
@@ -538,7 +552,6 @@ function createTransformerFactory(
538
552
  }
539
553
 
540
554
  if (nodeSymbol.escapedName === "prototype") {
541
- // accessing to prototype
542
555
  return putToCache(nodeSymbol, VisibilityType.External);
543
556
  }
544
557
 
@@ -637,6 +650,72 @@ function createTransformerFactory(
637
650
  return isSymbolClassMember(typeChecker.getSymbolAtLocation(node));
638
651
  }
639
652
 
640
- return (sourceFile: ts.SourceFile) => transformNodeAndChildren(sourceFile, context);
653
+ function tryInlineConstEnum(node: ts.PropertyAccessExpression): ts.Expression | null {
654
+ const expressionType = typeChecker.getTypeAtLocation(node.expression);
655
+ if (!isConstEnumType(expressionType)) return null;
656
+
657
+ const enumSymbol = expressionType.symbol || expressionType.aliasSymbol;
658
+ if (!enumSymbol) return null;
659
+
660
+ const enumInfo = constEnumRegistry.getEnumInfo(enumSymbol);
661
+ if (!enumInfo) return null;
662
+
663
+ const memberValue = enumInfo.members.get(node.name.text)?.value;
664
+ if (memberValue === undefined || memberValue === null) return null;
665
+
666
+ return enumEvaluator.createLiteral(memberValue);
667
+ }
668
+
669
+ function tryRemoveConstEnumImport(node: ts.ImportSpecifier): ts.ImportSpecifier | undefined {
670
+ const importedType = typeChecker.getTypeAtLocation(node);
671
+ if (isConstEnumType(importedType)) {
672
+ return undefined;
673
+ }
674
+ return node;
675
+ }
676
+
677
+ function tryRemoveConstEnumImportClause(node: ts.ImportClause): ts.ImportClause | undefined {
678
+ if (!node.name) return node;
679
+ const type = typeChecker.getTypeAtLocation(node.name);
680
+ if (isConstEnumType(type)) {
681
+ return undefined;
682
+ }
683
+ return node;
684
+ }
685
+
686
+ return (sourceFile: ts.SourceFile) => {
687
+ function handleEnumDeclaration(node: ts.EnumDeclaration): ts.EnumDeclaration | undefined {
688
+ if (fullOptions.inlineConstEnums === false) return node;
689
+ if (!hasModifier(node, ts.SyntaxKind.ConstKeyword)) return node;
690
+
691
+ if (sourceFile.isDeclarationFile) {
692
+ return ts.factory.updateEnumDeclaration(
693
+ node,
694
+ node.modifiers?.filter((m) => m.kind !== ts.SyntaxKind.ConstKeyword),
695
+ node.name,
696
+ node.members,
697
+ );
698
+ }
699
+ return undefined;
700
+ }
701
+
702
+ function wrapTransformNode(node: ts.Node): ts.Node | undefined {
703
+ if (ts.isEnumDeclaration(node)) {
704
+ const result = handleEnumDeclaration(node);
705
+ if (result === undefined) {
706
+ return undefined;
707
+ }
708
+ if (result !== node) {
709
+ return result;
710
+ }
711
+ }
712
+ return transformNode(node);
713
+ }
714
+
715
+ const wrappedTransformNodeAndChildren = (node: ts.Node): ts.Node | undefined =>
716
+ ts.visitEachChild(wrapTransformNode(node), wrappedTransformNodeAndChildren, context);
717
+
718
+ return wrappedTransformNodeAndChildren(sourceFile) as ts.SourceFile;
719
+ };
641
720
  };
642
721
  }
@@ -1,4 +1,4 @@
1
- export interface RenameOptions {
1
+ export interface OptimizerOptions {
2
2
  /**
3
3
  * An array of entry source files which will used to detect exported and internal fields.
4
4
  * Basically it should be entry point(s) of the library/project.
@@ -34,6 +34,11 @@ export interface RenameOptions {
34
34
  * A field is treated as "decorated" if itself or any its parent (on type level) has a decorator.
35
35
  */
36
36
  ignoreDecorated: boolean;
37
+
38
+ /**
39
+ * Whether to inline const enum values.
40
+ */
41
+ inlineConstEnums?: boolean;
37
42
  }
38
43
 
39
44
  export const enum VisibilityType {
@@ -42,10 +47,11 @@ export const enum VisibilityType {
42
47
  External = 2,
43
48
  }
44
49
 
45
- export const defaultOptions: RenameOptions = {
50
+ export const defaultOptions: OptimizerOptions = {
46
51
  entrySourceFiles: [],
47
52
  privatePrefix: "$p$",
48
53
  internalPrefix: "$i$",
49
54
  publicJSDocTag: "public",
50
55
  ignoreDecorated: false,
56
+ inlineConstEnums: true,
51
57
  };
@@ -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)) {
@@ -1,181 +0,0 @@
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
- }
File without changes
File without changes