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 +135 -73
- package/bench/out/include/Promise.lua +2068 -0
- package/bench/out/include/RuntimeLib.lua +260 -0
- package/bench/out/src/server/benchmark.server.luau +122 -0
- package/bench/out/src/shared/fns-bare.luau +258 -0
- package/bench/out/src/shared/fns.luau +259 -0
- package/bench/src/server/benchmark.server.ts +30 -30
- package/bench/src/shared/fns.ts +2 -0
- package/bench/tsconfig.notransform.json +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/index.js +3 -3
- package/dist/passes/annotate.js +240 -47
- package/dist/passes/native.d.ts +1 -1
- package/dist/passes/native.js +11 -5
- package/dist/util.d.ts +1 -0
- package/dist/util.js +4 -0
- package/package.json +1 -3
- package/src/config.ts +4 -0
- package/src/index.ts +2 -2
- package/src/passes/annotate.ts +251 -58
- package/src/passes/native.ts +25 -10
- package/src/util.ts +4 -0
- package/bench/package.json +0 -14
- package/scripts/postprocess.js +0 -29
- /package/bench/{benchmark.project.json → default.project.json} +0 -0
package/src/passes/annotate.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type ts from "typescript";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
|
|
5
|
-
// Luau type names for TypeScript type strings rotor knows about
|
|
6
5
|
const LUAU_TYPE: Record<string, string> = {
|
|
7
6
|
number: "number",
|
|
8
7
|
string: "string",
|
|
@@ -24,7 +23,6 @@ const LUAU_TYPE: Record<string, string> = {
|
|
|
24
23
|
Region3: "Region3",
|
|
25
24
|
Ray: "Ray",
|
|
26
25
|
buffer: "buffer",
|
|
27
|
-
// Roblox service types
|
|
28
26
|
Instance: "Instance",
|
|
29
27
|
BasePart: "BasePart",
|
|
30
28
|
Part: "Part",
|
|
@@ -34,37 +32,26 @@ const LUAU_TYPE: Record<string, string> = {
|
|
|
34
32
|
Workspace: "Workspace",
|
|
35
33
|
RunService: "RunService",
|
|
36
34
|
Players: "Players",
|
|
37
|
-
// Luau numeric arrays — kept as {number} in Luau
|
|
38
35
|
};
|
|
39
36
|
|
|
40
37
|
type FnAnnotation = {
|
|
41
|
-
params: Array<string | null>;
|
|
38
|
+
params: Array<string | null>;
|
|
39
|
+
ret: string | null;
|
|
42
40
|
};
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
type FileSidecar = {
|
|
43
|
+
fns: Map<string, FnAnnotation>;
|
|
44
|
+
consts: Set<string>;
|
|
45
|
+
};
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
}
|
|
47
|
+
const sidecar = new Map<string, FileSidecar>();
|
|
48
|
+
let hooked = false;
|
|
61
49
|
|
|
62
50
|
function mapTypeNode(ts: typeof import("typescript"), typeNode: ts.TypeNode): string | null {
|
|
63
51
|
if (ts.isTypeReferenceNode(typeNode)) {
|
|
64
52
|
const name = ts.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : null;
|
|
65
53
|
if (!name) return null;
|
|
66
54
|
if (LUAU_TYPE[name]) return LUAU_TYPE[name];
|
|
67
|
-
// Array<T> → {T}
|
|
68
55
|
if ((name === "Array" || name === "ReadonlyArray") && typeNode.typeArguments?.length === 1) {
|
|
69
56
|
const inner = mapTypeNode(ts, typeNode.typeArguments[0]);
|
|
70
57
|
return inner ? `{${inner}}` : "{any}";
|
|
@@ -84,25 +71,44 @@ function mapTypeNode(ts: typeof import("typescript"), typeNode: ts.TypeNode): st
|
|
|
84
71
|
return null;
|
|
85
72
|
}
|
|
86
73
|
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
74
|
+
function luauTypeForParam(
|
|
75
|
+
ts: typeof import("typescript"),
|
|
76
|
+
checker: ts.TypeChecker,
|
|
77
|
+
node: ts.ParameterDeclaration,
|
|
90
78
|
): string | null {
|
|
79
|
+
if (node.type) {
|
|
80
|
+
const mapped = mapTypeNode(ts, node.type);
|
|
81
|
+
if (mapped) return mapped;
|
|
82
|
+
}
|
|
83
|
+
const name = checker.typeToString(checker.getTypeAtLocation(node));
|
|
84
|
+
return LUAU_TYPE[name] ?? null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function luauTypeForReturn(
|
|
88
|
+
ts: typeof import("typescript"),
|
|
89
|
+
checker: ts.TypeChecker,
|
|
90
|
+
node: ts.FunctionDeclaration,
|
|
91
|
+
): string | null {
|
|
92
|
+
if (node.type) {
|
|
93
|
+
const mapped = mapTypeNode(ts, node.type);
|
|
94
|
+
if (mapped) return mapped;
|
|
95
|
+
}
|
|
96
|
+
const sig = checker.getSignatureFromDeclaration(node);
|
|
97
|
+
if (!sig) return null;
|
|
98
|
+
const ret = checker.getReturnTypeOfSignature(sig);
|
|
99
|
+
const name = checker.typeToString(ret);
|
|
100
|
+
return LUAU_TYPE[name] ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function outPathForSource(sourceFile: ts.SourceFile, program: ts.Program): string | null {
|
|
91
104
|
const options = program.getCompilerOptions();
|
|
92
105
|
const outDir = options.outDir;
|
|
93
106
|
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());
|
|
107
|
+
const rootDir = options.rootDir ?? commonRoot(program.getRootFileNames());
|
|
98
108
|
if (!rootDir) return null;
|
|
99
|
-
|
|
100
109
|
const rel = path.relative(rootDir, sourceFile.fileName);
|
|
101
110
|
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);
|
|
111
|
+
return path.join(outDir, rel.replace(/\.tsx?$/, ".luau"));
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
function commonRoot(files: readonly string[]): string | undefined {
|
|
@@ -124,15 +130,25 @@ function collectAnnotations(
|
|
|
124
130
|
sourceFile: ts.SourceFile,
|
|
125
131
|
outPath: string,
|
|
126
132
|
): void {
|
|
127
|
-
const
|
|
128
|
-
sidecar.set(outPath,
|
|
133
|
+
const entry = sidecar.get(outPath) ?? { fns: new Map<string, FnAnnotation>(), consts: new Set<string>() };
|
|
134
|
+
sidecar.set(outPath, entry);
|
|
129
135
|
|
|
130
136
|
function visit(node: ts.Node): void {
|
|
131
137
|
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
if (params.some(p => p !== null)) {
|
|
135
|
-
|
|
138
|
+
const params = node.parameters.map(p => luauTypeForParam(ts, checker, p));
|
|
139
|
+
const ret = luauTypeForReturn(ts, checker, node);
|
|
140
|
+
if (params.some(p => p !== null) || ret !== null) {
|
|
141
|
+
entry.fns.set(node.name.text, { params, ret });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (ts.isVariableStatement(node)) {
|
|
145
|
+
const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
|
|
146
|
+
if (isConst) {
|
|
147
|
+
for (const decl of node.declarationList.declarations) {
|
|
148
|
+
if (ts.isIdentifier(decl.name)) {
|
|
149
|
+
entry.consts.add(decl.name.text);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
136
152
|
}
|
|
137
153
|
}
|
|
138
154
|
ts.forEachChild(node, visit);
|
|
@@ -140,33 +156,215 @@ function collectAnnotations(
|
|
|
140
156
|
visit(sourceFile);
|
|
141
157
|
}
|
|
142
158
|
|
|
143
|
-
function
|
|
159
|
+
function byLengthDesc(a: string, b: string): number {
|
|
160
|
+
return b.length - a.length;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
type ImportGroup = { label: string; lines: string[] };
|
|
164
|
+
|
|
165
|
+
function organizePreamble(src: string): string {
|
|
166
|
+
const lines = src.split("\n");
|
|
167
|
+
let i = 0;
|
|
168
|
+
|
|
169
|
+
// Collect --! directives separately from other leading comments
|
|
170
|
+
const shebang: string[] = [];
|
|
171
|
+
const header: string[] = [];
|
|
172
|
+
while (i < lines.length && lines[i].startsWith("--")) {
|
|
173
|
+
if (lines[i].startsWith("--!")) {
|
|
174
|
+
shebang.push(lines[i++]);
|
|
175
|
+
} else {
|
|
176
|
+
header.push(lines[i++]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
shebang.sort(byLengthDesc);
|
|
180
|
+
|
|
181
|
+
const services: string[] = [];
|
|
182
|
+
const runtime: string[] = [];
|
|
183
|
+
const importGroups: ImportGroup[] = [];
|
|
184
|
+
const bindings: string[] = [];
|
|
185
|
+
|
|
186
|
+
let pendingLabel: string | null = null;
|
|
187
|
+
let currentImports: string[] = [];
|
|
188
|
+
|
|
189
|
+
function flushImports(): void {
|
|
190
|
+
if (currentImports.length > 0) {
|
|
191
|
+
importGroups.push({ label: pendingLabel ?? "-- Imports", lines: [...currentImports] });
|
|
192
|
+
currentImports = [];
|
|
193
|
+
}
|
|
194
|
+
pendingLabel = null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
while (i < lines.length) {
|
|
198
|
+
const line = lines[i];
|
|
199
|
+
|
|
200
|
+
if (line.trim() === "") {
|
|
201
|
+
// Blank line = end of current import group
|
|
202
|
+
flushImports();
|
|
203
|
+
i++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (/^--!/.test(line)) {
|
|
208
|
+
// Rotor can emit --!native after preamble locals — hoist it up
|
|
209
|
+
shebang.push(line); shebang.sort(byLengthDesc); i++;
|
|
210
|
+
} else if (/^--/.test(line)) {
|
|
211
|
+
// User comment becomes the label for the next import group
|
|
212
|
+
flushImports();
|
|
213
|
+
pendingLabel = line; i++;
|
|
214
|
+
} else if (/^local \w+ = game:GetService\(/.test(line)) {
|
|
215
|
+
flushImports();
|
|
216
|
+
services.push(line); i++;
|
|
217
|
+
} else if (/^local \w+ = require\(/.test(line)) {
|
|
218
|
+
flushImports();
|
|
219
|
+
runtime.push(line); i++;
|
|
220
|
+
} else if (/^local \w+ = TS\.import\(/.test(line)) {
|
|
221
|
+
currentImports.push(line); i++;
|
|
222
|
+
} else if (/^local \w+ = \w+[\.\[]/.test(line) && !/^local function/.test(line)) {
|
|
223
|
+
flushImports();
|
|
224
|
+
bindings.push(line); i++;
|
|
225
|
+
} else {
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
flushImports();
|
|
230
|
+
|
|
231
|
+
services.sort(byLengthDesc);
|
|
232
|
+
bindings.sort(byLengthDesc);
|
|
233
|
+
|
|
234
|
+
const out: string[] = [...shebang];
|
|
235
|
+
if (header.length > 0) out.push("", ...header);
|
|
236
|
+
if (runtime.length > 0) out.push("", "-- Runtime", ...runtime);
|
|
237
|
+
if (services.length > 0) out.push("", "-- Services", ...services);
|
|
238
|
+
for (const group of importGroups) {
|
|
239
|
+
group.lines.sort(byLengthDesc);
|
|
240
|
+
out.push("", group.label, ...group.lines);
|
|
241
|
+
}
|
|
242
|
+
if (bindings.length > 0) out.push("", "-- Bindings", ...bindings);
|
|
243
|
+
if (i < lines.length) out.push("", ...lines.slice(i));
|
|
244
|
+
|
|
245
|
+
return out.join("\n");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function hoistGetService(src: string): string {
|
|
249
|
+
// Count occurrences of each game:GetService("X") call
|
|
250
|
+
const re = /game:GetService\("([^"]+)"\)/g;
|
|
251
|
+
const counts = new Map<string, number>();
|
|
252
|
+
for (const m of src.matchAll(re)) {
|
|
253
|
+
counts.set(m[1], (counts.get(m[1]) ?? 0) + 1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const toHoist = [...counts.entries()].filter(([, n]) => n >= 2).map(([svc]) => svc);
|
|
257
|
+
if (toHoist.length === 0) return src;
|
|
258
|
+
|
|
259
|
+
// Build locals and replace
|
|
260
|
+
const decls = toHoist
|
|
261
|
+
.map(svc => `local _${svc} = game:GetService("${svc}")`)
|
|
262
|
+
.join("\n");
|
|
263
|
+
|
|
264
|
+
for (const svc of toHoist) {
|
|
265
|
+
src = src.split(`game:GetService("${svc}")`).join(`_${svc}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Insert after any leading --! directives and the rotor header comment
|
|
269
|
+
const insertAt = src.search(/^(?!--[!\s]|--\s*Compiled)/m);
|
|
270
|
+
if (insertAt === -1) return decls + "\n" + src;
|
|
271
|
+
return src.slice(0, insertAt) + decls + "\n" + src.slice(insertAt);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function addSpacing(src: string): string {
|
|
275
|
+
const lines = src.split("\n");
|
|
276
|
+
const out: string[] = [];
|
|
277
|
+
|
|
278
|
+
for (let i = 0; i < lines.length; i++) {
|
|
279
|
+
const line = lines[i];
|
|
280
|
+
const trimmed = line.trim();
|
|
281
|
+
const prevOut = out.length > 0 ? out[out.length - 1] : "";
|
|
282
|
+
const prevTrimmed = prevOut.trim();
|
|
283
|
+
const alreadyBlank = prevTrimmed === "";
|
|
284
|
+
|
|
285
|
+
if (!alreadyBlank) {
|
|
286
|
+
// Blank before top-level local function
|
|
287
|
+
if (/^local function /.test(trimmed)) {
|
|
288
|
+
out.push("");
|
|
289
|
+
}
|
|
290
|
+
// Blank before return when it's not the first statement in its block
|
|
291
|
+
else if (
|
|
292
|
+
/^return\b/.test(trimmed) &&
|
|
293
|
+
!/\b(then|do|repeat)$/.test(prevTrimmed) &&
|
|
294
|
+
!/function\s*\([^)]*\)$/.test(prevTrimmed) &&
|
|
295
|
+
!/^local function /.test(prevTrimmed)
|
|
296
|
+
) {
|
|
297
|
+
out.push("");
|
|
298
|
+
}
|
|
299
|
+
// Blank before a block starter (do/while/for/if/repeat) when prev is local/const
|
|
300
|
+
else if (/^(do\b|while |for |if |repeat\b)/.test(trimmed) && /^(local |const )/.test(prevTrimmed)) {
|
|
301
|
+
out.push("");
|
|
302
|
+
}
|
|
303
|
+
// Blank on const → local transition
|
|
304
|
+
else if (/^local /.test(trimmed) && /^const /.test(prevTrimmed)) {
|
|
305
|
+
out.push("");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
out.push(line);
|
|
310
|
+
|
|
311
|
+
// Blank after `end` when next non-blank line is not end/else/elseif/until
|
|
312
|
+
if (trimmed === "end") {
|
|
313
|
+
const next = lines[i + 1]?.trim() ?? "";
|
|
314
|
+
if (next !== "" && !/^(end\b|else\b|elseif\b|until\b)/.test(next)) {
|
|
315
|
+
out.push("");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return out.join("\n");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function injectAnnotations(luauPath: string, entry: FileSidecar): void {
|
|
144
324
|
if (!fs.existsSync(luauPath)) return;
|
|
145
325
|
let src = fs.readFileSync(luauPath, "utf8");
|
|
146
326
|
let changed = false;
|
|
147
327
|
|
|
148
|
-
|
|
149
|
-
|
|
328
|
+
// Inject param + return type annotations
|
|
329
|
+
for (const [fnName, ann] of entry.fns) {
|
|
330
|
+
if (ann.params.every(p => p === null) && ann.ret === null) continue;
|
|
150
331
|
|
|
151
|
-
// Match: local function fnName(a, b, c)
|
|
152
|
-
// Captures the param list so we can replace individual names
|
|
153
332
|
const re = new RegExp(
|
|
154
|
-
`(local function ${escapeRegex(fnName)}\\()([^)]*)(
|
|
333
|
+
`(local function ${escapeRegex(fnName)}\\()([^)]*)(\\.\\.\\.)?(\\))`,
|
|
155
334
|
);
|
|
156
|
-
src = src.replace(re, (
|
|
335
|
+
src = src.replace(re, (_m, open: string, rawParams: string, vararg: string | undefined, close: string) => {
|
|
157
336
|
const names = rawParams.split(",").map((s: string) => s.trim()).filter(Boolean);
|
|
158
337
|
const annotated = names.map((name: string, i: number) => {
|
|
159
|
-
// strip any existing annotation
|
|
160
338
|
const bare = name.split(":")[0].trim();
|
|
161
339
|
const typ = ann.params[i];
|
|
162
340
|
return typ ? `${bare}: ${typ}` : bare;
|
|
163
341
|
});
|
|
164
342
|
if (vararg) annotated.push("...");
|
|
343
|
+
const retSuffix = ann.ret ? `: ${ann.ret}` : "";
|
|
165
344
|
changed = true;
|
|
166
|
-
return `${open}${annotated.join(", ")}
|
|
345
|
+
return `${open}${annotated.join(", ")}${close}${retSuffix}`;
|
|
167
346
|
});
|
|
168
347
|
}
|
|
169
348
|
|
|
349
|
+
// Replace local → const for TypeScript const declarations
|
|
350
|
+
for (const name of entry.consts) {
|
|
351
|
+
const re = new RegExp(`^(\\t*)local (${escapeRegex(name)}) =`, "m");
|
|
352
|
+
const next = src.replace(re, `$1const $2 =`);
|
|
353
|
+
if (next !== src) { src = next; changed = true; }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Hoist any repeated game:GetService() calls injected by the compiler
|
|
357
|
+
const hoisted = hoistGetService(src);
|
|
358
|
+
if (hoisted !== src) { src = hoisted; changed = true; }
|
|
359
|
+
|
|
360
|
+
// Organize preamble into labeled sections
|
|
361
|
+
const organized = organizePreamble(src);
|
|
362
|
+
if (organized !== src) { src = organized; changed = true; }
|
|
363
|
+
|
|
364
|
+
// Add blank lines between top-level blocks for readability
|
|
365
|
+
const spaced = addSpacing(src);
|
|
366
|
+
if (spaced !== src) { src = spaced; changed = true; }
|
|
367
|
+
|
|
170
368
|
if (changed) fs.writeFileSync(luauPath, src, "utf8");
|
|
171
369
|
}
|
|
172
370
|
|
|
@@ -182,12 +380,10 @@ function installWatcher(outDir: string): void {
|
|
|
182
380
|
if (!filename || !filename.endsWith(".luau")) return;
|
|
183
381
|
const full = path.join(outDir, filename);
|
|
184
382
|
if (seen.has(full)) return;
|
|
185
|
-
const
|
|
186
|
-
if (!
|
|
383
|
+
const entry = sidecar.get(full);
|
|
384
|
+
if (!entry) return;
|
|
187
385
|
seen.add(full);
|
|
188
|
-
|
|
189
|
-
try { injectAnnotations(full, fileMap); } catch { /* ignore */ }
|
|
190
|
-
}
|
|
386
|
+
try { injectAnnotations(full, entry); } catch { /* ignore */ }
|
|
191
387
|
});
|
|
192
388
|
watcher.unref();
|
|
193
389
|
}
|
|
@@ -199,9 +395,6 @@ export function annotatePass(
|
|
|
199
395
|
): void {
|
|
200
396
|
const outPath = outPathForSource(sourceFile, program);
|
|
201
397
|
if (!outPath) return;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
collectAnnotations(ts, checker, sourceFile, outPath);
|
|
205
|
-
const outDir = program.getCompilerOptions().outDir!;
|
|
206
|
-
installWatcher(outDir);
|
|
398
|
+
collectAnnotations(ts, program.getTypeChecker(), sourceFile, outPath);
|
|
399
|
+
installWatcher(program.getCompilerOptions().outDir!);
|
|
207
400
|
}
|
package/src/passes/native.ts
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
import type ts from "typescript";
|
|
2
|
-
import { hasOptimizeDirective } from "../util";
|
|
2
|
+
import { hasOptimizeDirective, hasStrictDirective } from "../util";
|
|
3
3
|
|
|
4
4
|
export function nativePass(
|
|
5
5
|
ts: typeof import("typescript"),
|
|
6
6
|
ctx: ts.TransformationContext,
|
|
7
7
|
sourceFile: ts.SourceFile,
|
|
8
|
+
optimize: boolean,
|
|
9
|
+
strict: boolean,
|
|
8
10
|
): ts.SourceFile {
|
|
9
|
-
if (hasOptimizeDirective(sourceFile)) return sourceFile;
|
|
10
|
-
|
|
11
11
|
const factory = ctx.factory;
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
const prepend: ts.Statement[] = [];
|
|
13
|
+
|
|
14
|
+
if (strict && !hasStrictDirective(sourceFile)) {
|
|
15
|
+
prepend.push(ts.addSyntheticLeadingComment(
|
|
16
|
+
factory.createNotEmittedStatement(sourceFile),
|
|
17
|
+
ts.SyntaxKind.SingleLineCommentTrivia,
|
|
18
|
+
"!strict",
|
|
19
|
+
true,
|
|
20
|
+
));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (optimize && !hasOptimizeDirective(sourceFile)) {
|
|
24
|
+
prepend.push(ts.addSyntheticLeadingComment(
|
|
25
|
+
factory.createNotEmittedStatement(sourceFile),
|
|
26
|
+
ts.SyntaxKind.SingleLineCommentTrivia,
|
|
27
|
+
"!optimize 2",
|
|
28
|
+
true,
|
|
29
|
+
));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (prepend.length === 0) return sourceFile;
|
|
33
|
+
return factory.updateSourceFile(sourceFile, [...prepend, ...Array.from(sourceFile.statements)]);
|
|
19
34
|
}
|
package/src/util.ts
CHANGED
|
@@ -4,6 +4,10 @@ export function hasOptimizeDirective(sourceFile: ts.SourceFile): boolean {
|
|
|
4
4
|
return /^--!optimize\b/m.test(sourceFile.text) || /^\/\/!optimize\b/m.test(sourceFile.text);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
export function hasStrictDirective(sourceFile: ts.SourceFile): boolean {
|
|
8
|
+
return /^--!strict\b/m.test(sourceFile.text) || /^\/\/!strict\b/m.test(sourceFile.text);
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
export function chainKey(ts: typeof import("typescript"), node: ts.Expression): string | undefined {
|
|
8
12
|
if (ts.isIdentifier(node)) return node.text;
|
|
9
13
|
if (ts.isPropertyAccessExpression(node)) {
|
package/bench/package.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "rbxts-transform-perf-test",
|
|
3
|
-
"private": true,
|
|
4
|
-
"scripts": {
|
|
5
|
-
"compile": "lumen src -o out/src -i out/include -p default.project.json",
|
|
6
|
-
"watch": "lumen src -o out/src -i out/include -p default.project.json --watch"
|
|
7
|
-
},
|
|
8
|
-
"devDependencies": {
|
|
9
|
-
"@rbxts/compiler-types": "^3.0.0-types.0",
|
|
10
|
-
"@rbxts/types": "^1.0.908",
|
|
11
|
-
"typescript": "=5.5.3",
|
|
12
|
-
"rbxts-transform-boost": "file:../"
|
|
13
|
-
}
|
|
14
|
-
}
|
package/scripts/postprocess.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
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);
|
|
File without changes
|