rbxts-transform-boost 0.1.0

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/dist/util.js ADDED
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hasOptimizeDirective = hasOptimizeDirective;
4
+ exports.chainKey = chainKey;
5
+ exports.walk = walk;
6
+ exports.isAssignmentTarget = isAssignmentTarget;
7
+ function hasOptimizeDirective(sourceFile) {
8
+ return /^--!optimize\b/m.test(sourceFile.text) || /^\/\/!optimize\b/m.test(sourceFile.text);
9
+ }
10
+ function chainKey(ts, node) {
11
+ if (ts.isIdentifier(node))
12
+ return node.text;
13
+ if (ts.isPropertyAccessExpression(node)) {
14
+ const left = chainKey(ts, node.expression);
15
+ if (left === undefined)
16
+ return undefined;
17
+ return `${left}.${node.name.text}`;
18
+ }
19
+ return undefined;
20
+ }
21
+ function walk(ts, node, visitor) {
22
+ if (!node)
23
+ return;
24
+ visitor(node);
25
+ ts.forEachChild(node, child => { if (child)
26
+ walk(ts, child, visitor); });
27
+ }
28
+ function isAssignmentTarget(ts, node) {
29
+ const parent = node.parent;
30
+ if (!parent)
31
+ return false;
32
+ if (ts.isBinaryExpression(parent)) {
33
+ return parent.left === node &&
34
+ parent.operatorToken.kind === ts.SyntaxKind.EqualsToken;
35
+ }
36
+ if (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent))
37
+ return true;
38
+ return false;
39
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "rbxts-transform-boost",
3
+ "version": "0.1.0",
4
+ "description": "roblox-ts transformer: automatic --!native, GetService hoisting, property chain caching, loop bounds hoisting, and Luau type annotation injection",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "bench:build": "npm run build && cd bench && ~/.rokit/bin/rotor build",
11
+ "bench:build:baseline": "cd bench && ~/.rokit/bin/rotor build -p tsconfig.notransform.json -i out/include",
12
+ "bench:dev": "cd bench && ~/.rokit/bin/rotor dev",
13
+ "bench:rbxlx": "npm run bench:build && npm run bench:build:baseline && cd bench && ~/.rokit/bin/rojo build benchmark.project.json -o benchmark.rbxlx"
14
+ },
15
+ "keywords": [
16
+ "roblox-ts",
17
+ "rbxts",
18
+ "transformer",
19
+ "luau",
20
+ "native",
21
+ "performance"
22
+ ],
23
+ "peerDependencies": {
24
+ "typescript": ">=5.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^26.0.0"
28
+ }
29
+ }
package/rokit.toml ADDED
@@ -0,0 +1,3 @@
1
+ [tools]
2
+ rojo = "rojo-rbx/rojo@7.6.1"
3
+ rotor = "uproot/rotor@2.2.0"
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ // Replaces "-- !fn-native\n" markers (emitted by rbxts-transform-perf's native pass)
3
+ // with bare "@native\n" Luau attributes in compiled output files.
4
+ // Run after `rotor build` to make per-function @native actually take effect.
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ const MARKER = "--!fn-native\n";
10
+ const ATTRIBUTE = "@native\n";
11
+
12
+ function processDir(dir) {
13
+ if (!fs.existsSync(dir)) return;
14
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
15
+ const full = path.join(dir, entry.name);
16
+ if (entry.isDirectory()) {
17
+ processDir(full);
18
+ } else if (entry.name.endsWith(".luau") || entry.name.endsWith(".lua")) {
19
+ const original = fs.readFileSync(full, "utf8");
20
+ if (!original.includes(MARKER)) continue;
21
+ const patched = original.replaceAll(MARKER, ATTRIBUTE);
22
+ fs.writeFileSync(full, patched, "utf8");
23
+ console.log(`@native patched: ${full}`);
24
+ }
25
+ }
26
+ }
27
+
28
+ const outDir = process.argv[2] ?? path.join(__dirname, "..", "test", "out");
29
+ processDir(outDir);
package/src/config.ts ADDED
@@ -0,0 +1,10 @@
1
+ export interface PluginConfig {
2
+ // Prepend --!optimize 2 to every file that doesn't already have it.
3
+ // Default: true
4
+ optimize?: boolean;
5
+
6
+ // Hoist repeated game.GetService() calls to module-level locals,
7
+ // and hoist repeated property access chains within functions to locals.
8
+ // Default: true
9
+ hoist?: boolean;
10
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type ts from "typescript";
2
+ import type { PluginConfig } from "./config";
3
+ import { nativePass } from "./passes/native";
4
+ import { cachePass } from "./passes/cache";
5
+ import { loopsPass } from "./passes/loops";
6
+ import { annotatePass } from "./passes/annotate";
7
+
8
+ export type { PluginConfig };
9
+
10
+ export default function(
11
+ program: ts.Program,
12
+ config: PluginConfig = {},
13
+ { ts }: { ts: typeof import("typescript") },
14
+ ): ts.TransformerFactory<ts.SourceFile> {
15
+ const { optimize = true, hoist = true } = config;
16
+
17
+ return (ctx: ts.TransformationContext) => (sourceFile: ts.SourceFile): ts.SourceFile => {
18
+ annotatePass(ts, program, sourceFile);
19
+ let result = sourceFile;
20
+ if (hoist) result = cachePass(ts, program, ctx, result);
21
+ result = loopsPass(ts, program, ctx, result);
22
+ if (optimize) result = nativePass(ts, ctx, result);
23
+ return result;
24
+ };
25
+ }
@@ -0,0 +1,207 @@
1
+ import type ts from "typescript";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ // Luau type names for TypeScript type strings rotor knows about
6
+ const LUAU_TYPE: Record<string, string> = {
7
+ number: "number",
8
+ string: "string",
9
+ boolean: "boolean",
10
+ Vector3: "Vector3",
11
+ Vector2: "Vector2",
12
+ Vector2int16: "Vector2int16",
13
+ Vector3int16: "Vector3int16",
14
+ CFrame: "CFrame",
15
+ UDim: "UDim",
16
+ UDim2: "UDim2",
17
+ Color3: "Color3",
18
+ BrickColor: "BrickColor",
19
+ TweenInfo: "TweenInfo",
20
+ NumberRange: "NumberRange",
21
+ NumberSequence: "NumberSequence",
22
+ ColorSequence: "ColorSequence",
23
+ Rect: "Rect",
24
+ Region3: "Region3",
25
+ Ray: "Ray",
26
+ buffer: "buffer",
27
+ // Roblox service types
28
+ Instance: "Instance",
29
+ BasePart: "BasePart",
30
+ Part: "Part",
31
+ Model: "Model",
32
+ Player: "Player",
33
+ Camera: "Camera",
34
+ Workspace: "Workspace",
35
+ RunService: "RunService",
36
+ Players: "Players",
37
+ // Luau numeric arrays — kept as {number} in Luau
38
+ };
39
+
40
+ type FnAnnotation = {
41
+ params: Array<string | null>; // null = unknown/skip
42
+ };
43
+
44
+ // Global sidecar: outLuauPath → list of function annotations for that file
45
+ const sidecar = new Map<string, Map<string, FnAnnotation>>();
46
+ let hooked = false;
47
+
48
+ function luauTypeForTsType(
49
+ ts: typeof import("typescript"),
50
+ checker: ts.TypeChecker,
51
+ node: ts.ParameterDeclaration,
52
+ ): string | null {
53
+ if (node.type) {
54
+ const mapped = mapTypeNode(ts, node.type);
55
+ if (mapped) return mapped;
56
+ }
57
+ const type = checker.getTypeAtLocation(node);
58
+ const name = checker.typeToString(type);
59
+ return LUAU_TYPE[name] ?? null;
60
+ }
61
+
62
+ function mapTypeNode(ts: typeof import("typescript"), typeNode: ts.TypeNode): string | null {
63
+ if (ts.isTypeReferenceNode(typeNode)) {
64
+ const name = ts.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : null;
65
+ if (!name) return null;
66
+ if (LUAU_TYPE[name]) return LUAU_TYPE[name];
67
+ // Array<T> → {T}
68
+ if ((name === "Array" || name === "ReadonlyArray") && typeNode.typeArguments?.length === 1) {
69
+ const inner = mapTypeNode(ts, typeNode.typeArguments[0]);
70
+ return inner ? `{${inner}}` : "{any}";
71
+ }
72
+ return null;
73
+ }
74
+ if (ts.isArrayTypeNode(typeNode)) {
75
+ const inner = mapTypeNode(ts, typeNode.elementType);
76
+ return inner ? `{${inner}}` : "{any}";
77
+ }
78
+ const kw: Partial<Record<number, string>> = {
79
+ [ts.SyntaxKind.NumberKeyword]: "number",
80
+ [ts.SyntaxKind.StringKeyword]: "string",
81
+ [ts.SyntaxKind.BooleanKeyword]: "boolean",
82
+ };
83
+ if (typeNode.kind in kw) return kw[typeNode.kind]!;
84
+ return null;
85
+ }
86
+
87
+ function outPathForSource(
88
+ sourceFile: ts.SourceFile,
89
+ program: ts.Program,
90
+ ): string | null {
91
+ const options = program.getCompilerOptions();
92
+ const outDir = options.outDir;
93
+ if (!outDir) return null;
94
+
95
+ // Compute rootDir: explicit option or the common root of all source files
96
+ const rootDir = options.rootDir
97
+ ?? commonRoot(program.getRootFileNames());
98
+ if (!rootDir) return null;
99
+
100
+ const rel = path.relative(rootDir, sourceFile.fileName);
101
+ if (rel.startsWith("..")) return null;
102
+
103
+ // Change .ts / .tsx extension to .luau
104
+ const luauRel = rel.replace(/\.tsx?$/, ".luau");
105
+ return path.join(outDir, luauRel);
106
+ }
107
+
108
+ function commonRoot(files: readonly string[]): string | undefined {
109
+ if (files.length === 0) return undefined;
110
+ const parts = files[0].split(path.sep);
111
+ let root = parts.slice(0, parts.length - 1);
112
+ for (const f of files.slice(1)) {
113
+ const fp = f.split(path.sep);
114
+ let i = 0;
115
+ while (i < root.length && i < fp.length - 1 && root[i] === fp[i]) i++;
116
+ root = root.slice(0, i);
117
+ }
118
+ return root.join(path.sep) || undefined;
119
+ }
120
+
121
+ function collectAnnotations(
122
+ ts: typeof import("typescript"),
123
+ checker: ts.TypeChecker,
124
+ sourceFile: ts.SourceFile,
125
+ outPath: string,
126
+ ): void {
127
+ const fileMap = sidecar.get(outPath) ?? new Map<string, FnAnnotation>();
128
+ sidecar.set(outPath, fileMap);
129
+
130
+ function visit(node: ts.Node): void {
131
+ if (ts.isFunctionDeclaration(node) && node.name) {
132
+ const fnName = node.name.text;
133
+ const params = node.parameters.map(p => luauTypeForTsType(ts, checker, p));
134
+ if (params.some(p => p !== null)) {
135
+ fileMap.set(fnName, { params });
136
+ }
137
+ }
138
+ ts.forEachChild(node, visit);
139
+ }
140
+ visit(sourceFile);
141
+ }
142
+
143
+ function injectAnnotations(luauPath: string, fileMap: Map<string, FnAnnotation>): void {
144
+ if (!fs.existsSync(luauPath)) return;
145
+ let src = fs.readFileSync(luauPath, "utf8");
146
+ let changed = false;
147
+
148
+ for (const [fnName, ann] of fileMap) {
149
+ if (ann.params.every(p => p === null)) continue;
150
+
151
+ // Match: local function fnName(a, b, c)
152
+ // Captures the param list so we can replace individual names
153
+ const re = new RegExp(
154
+ `(local function ${escapeRegex(fnName)}\\()([^)]*)(\\.\\.\\.\\))?\\)`,
155
+ );
156
+ src = src.replace(re, (_match: string, open: string, rawParams: string, vararg: string | undefined) => {
157
+ const names = rawParams.split(",").map((s: string) => s.trim()).filter(Boolean);
158
+ const annotated = names.map((name: string, i: number) => {
159
+ // strip any existing annotation
160
+ const bare = name.split(":")[0].trim();
161
+ const typ = ann.params[i];
162
+ return typ ? `${bare}: ${typ}` : bare;
163
+ });
164
+ if (vararg) annotated.push("...");
165
+ changed = true;
166
+ return `${open}${annotated.join(", ")})`;
167
+ });
168
+ }
169
+
170
+ if (changed) fs.writeFileSync(luauPath, src, "utf8");
171
+ }
172
+
173
+ function escapeRegex(s: string): string {
174
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
175
+ }
176
+
177
+ function installWatcher(outDir: string): void {
178
+ if (hooked) return;
179
+ hooked = true;
180
+ const seen = new Set<string>();
181
+ const watcher = fs.watch(outDir, { recursive: true }, (_event, filename) => {
182
+ if (!filename || !filename.endsWith(".luau")) return;
183
+ const full = path.join(outDir, filename);
184
+ if (seen.has(full)) return;
185
+ const fileMap = sidecar.get(full);
186
+ if (!fileMap) return;
187
+ seen.add(full);
188
+ if (fileMap.size > 0) {
189
+ try { injectAnnotations(full, fileMap); } catch { /* ignore */ }
190
+ }
191
+ });
192
+ watcher.unref();
193
+ }
194
+
195
+ export function annotatePass(
196
+ ts: typeof import("typescript"),
197
+ program: ts.Program,
198
+ sourceFile: ts.SourceFile,
199
+ ): void {
200
+ const outPath = outPathForSource(sourceFile, program);
201
+ if (!outPath) return;
202
+
203
+ const checker = program.getTypeChecker();
204
+ collectAnnotations(ts, checker, sourceFile, outPath);
205
+ const outDir = program.getCompilerOptions().outDir!;
206
+ installWatcher(outDir);
207
+ }
@@ -0,0 +1,163 @@
1
+ import type ts from "typescript";
2
+ import { chainKey, walk, isAssignmentTarget } from "../util";
3
+
4
+ function isGetServiceCall(ts: typeof import("typescript"), node: ts.Node): node is ts.CallExpression {
5
+ if (!ts.isCallExpression(node)) return false;
6
+ const expr = node.expression;
7
+ if (!ts.isPropertyAccessExpression(expr)) return false;
8
+ const obj = expr.expression;
9
+ return ts.isIdentifier(obj) && obj.text === "game" && expr.name.text === "GetService";
10
+ }
11
+
12
+ function getServiceName(ts: typeof import("typescript"), call: ts.CallExpression): string | undefined {
13
+ const args = call.arguments;
14
+ if (args.length !== 1) return undefined;
15
+ const arg = args[0];
16
+ if (!ts.isStringLiteral(arg)) return undefined;
17
+ return arg.text;
18
+ }
19
+
20
+ export function cachePass(
21
+ ts: typeof import("typescript"),
22
+ _program: ts.Program,
23
+ ctx: ts.TransformationContext,
24
+ sourceFile: ts.SourceFile,
25
+ ): ts.SourceFile {
26
+ const factory = ctx.factory;
27
+
28
+ // --- GetService hoisting ---
29
+ const services = new Map<string, string>();
30
+ walk(ts, sourceFile, node => {
31
+ if (!node || !isGetServiceCall(ts, node)) return;
32
+ const name = getServiceName(ts, node as ts.CallExpression);
33
+ if (name && !services.has(name)) services.set(name, `_${name}`);
34
+ });
35
+
36
+ const serviceVisitor = (node: ts.Node): ts.Node => {
37
+ if (isGetServiceCall(ts, node)) {
38
+ const name = getServiceName(ts, node as ts.CallExpression);
39
+ if (name && services.has(name)) return factory.createIdentifier(services.get(name)!);
40
+ }
41
+ return ts.visitEachChild(node, serviceVisitor, ctx);
42
+ };
43
+
44
+ let result = ts.visitEachChild(sourceFile, serviceVisitor, ctx) as ts.SourceFile;
45
+
46
+ if (services.size > 0) {
47
+ const hoistDecls = Array.from(services.entries()).map(([name, localName]) =>
48
+ factory.createVariableStatement(
49
+ undefined,
50
+ factory.createVariableDeclarationList(
51
+ [factory.createVariableDeclaration(
52
+ factory.createIdentifier(localName),
53
+ undefined,
54
+ undefined,
55
+ factory.createCallExpression(
56
+ factory.createPropertyAccessExpression(
57
+ factory.createIdentifier("game"),
58
+ "GetService",
59
+ ),
60
+ undefined,
61
+ [factory.createStringLiteral(name)],
62
+ ),
63
+ )],
64
+ ts.NodeFlags.Const,
65
+ ),
66
+ )
67
+ );
68
+ result = factory.updateSourceFile(result, [...hoistDecls, ...Array.from(result.statements)]);
69
+ }
70
+
71
+ // --- Property chain hoisting within functions ---
72
+ result = hoistPropertyChains(ts, result, factory, ctx);
73
+ return result;
74
+ }
75
+
76
+ function hoistPropertyChains(
77
+ ts: typeof import("typescript"),
78
+ sourceFile: ts.SourceFile,
79
+ factory: ts.NodeFactory,
80
+ ctx: ts.TransformationContext,
81
+ ): ts.SourceFile {
82
+ const visitor = (node: ts.Node): ts.Node => {
83
+ if (
84
+ ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) ||
85
+ ts.isArrowFunction(node) || ts.isMethodDeclaration(node)
86
+ ) {
87
+ return hoistInFunction(ts, node as ts.FunctionLikeDeclaration, factory, ctx);
88
+ }
89
+ return ts.visitEachChild(node, visitor, ctx);
90
+ };
91
+ return ts.visitEachChild(sourceFile, visitor, ctx) as ts.SourceFile;
92
+ }
93
+
94
+ function hoistInFunction(
95
+ ts: typeof import("typescript"),
96
+ fn: ts.FunctionLikeDeclaration,
97
+ factory: ts.NodeFactory,
98
+ ctx: ts.TransformationContext,
99
+ ): ts.FunctionLikeDeclaration {
100
+ if (!fn.body || !ts.isBlock(fn.body)) return fn;
101
+
102
+ const counts = new Map<string, number>();
103
+ walk(ts, fn.body, node => {
104
+ if (!node || !ts.isPropertyAccessExpression(node)) return;
105
+ const key = chainKey(ts, node);
106
+ if (!key || !key.includes(".")) return;
107
+ if (isAssignmentTarget(ts, node)) return;
108
+ counts.set(key, (counts.get(key) ?? 0) + 1);
109
+ });
110
+
111
+ const toHoist = new Map<string, string>();
112
+ let counter = 0;
113
+
114
+ const candidates = Array.from(counts.entries())
115
+ .filter(([, count]) => count >= 2)
116
+ .sort((a, b) => b[0].length - a[0].length);
117
+
118
+ for (const [key] of candidates) {
119
+ const alreadyCovered = Array.from(toHoist.keys()).some(h => h.startsWith(key + "."));
120
+ if (alreadyCovered) continue;
121
+ toHoist.set(key, `_cache${counter++}`);
122
+ }
123
+
124
+ if (toHoist.size === 0) return fn;
125
+
126
+ const chainVisitor = (node: ts.Node): ts.Node => {
127
+ if (ts.isPropertyAccessExpression(node)) {
128
+ const key = chainKey(ts, node);
129
+ if (key && toHoist.has(key) && !isAssignmentTarget(ts, node)) {
130
+ return factory.createIdentifier(toHoist.get(key)!);
131
+ }
132
+ }
133
+ return ts.visitEachChild(node, chainVisitor, ctx);
134
+ };
135
+
136
+ const newBody = ts.visitEachChild(fn.body, chainVisitor, ctx) as ts.Block;
137
+
138
+ const hoistStmts = Array.from(toHoist.entries()).map(([key, localName]) => {
139
+ const parts = key.split(".");
140
+ let expr: ts.Expression = factory.createIdentifier(parts[0]);
141
+ for (let i = 1; i < parts.length; i++) {
142
+ expr = factory.createPropertyAccessExpression(expr, parts[i]);
143
+ }
144
+ return factory.createVariableStatement(
145
+ undefined,
146
+ factory.createVariableDeclarationList(
147
+ [factory.createVariableDeclaration(
148
+ factory.createIdentifier(localName),
149
+ undefined, undefined, expr,
150
+ )],
151
+ ts.NodeFlags.Const,
152
+ ),
153
+ );
154
+ });
155
+
156
+ const updatedBody = factory.updateBlock(newBody, [...hoistStmts, ...Array.from(newBody.statements)]);
157
+
158
+ if (ts.isFunctionDeclaration(fn)) return factory.updateFunctionDeclaration(fn, fn.modifiers, fn.asteriskToken, fn.name, fn.typeParameters, fn.parameters, fn.type, updatedBody);
159
+ if (ts.isFunctionExpression(fn)) return factory.updateFunctionExpression(fn, fn.modifiers, fn.asteriskToken, fn.name, fn.typeParameters, fn.parameters, fn.type, updatedBody);
160
+ if (ts.isArrowFunction(fn)) return factory.updateArrowFunction(fn, fn.modifiers, fn.typeParameters, fn.parameters, fn.type, fn.equalsGreaterThanToken, updatedBody);
161
+ if (ts.isMethodDeclaration(fn)) return factory.updateMethodDeclaration(fn, fn.modifiers, fn.asteriskToken, fn.name, fn.questionToken, fn.typeParameters, fn.parameters, fn.type, updatedBody);
162
+ return fn;
163
+ }
@@ -0,0 +1,80 @@
1
+ import type ts from "typescript";
2
+
3
+ export function loopsPass(
4
+ ts: typeof import("typescript"),
5
+ _program: ts.Program,
6
+ ctx: ts.TransformationContext,
7
+ sourceFile: ts.SourceFile,
8
+ ): ts.SourceFile {
9
+ const factory = ctx.factory;
10
+
11
+ function isArrayLengthExpr(node: ts.Expression): ts.Expression | undefined {
12
+ // matches: arr.size() or arr.length (if it ever appears)
13
+ if (ts.isCallExpression(node)) {
14
+ const expr = node.expression;
15
+ if (ts.isPropertyAccessExpression(expr) && expr.name.text === "size" && node.arguments.length === 0) {
16
+ return expr.expression;
17
+ }
18
+ }
19
+ return undefined;
20
+ }
21
+
22
+ function rewriteForLoop(node: ts.ForStatement): ts.Statement {
23
+ // Match: for (let i = 0; i < arr.size(); i++) { ... }
24
+ const { initializer, condition, incrementor, statement } = node;
25
+ if (!initializer || !condition || !incrementor) return node;
26
+ if (!ts.isVariableDeclarationList(initializer)) return node;
27
+ const decls = initializer.declarations;
28
+ if (decls.length !== 1) return node;
29
+ const decl = decls[0];
30
+ if (!ts.isIdentifier(decl.name)) return node;
31
+ if (!decl.initializer || !ts.isNumericLiteral(decl.initializer) || decl.initializer.text !== "0") return node;
32
+ if (!ts.isBinaryExpression(condition)) return node;
33
+ if (condition.operatorToken.kind !== ts.SyntaxKind.LessThanToken) return node;
34
+
35
+ const arr = isArrayLengthExpr(condition.right);
36
+ if (!arr) return node;
37
+
38
+ const loopVar = decl.name.text;
39
+ const arrName = ts.isIdentifier(arr) ? arr.text : undefined;
40
+ if (!arrName) return node;
41
+
42
+ const lenName = `_len_${arrName}`;
43
+ const lenDecl = factory.createVariableStatement(
44
+ undefined,
45
+ factory.createVariableDeclarationList(
46
+ [factory.createVariableDeclaration(
47
+ factory.createIdentifier(lenName),
48
+ undefined,
49
+ factory.createTypeReferenceNode("number"),
50
+ factory.createCallExpression(
51
+ factory.createPropertyAccessExpression(arr, "size"),
52
+ undefined, [],
53
+ ),
54
+ )],
55
+ ts.NodeFlags.Const,
56
+ ),
57
+ );
58
+
59
+ const newCondition = factory.updateBinaryExpression(
60
+ condition,
61
+ condition.left,
62
+ condition.operatorToken,
63
+ factory.createIdentifier(lenName),
64
+ );
65
+ const newFor = factory.updateForStatement(
66
+ node, initializer, newCondition, incrementor, statement,
67
+ );
68
+
69
+ return factory.createBlock([lenDecl, newFor], true) as unknown as ts.Statement;
70
+ }
71
+
72
+ const visitor = (node: ts.Node): ts.Node => {
73
+ if (ts.isForStatement(node)) {
74
+ return rewriteForLoop(node);
75
+ }
76
+ return ts.visitEachChild(node, visitor, ctx);
77
+ };
78
+
79
+ return ts.visitEachChild(sourceFile, visitor, ctx) as ts.SourceFile;
80
+ }
@@ -0,0 +1,19 @@
1
+ import type ts from "typescript";
2
+ import { hasOptimizeDirective } from "../util";
3
+
4
+ export function nativePass(
5
+ ts: typeof import("typescript"),
6
+ ctx: ts.TransformationContext,
7
+ sourceFile: ts.SourceFile,
8
+ ): ts.SourceFile {
9
+ if (hasOptimizeDirective(sourceFile)) return sourceFile;
10
+
11
+ const factory = ctx.factory;
12
+ const optimize = ts.addSyntheticLeadingComment(
13
+ factory.createNotEmittedStatement(sourceFile),
14
+ ts.SyntaxKind.SingleLineCommentTrivia,
15
+ "!optimize 2",
16
+ true,
17
+ );
18
+ return factory.updateSourceFile(sourceFile, [optimize, ...Array.from(sourceFile.statements)]);
19
+ }
package/src/util.ts ADDED
@@ -0,0 +1,33 @@
1
+ import type ts from "typescript";
2
+
3
+ export function hasOptimizeDirective(sourceFile: ts.SourceFile): boolean {
4
+ return /^--!optimize\b/m.test(sourceFile.text) || /^\/\/!optimize\b/m.test(sourceFile.text);
5
+ }
6
+
7
+ export function chainKey(ts: typeof import("typescript"), node: ts.Expression): string | undefined {
8
+ if (ts.isIdentifier(node)) return node.text;
9
+ if (ts.isPropertyAccessExpression(node)) {
10
+ const left = chainKey(ts, node.expression);
11
+ if (left === undefined) return undefined;
12
+ return `${left}.${node.name.text}`;
13
+ }
14
+ return undefined;
15
+ }
16
+
17
+ export function walk(ts: typeof import("typescript"), node: ts.Node, visitor: (n: ts.Node) => void): void {
18
+ if (!node) return;
19
+ visitor(node);
20
+ ts.forEachChild(node, child => { if (child) walk(ts, child, visitor); });
21
+ }
22
+
23
+ export function isAssignmentTarget(ts: typeof import("typescript"), node: ts.Node): boolean {
24
+ const parent = node.parent;
25
+ if (!parent) return false;
26
+ if (ts.isBinaryExpression(parent)) {
27
+ return parent.left === node &&
28
+ parent.operatorToken.kind === ts.SyntaxKind.EqualsToken;
29
+ }
30
+ if (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) return true;
31
+ return false;
32
+ }
33
+
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "moduleResolution": "node",
6
+ "strict": true,
7
+ "declaration": true,
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "esModuleInterop": true,
11
+ "paths": { "typescript": ["./test/node_modules/typescript"] },
12
+ "skipLibCheck": true,
13
+ "types": ["node"]
14
+ },
15
+ "include": ["src"]
16
+ }