rbxts-transform-boost 0.1.0 → 1.0.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/README.md CHANGED
@@ -4,11 +4,11 @@
4
4
  >
5
5
  > **What carried over:** Luau type annotation injection on function parameters for native codegen (primitives, Roblox value types, arrays).
6
6
  >
7
- > **What's new:** `--!optimize 2` on every file, `game:GetService()` hoisting to module-level locals, repeated property chain hoisting, loop bounds hoisting all without needing `//!native` in every file.
7
+ > **What's new:** `--!optimize 2` on every file, `game:GetService()` hoisting to module-level locals, repeated property chain hoisting, loop bounds hoisting, `const` keyword for TypeScript `const` declarations, output formatting so compiled files look human-written.
8
8
  >
9
- > **What's different:** The old package also annotated return types, local variable declarations, class methods, and user-defined interfaces/type aliases. Those are not yet in this package — they're planned but the implementation is non-trivial. The old package also had reliability issues that this rewrite addresses.
9
+ > **What's different:** The old package also annotated return types, local variable declarations, class methods, and user-defined interfaces/type aliases. Those are not yet in this package — they're planned. The old package also had reliability issues that this rewrite addresses.
10
10
 
11
- A roblox-ts transformer that automatically applies Luau performance directives at compile time — no runtime cost, no code changes required.
11
+ A TypeScript transformer for Roblox that automatically applies Luau performance directives and cleans up compiled output at build time — no runtime cost, no code changes required.
12
12
 
13
13
  ## Installation
14
14
 
@@ -24,6 +24,7 @@ npm install --save-dev rbxts-transform-boost
24
24
  {
25
25
  "transform": "rbxts-transform-boost",
26
26
  "optimize": true,
27
+ "strict": true,
27
28
  "hoist": true
28
29
  }
29
30
  ]
@@ -36,14 +37,15 @@ npm install --save-dev rbxts-transform-boost
36
37
  | Option | Type | Default | Description |
37
38
  |--------|------|---------|-------------|
38
39
  | `optimize` | `boolean` | `true` | Prepend `--!optimize 2` to every file that doesn't already have it |
40
+ | `strict` | `boolean` | `true` | Prepend `--!strict` to every file that doesn't already have it |
39
41
  | `hoist` | `boolean` | `true` | Hoist `GetService` calls and repeated property reads to locals |
40
42
 
41
- `--!native` is never auto-inserted. Add `//!native` at the top of your TypeScript file for hot paths you've profiled — rotor preserves it.
43
+ `--!native` is never auto-inserted. Add `//!native` at the top of your TypeScript file for hot paths you've profiled — the compiler preserves it.
42
44
 
43
45
  ```typescript
44
46
  //!native
45
47
  export function integrate(pos: Vector3, vel: Vector3, acc: Vector3, dt: number) {
46
- ...
48
+ // ...
47
49
  }
48
50
  ```
49
51
 
@@ -53,21 +55,10 @@ export function integrate(pos: Vector3, vel: Vector3, acc: Vector3, dt: number)
53
55
 
54
56
  ### `--!optimize 2` — always on top
55
57
 
56
- Every file gets `--!optimize 2` prepended if it doesn't already have it. Roblox already runs all scripts at optimization level 2, but the directive makes Studio behaviour match production and signals intent.
57
-
58
- ```typescript
59
- // TypeScript source
60
- export function encodeFixed(buf: buffer, offset: number, value: number, scale: number): number {
61
- const fixed = math.floor(value * scale);
62
- const clamped = math.clamp(fixed, -32768, 32767);
63
- buffer.writei16(buf, offset, clamped);
64
- return offset + 2;
65
- }
66
- ```
58
+ Every file gets `--!optimize 2` prepended if it doesn't already have it. Roblox already runs all scripts at optimization level 2 in live games, but the directive makes Studio behaviour match production and signals intent.
67
59
 
68
60
  ```lua
69
61
  -- Without transformer
70
- -- Compiled with rotor v2.2.0
71
62
  local function encodeFixed(buf, offset, value, scale)
72
63
  local fixed = math.floor(value * scale)
73
64
  local clamped = math.clamp(fixed, -32768, 32767)
@@ -79,20 +70,41 @@ end
79
70
  ```lua
80
71
  -- With transformer
81
72
  --!optimize 2
82
- -- Compiled with rotor v2.2.0
83
- local function encodeFixed(buf: buffer, offset: number, value: number, scale: number)
84
- local fixed = math.floor(value * scale)
85
- local clamped = math.clamp(fixed, -32768, 32767)
73
+
74
+ local function encodeFixed(buf: buffer, offset: number, value: number, scale: number): number
75
+ const fixed = math.floor(value * scale)
76
+ const clamped = math.clamp(fixed, -32768, 32767)
86
77
  buffer.writei16(buf, offset, clamped)
78
+
87
79
  return offset + 2
88
80
  end
89
81
  ```
90
82
 
91
83
  ---
92
84
 
85
+ ### `const` for TypeScript `const` declarations
86
+
87
+ TypeScript `const` declarations are emitted as Luau `const` (shipped in Roblox Studio March 2026). TypeScript `let` stays as `local`. Rotor-generated internal variables (`_cache0`, `_shouldIncrement`, etc.) are not affected.
88
+
89
+ ```typescript
90
+ // TypeScript
91
+ const N = 100000;
92
+ let i = 0;
93
+ const elapsed = os.clock() - t0;
94
+ ```
95
+
96
+ ```lua
97
+ -- Compiled output
98
+ const N = 100000
99
+ local i = 0
100
+ const elapsed = os.clock() - t0
101
+ ```
102
+
103
+ ---
104
+
93
105
  ### GetService hoisting
94
106
 
95
- Every `game:GetService("X")` call in a file is hoisted to a module-level local on first load. Functions that call `GetService` on every invocation — the most common pattern in rotor output — pay the registry lookup cost zero times at runtime.
107
+ Every `game:GetService("X")` call in a file is hoisted to a module-level local on first load. Functions that call `GetService` on every invocation — the most common compiled pattern — pay the registry lookup cost zero times at runtime.
96
108
 
97
109
  ```typescript
98
110
  // TypeScript source
@@ -115,12 +127,15 @@ end
115
127
  ```lua
116
128
  -- With transformer
117
129
  --!optimize 2
118
- local _Players = game:GetService("Players") -- hoisted once at module load
130
+
131
+ -- Services
119
132
  local _RunService = game:GetService("RunService")
133
+ local _Players = game:GetService("Players")
134
+
135
+ local function serviceWork(): string
136
+ const count = #_Players:GetPlayers()
137
+ const running = _RunService:IsRunning()
120
138
 
121
- local function serviceWork()
122
- local count = #_Players:GetPlayers()
123
- local running = _RunService:IsRunning()
124
139
  return `{count}-{running}`
125
140
  end
126
141
  ```
@@ -146,8 +161,8 @@ export function cameraWork(camera: Camera): number {
146
161
  ```lua
147
162
  -- Without transformer
148
163
  local function cameraWork(camera)
149
- local pos = camera.CFrame.Position -- engine call #1
150
- local look = camera.CFrame.LookVector -- engine call #2
164
+ local pos = camera.CFrame.Position
165
+ local look = camera.CFrame.LookVector
151
166
  local fov = camera.FieldOfView
152
167
  return pos.Magnitude + look.X + fov
153
168
  end
@@ -155,48 +170,28 @@ end
155
170
 
156
171
  ```lua
157
172
  -- With transformer
158
- local function cameraWork(camera: Camera)
159
- local _cache0 = camera.CFrame -- one engine call
160
- local pos = _cache0.Position -- local read
161
- local look = _cache0.LookVector -- local read
162
- local fov = camera.FieldOfView
173
+ local function cameraWork(camera: Camera): number
174
+ const _cache0 = camera.CFrame
175
+ const pos = _cache0.Position
176
+ const look = _cache0.LookVector
177
+ const fov = camera.FieldOfView
178
+
163
179
  return pos.Magnitude + look.X + fov
164
180
  end
165
181
  ```
166
182
 
167
183
  **1.5× faster.** Also hoists value type field reads (`.X`, `.Y`, `.Z`) when they appear multiple times — the `cross` function reads each component twice, so all six are hoisted:
168
184
 
169
- ```typescript
170
- // TypeScript source
171
- export function cross(a: Vector3, b: Vector3): Vector3 {
172
- return new Vector3(
173
- a.Y * b.Z - a.Z * b.Y,
174
- a.Z * b.X - a.X * b.Z,
175
- a.X * b.Y - a.Y * b.X,
176
- );
177
- }
178
- ```
179
-
180
- ```lua
181
- -- Without transformer
182
- local function cross(a, b)
183
- return Vector3.new(
184
- a.Y * b.Z - a.Z * b.Y,
185
- a.Z * b.X - a.X * b.Z,
186
- a.X * b.Y - a.Y * b.X
187
- )
188
- end
189
- ```
190
-
191
185
  ```lua
192
186
  -- With transformer
193
- local function cross(a: Vector3, b: Vector3)
194
- local _cache0 = a.Y -- each component read twice → all hoisted
195
- local _cache1 = b.Z
196
- local _cache2 = a.Z
197
- local _cache3 = b.Y
198
- local _cache4 = b.X
199
- local _cache5 = a.X
187
+ local function cross(a: Vector3, b: Vector3): Vector3
188
+ const _cache0 = a.Y
189
+ const _cache1 = b.Z
190
+ const _cache2 = a.Z
191
+ const _cache3 = b.Y
192
+ const _cache4 = b.X
193
+ const _cache5 = a.X
194
+
200
195
  return Vector3.new(
201
196
  _cache0 * _cache1 - _cache2 * _cache3,
202
197
  _cache2 * _cache4 - _cache5 * _cache1,
@@ -211,7 +206,7 @@ end
211
206
 
212
207
  ### Loop bounds hoisting
213
208
 
214
- `arr.size()` in a `for` loop condition is re-evaluated on every iteration in rotor's output. The loops pass hoists it to a `const` before the loop.
209
+ `arr.size()` in a `for` loop condition is re-evaluated on every iteration in compiled output. The loops pass hoists it to a local before the loop.
215
210
 
216
211
  ```typescript
217
212
  // TypeScript source
@@ -228,7 +223,7 @@ export function sumWeighted(values: Array<number>, weights: Array<number>): numb
228
223
  -- Without transformer
229
224
  local function sumWeighted(values, weights)
230
225
  local total = 0
231
- for i = 0, #values - 1 do -- #values evaluated once per iteration
226
+ for i = 0, #values - 1 do
232
227
  total += values[i + 1] * weights[i + 1]
233
228
  end
234
229
  return total
@@ -237,12 +232,32 @@ end
237
232
 
238
233
  ```lua
239
234
  -- With transformer
240
- local function sumWeighted(values: {number}, weights: {number})
235
+ local function sumWeighted(values: {number}, weights: {number}): number
241
236
  local total = 0
242
- local _len_values = #values -- hoisted: evaluated once total
243
- for i = 0, _len_values - 1 do
244
- total += values[i + 1] * weights[i + 1]
237
+
238
+ do
239
+ const _len_values = #values
240
+
241
+ do
242
+ local i = 0
243
+ local _shouldIncrement = false
244
+
245
+ while true do
246
+ if _shouldIncrement then
247
+ i += 1
248
+ else
249
+ _shouldIncrement = true
250
+ end
251
+
252
+ if not (i < _len_values) then
253
+ break
254
+ end
255
+
256
+ total += values[i + 1] * weights[i + 1]
257
+ end
258
+ end
245
259
  end
260
+
246
261
  return total
247
262
  end
248
263
  ```
@@ -253,7 +268,7 @@ end
253
268
 
254
269
  ### Luau type annotation injection
255
270
 
256
- After rotor writes `.luau` files, the transformer injects Luau type annotations on function parameters. This lets the native compiler generate specialized code for numeric and Roblox value types.
271
+ After the compiler writes `.luau` files, the transformer injects Luau type annotations on function parameters and return types. This lets the native compiler generate specialized code for numeric and Roblox value types.
257
272
 
258
273
  ```typescript
259
274
  // TypeScript source
@@ -271,7 +286,7 @@ end
271
286
 
272
287
  ```lua
273
288
  -- With transformer
274
- local function dot(a: Vector3, b: Vector3)
289
+ local function dot(a: Vector3, b: Vector3): number
275
290
  return a.X * b.X + a.Y * b.Y + a.Z * b.Z
276
291
  end
277
292
  ```
@@ -280,6 +295,51 @@ Supported types: `number`, `string`, `boolean`, `Vector3`, `Vector2`, `CFrame`,
280
295
 
281
296
  ---
282
297
 
298
+ ### Output formatting
299
+
300
+ This one's a personal pet peeve — yes, most people will never open a compiled `.luau` file. But the transformer post-processes every compiled `.luau` file so the output looks like a human wrote it anyway, not a compiler.
301
+
302
+ **Preamble organisation** — top-level declarations are sorted into labeled sections in dependency order. Sections are sorted by line length (longest first). If you put a comment before a group of imports in TypeScript, that comment becomes the section label:
303
+
304
+ ```typescript
305
+ // Shared
306
+ import * as utils from "../shared/utils";
307
+
308
+ // Server
309
+ import * as data from "../server/data";
310
+ ```
311
+
312
+ ```lua
313
+ --!optimize 2
314
+ --!native
315
+
316
+ -- Compiled with rotor v2.2.0
317
+
318
+ -- Runtime
319
+ local TS = require(...)
320
+
321
+ -- Services
322
+ local _ReplicatedStorage = game:GetService("ReplicatedStorage")
323
+ local _Workspace = game:GetService("Workspace")
324
+
325
+ -- Shared
326
+ local utils = TS.import(script, ...)
327
+
328
+ -- Server
329
+ local data = TS.import(script, ...)
330
+ ```
331
+
332
+ **Spacing inside functions** — blank lines are added so blocks breathe:
333
+
334
+ - Before `return` when it's not the only statement in the function
335
+ - After `end` blocks when the next line is not another `end`, `else`, or `elseif`
336
+ - Before block starters (`do`/`while`/`for`/`if`) when preceded by a group of `local`/`const` assignments
337
+ - At `const` → `local` transitions
338
+
339
+ **`--!` directives** are sorted by length and separated from the rotor header comment with a blank line.
340
+
341
+ ---
342
+
283
343
  ## Benchmarks
284
344
 
285
345
  Measured in Roblox Studio server context. 100,000 iterations per benchmark (10,000 for `cfLookAt`). Same TypeScript source compiled two ways — with and without the transformer.
@@ -316,8 +376,10 @@ Measured in Roblox Studio server context. 100,000 iterations per benchmark (10,0
316
376
  ## Development
317
377
 
318
378
  ```bash
319
- npm run build # build the transformer
320
- npm run bench:rbxlx # build both versions + produce bench/benchmark.rbxlx
379
+ npm run build # compile the transformer (tsc → dist/)
380
+ npm run bench:build # build the benchmark suite with transformer applied
381
+ npm run bench:build:baseline # build the baseline suite (no transformer)
382
+ npm run bench:rbxlx # produce bench/benchmark.rbxlx (both suites in one place)
321
383
  ```
322
384
 
323
- Open `bench/benchmark.rbxlx` in Roblox Studio and run the server. The optimized suite prints first, then the baseline suite, sequentially in the output window.
385
+ Open `bench/benchmark.rbxlx` in Roblox Studio and run the server. The optimized suite prints first, then the baseline suite.