cluaupp 0.1.4 → 0.2.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.
Files changed (113) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +12 -7
  3. package/bin/cluau.js +1 -1
  4. package/bin/cluaupp.js +1 -1
  5. package/docs/README.md +1 -1
  6. package/docs/architecture.md +52 -77
  7. package/docs/cli.md +29 -9
  8. package/docs/comparison.md +3 -3
  9. package/docs/config.md +6 -6
  10. package/docs/cpp-organization.md +2 -2
  11. package/docs/examples/index.md +2 -0
  12. package/docs/getting-started.md +7 -5
  13. package/docs/intellisense.md +49 -31
  14. package/docs/intro.md +2 -2
  15. package/docs/oop/file-tags.md +16 -17
  16. package/docs/oop/index.md +2 -2
  17. package/docs/oop/modules.md +3 -1
  18. package/docs/oop/services.md +7 -14
  19. package/docs/syntax.md +6 -5
  20. package/editors/vscode/extension.js +97 -146
  21. package/editors/vscode/package.json +6 -6
  22. package/examples/README.md +18 -0
  23. package/examples/game/.clangd +25 -0
  24. package/examples/game/.vscode/c_cpp_properties.json +27 -0
  25. package/examples/game/.vscode/extensions.json +5 -0
  26. package/examples/game/.vscode/settings.json +27 -0
  27. package/examples/game/Packages/.gitkeep +0 -0
  28. package/examples/game/cluaupp.config.json +6 -0
  29. package/examples/game/compile_flags.txt +6 -0
  30. package/examples/game/default.project.json +39 -0
  31. package/examples/game/package.json +9 -0
  32. package/examples/game/rokit.toml +5 -0
  33. package/examples/game/src/client/hud.client.cpp +12 -0
  34. package/{src → examples/game/src}/client/init.client.cpp +1 -0
  35. package/examples/game/src/server/combat.server.cpp +23 -0
  36. package/{src → examples/game/src}/server/leaderstats.server.cpp +1 -0
  37. package/examples/game/wally.toml +11 -0
  38. package/generated/api.js +51 -0
  39. package/generated/architecture.js +473 -0
  40. package/generated/ast.js +2 -0
  41. package/generated/cli.js +191 -0
  42. package/generated/compile.js +115 -0
  43. package/generated/editor-install.js +170 -0
  44. package/generated/emit.js +503 -0
  45. package/generated/emitter/luau-codegen.js +167 -0
  46. package/generated/emitter/translators.js +164 -0
  47. package/generated/headers.js +220 -0
  48. package/generated/index.html +49 -0
  49. package/generated/intellisense.js +277 -0
  50. package/generated/layout.js +112 -0
  51. package/generated/lex.js +146 -0
  52. package/generated/libs.js +484 -0
  53. package/generated/lsp.js +55 -0
  54. package/generated/package-info.js +11 -0
  55. package/generated/parse.js +607 -0
  56. package/generated/parser/collector.js +100 -0
  57. package/generated/parser/index.js +30 -0
  58. package/generated/preprocess.js +181 -0
  59. package/generated/system-understander.js +443 -0
  60. package/generated/transpile.js +66 -0
  61. package/generated/types.js +2 -0
  62. package/generated/understand.js +298 -0
  63. package/generated/utils/process-orchestrator.js +63 -0
  64. package/generated/utils/project.js +491 -0
  65. package/generated/utils/rojo-mapper.js +181 -0
  66. package/generated/utils/safe-paths.js +104 -0
  67. package/package.json +25 -8
  68. package/src/api.ts +53 -0
  69. package/src/{architecture.js → architecture.ts} +80 -33
  70. package/src/ast.ts +37 -0
  71. package/src/cli.ts +214 -0
  72. package/src/compile.ts +118 -0
  73. package/src/editor-install.ts +182 -0
  74. package/src/{emit.js → emit.ts} +5 -6
  75. package/src/emitter/luau-codegen.ts +187 -0
  76. package/src/emitter/translators.ts +168 -0
  77. package/src/headers.ts +233 -0
  78. package/src/intellisense.ts +286 -0
  79. package/src/{layout.js → layout.ts} +114 -129
  80. package/src/{lex.js → lex.ts} +4 -6
  81. package/src/{libs.js → libs.ts} +119 -46
  82. package/src/lsp.ts +60 -0
  83. package/src/package-info.ts +7 -0
  84. package/src/{parse.js → parse.ts} +3 -4
  85. package/src/parser/collector.ts +115 -0
  86. package/src/parser/index.ts +27 -0
  87. package/src/{preprocess.js → preprocess.ts} +64 -32
  88. package/src/system-understander.ts +501 -0
  89. package/src/transpile.ts +64 -0
  90. package/src/tree-sitter-cpp.d.ts +4 -0
  91. package/src/types.ts +81 -0
  92. package/src/{understand.js → understand.ts} +24 -213
  93. package/src/utils/process-orchestrator.ts +72 -0
  94. package/src/utils/project.ts +506 -0
  95. package/src/utils/rojo-mapper.ts +190 -0
  96. package/src/utils/safe-paths.ts +98 -0
  97. package/templates/game/.clangd +9 -0
  98. package/templates/game/.vscode/c_cpp_properties.json +1 -1
  99. package/templates/game/.vscode/extensions.json +5 -6
  100. package/templates/game/.vscode/settings.json +7 -6
  101. package/templates/game/cluaupp.config.json +2 -2
  102. package/templates/game/compile_flags.txt +1 -0
  103. package/templates/game/rokit.toml +5 -0
  104. package/templates/game/src/client/init.client.cpp +1 -0
  105. package/templates/game/src/server/leaderstats.server.cpp +1 -0
  106. package/src/api.js +0 -57
  107. package/src/cli.js +0 -481
  108. package/src/compile.js +0 -56
  109. package/src/editor-install.js +0 -218
  110. package/src/intellisense.js +0 -1368
  111. package/src/lsp.js +0 -227
  112. /package/{src → examples/game/src}/shared/config.cpp +0 -0
  113. /package/{src → examples/game/src}/shared/config.h +0 -0
@@ -0,0 +1,501 @@
1
+ import path from "node:path";
2
+
3
+ export type ArchitectureRole = "manager" | "controller" | "service" | "utility" | "handler";
4
+ export type RuntimeSide = "server" | "client" | "plugin" | "shared";
5
+ export type EmitKind = "service" | "module" | "legacy";
6
+ export type RojoClass = "Script" | "LocalScript" | "ModuleScript";
7
+ export type RunContext = "Server" | "Client" | "Plugin" | null;
8
+
9
+ export interface IntentSpec {
10
+ role: ArchitectureRole;
11
+ module: string;
12
+ side: RuntimeSide | "any";
13
+ tokens: string[];
14
+ }
15
+
16
+ export interface ScoredIntent {
17
+ name: string;
18
+ score: number;
19
+ evidence: string[];
20
+ role: ArchitectureRole;
21
+ module: string;
22
+ side: RuntimeSide | "any";
23
+ }
24
+
25
+ export interface FileTagResult {
26
+ key: string;
27
+ emit: EmitKind;
28
+ runtime: RuntimeSide;
29
+ rojo: RojoClass;
30
+ runContext: RunContext;
31
+ }
32
+
33
+ export interface ArchitectureReport {
34
+ role: ArchitectureRole;
35
+ suggestedModule: string;
36
+ fileTag: FileTagResult;
37
+ injectedServices: string[];
38
+ isUtility: boolean;
39
+ intents: ScoredIntent[];
40
+ reasoning: string[];
41
+ }
42
+
43
+ const EXT = "(cpp|cc|cxx|c|h|hpp|hh)";
44
+ const EXT_REGEX = new RegExp(`\\.${EXT}$`, "i");
45
+ const SCORE_THRESHOLD = 2;
46
+
47
+ const TREE_NAME_TYPES = new Set([
48
+ "identifier",
49
+ "field_identifier",
50
+ "type_identifier",
51
+ "namespace_identifier",
52
+ ]);
53
+
54
+ const ROBLOX_SERVICES = new Set([
55
+ "Players",
56
+ "Workspace",
57
+ "Lighting",
58
+ "ReplicatedStorage",
59
+ "ReplicatedFirst",
60
+ "ServerStorage",
61
+ "ServerScriptService",
62
+ "StarterGui",
63
+ "StarterPack",
64
+ "StarterPlayer",
65
+ "SoundService",
66
+ "TweenService",
67
+ "RunService",
68
+ "UserInputService",
69
+ "ContextActionService",
70
+ "GuiService",
71
+ "TextChatService",
72
+ "TextService",
73
+ "CollectionService",
74
+ "PhysicsService",
75
+ "PathfindingService",
76
+ "DataStoreService",
77
+ "MemoryStoreService",
78
+ "MessagingService",
79
+ "TeleportService",
80
+ "HttpService",
81
+ "MarketplaceService",
82
+ "BadgeService",
83
+ "PolicyService",
84
+ "ProximityPromptService",
85
+ "HapticService",
86
+ "VRService",
87
+ "GamepadService",
88
+ "TouchInputService",
89
+ "VoiceChatService",
90
+ "AnimationClipProvider",
91
+ "AssetService",
92
+ "AvatarEditorService",
93
+ "Chat",
94
+ "Debris",
95
+ "InsertService",
96
+ "JointsService",
97
+ "KeyframeSequenceProvider",
98
+ "LocalizationService",
99
+ "LogService",
100
+ "MaterialService",
101
+ "NotificationService",
102
+ "PointsService",
103
+ "SocialService",
104
+ "Stats",
105
+ "Teams",
106
+ "TestService",
107
+ "TimerService",
108
+ ]);
109
+
110
+ /**
111
+ * Filename key first (roblox-ts ScriptType), then AST token density
112
+ * (Knit/Flamework Service vs Controller naming).
113
+ * @see https://github.com/roblox-ts/roblox-ts
114
+ * @see https://github.com/roblox-csharp/roblox-cs
115
+ * @see https://create.roblox.com/docs/reference/engine
116
+ */
117
+ export const INTENTS: Record<string, IntentSpec> = {
118
+ players: {
119
+ role: "manager",
120
+ module: "PlayersManager",
121
+ side: "server",
122
+ tokens: ["GetPlayers", "PlayerAdded", "PlayerRemoving", "Players"],
123
+ },
124
+ cache: {
125
+ role: "controller",
126
+ module: "CacheController",
127
+ side: "server",
128
+ tokens: ["Folder", "IntValue", "StringValue", "BoolValue", "NumberValue", "leaderstats"],
129
+ },
130
+ combat: {
131
+ role: "controller",
132
+ module: "CombatController",
133
+ side: "server",
134
+ tokens: [
135
+ "TakeDamage",
136
+ "Humanoid",
137
+ "Raycast",
138
+ "RaycastParams",
139
+ "Hitbox",
140
+ "Health",
141
+ "Damage",
142
+ "Weapon",
143
+ "Tool",
144
+ "Activated",
145
+ "Combat",
146
+ "Hurt",
147
+ "Attack",
148
+ ],
149
+ },
150
+ character: {
151
+ role: "controller",
152
+ module: "CharacterController",
153
+ side: "any",
154
+ tokens: ["CharacterAdded", "CharacterRemoving", "HumanoidRootPart", "GetPivot", "PivotTo", "Character"],
155
+ },
156
+ input: {
157
+ role: "controller",
158
+ module: "InputController",
159
+ side: "client",
160
+ tokens: [
161
+ "UserInputService",
162
+ "ContextActionService",
163
+ "InputBegan",
164
+ "InputEnded",
165
+ "InputChanged",
166
+ "Mouse",
167
+ "GetMouse",
168
+ "PreferredInput",
169
+ "GamepadService",
170
+ "TouchInputService",
171
+ ],
172
+ },
173
+ ui: {
174
+ role: "controller",
175
+ module: "ViewController",
176
+ side: "client",
177
+ tokens: [
178
+ "PlayerGui",
179
+ "ScreenGui",
180
+ "BillboardGui",
181
+ "SurfaceGui",
182
+ "TextLabel",
183
+ "TextButton",
184
+ "Frame",
185
+ "ImageLabel",
186
+ "TweenService",
187
+ "Twinkle",
188
+ "GuiService",
189
+ "StarterGui",
190
+ ],
191
+ },
192
+ net: {
193
+ role: "controller",
194
+ module: "NetController",
195
+ side: "any",
196
+ tokens: [
197
+ "RemoteEvent",
198
+ "RemoteFunction",
199
+ "UnreliableRemoteEvent",
200
+ "FireServer",
201
+ "FireAllClients",
202
+ "FireClient",
203
+ "OnServerEvent",
204
+ "OnClientEvent",
205
+ "Net",
206
+ "NetEvent",
207
+ ],
208
+ },
209
+ data: {
210
+ role: "service",
211
+ module: "DataController",
212
+ side: "server",
213
+ tokens: [
214
+ "DataStore",
215
+ "DataStoreService",
216
+ "DataService",
217
+ "SetAsync",
218
+ "GetAsync",
219
+ "UpdateAsync",
220
+ "ProfileStore",
221
+ "GetChangedSignal",
222
+ "WaitFor",
223
+ "MemoryStoreService",
224
+ "MessagingService",
225
+ ],
226
+ },
227
+ animation: {
228
+ role: "controller",
229
+ module: "AnimationController",
230
+ side: "any",
231
+ tokens: ["Animator", "Animation", "AnimationTrack", "LoadAnimation"],
232
+ },
233
+ inventory: {
234
+ role: "handler",
235
+ module: "InventoryHandler",
236
+ side: "any",
237
+ tokens: ["Backpack", "Inventory"],
238
+ },
239
+ marketplace: {
240
+ role: "service",
241
+ module: "MarketplaceService",
242
+ side: "server",
243
+ tokens: ["MarketplaceService", "PromptGamePassPurchase", "PromptProductPurchase", "ProcessReceipt"],
244
+ },
245
+ collection: {
246
+ role: "manager",
247
+ module: "CollectionManager",
248
+ side: "shared",
249
+ tokens: ["CollectionService", "GetTagged", "AddTag", "RemoveTag", "GetInstanceAddedSignal"],
250
+ },
251
+ proximity: {
252
+ role: "handler",
253
+ module: "ProximityHandler",
254
+ side: "any",
255
+ tokens: ["ProximityPrompt", "ProximityPromptService", "Triggered"],
256
+ },
257
+ camera: {
258
+ role: "controller",
259
+ module: "CameraController",
260
+ side: "client",
261
+ tokens: ["CurrentCamera", "Camera", "CameraType", "CameraSubject"],
262
+ },
263
+ run: {
264
+ role: "utility",
265
+ module: "HeartbeatUtil",
266
+ side: "any",
267
+ tokens: ["RunService", "Heartbeat", "RenderStepped", "Stepped", "PreSimulation"],
268
+ },
269
+ };
270
+
271
+ type SyntaxLike = {
272
+ type: string;
273
+ text?: string;
274
+ childCount?: number;
275
+ child?: (index: number) => unknown;
276
+ };
277
+
278
+ function isTreeNode(node: unknown): node is SyntaxLike {
279
+ return Boolean(
280
+ node &&
281
+ typeof node === "object" &&
282
+ "childCount" in node &&
283
+ typeof (node as SyntaxLike).child === "function",
284
+ );
285
+ }
286
+
287
+ export function walk(node: unknown, visit: (node: Record<string, unknown>) => void): void {
288
+ if (!node || typeof node !== "object") {
289
+ return;
290
+ }
291
+ visit(node as Record<string, unknown>);
292
+ if (isTreeNode(node) && typeof node.childCount === "number" && node.child) {
293
+ for (let index = 0; index < node.childCount; index += 1) {
294
+ const child = node.child(index);
295
+ if (child) {
296
+ walk(child, visit);
297
+ }
298
+ }
299
+ return;
300
+ }
301
+ for (const value of Object.values(node)) {
302
+ if (Array.isArray(value)) {
303
+ for (const item of value) {
304
+ walk(item, visit);
305
+ }
306
+ } else if (value && typeof value === "object") {
307
+ walk(value, visit);
308
+ }
309
+ }
310
+ }
311
+
312
+ function addName(found: Set<string>, value: unknown): void {
313
+ if (typeof value !== "string") {
314
+ return;
315
+ }
316
+ const trimmed = value.replace(/^["'<]+|["'>]+$/g, "").trim();
317
+ if (trimmed) {
318
+ found.add(trimmed);
319
+ }
320
+ }
321
+
322
+ export function namesIn(ast: unknown, options: { strings?: boolean } = {}): Set<string> {
323
+ const found = new Set<string>();
324
+ walk(ast, (node) => {
325
+ const type = String(node.type || "");
326
+ if ((type === "call" || type === "member" || type === "ident") && node.name) {
327
+ addName(found, node.name);
328
+ }
329
+ if (type === "new" && node.className) {
330
+ addName(found, node.className);
331
+ }
332
+ if (type === "getService" && node.service) {
333
+ addName(found, node.service);
334
+ }
335
+ if (options.strings !== false && type === "string" && node.value) {
336
+ addName(found, node.value);
337
+ }
338
+ if (TREE_NAME_TYPES.has(type) && typeof node.text === "string") {
339
+ addName(found, node.text);
340
+ }
341
+ if (options.strings !== false && type === "string_literal" && typeof node.text === "string") {
342
+ addName(found, node.text);
343
+ }
344
+ });
345
+ return found;
346
+ }
347
+
348
+ export function parseFileTag(fileName: string): FileTagResult {
349
+ const base = path.basename(fileName);
350
+ if (new RegExp(`\\.legacy\\.plugin\\.${EXT}$`, "i").test(base)) {
351
+ return { key: "legacy.plugin", emit: "legacy", runtime: "plugin", rojo: "Script", runContext: "Plugin" };
352
+ }
353
+ if (new RegExp(`\\.legacy\\.server\\.${EXT}$`, "i").test(base)) {
354
+ return { key: "legacy.server", emit: "legacy", runtime: "server", rojo: "Script", runContext: null };
355
+ }
356
+ if (new RegExp(`\\.legacy\\.client\\.${EXT}$`, "i").test(base)) {
357
+ return { key: "legacy.client", emit: "legacy", runtime: "client", rojo: "LocalScript", runContext: null };
358
+ }
359
+ if (new RegExp(`\\.legacy\\.${EXT}$`, "i").test(base)) {
360
+ return { key: "legacy", emit: "legacy", runtime: "server", rojo: "Script", runContext: null };
361
+ }
362
+ if (new RegExp(`\\.plugin\\.${EXT}$`, "i").test(base)) {
363
+ return { key: "plugin", emit: "service", runtime: "plugin", rojo: "Script", runContext: "Plugin" };
364
+ }
365
+ if (new RegExp(`\\.server\\.${EXT}$`, "i").test(base)) {
366
+ return { key: "server", emit: "service", runtime: "server", rojo: "Script", runContext: "Server" };
367
+ }
368
+ if (new RegExp(`\\.client\\.${EXT}$`, "i").test(base)) {
369
+ return { key: "client", emit: "service", runtime: "client", rojo: "LocalScript", runContext: "Client" };
370
+ }
371
+ return { key: "module", emit: "module", runtime: "shared", rojo: "ModuleScript", runContext: null };
372
+ }
373
+
374
+ function sideWeight(runtime: RuntimeSide, side: RuntimeSide | "any"): number {
375
+ if (side === "any" || side === "shared") {
376
+ return 1;
377
+ }
378
+ if (side === runtime) {
379
+ return 1.25;
380
+ }
381
+ if (runtime === "shared") {
382
+ return 0.6;
383
+ }
384
+ return 0.35;
385
+ }
386
+
387
+ export function scoreIntents(namedSet: Set<string>, runtime: RuntimeSide = "shared"): ScoredIntent[] {
388
+ const scored: ScoredIntent[] = [];
389
+ for (const [name, spec] of Object.entries(INTENTS)) {
390
+ const evidence = spec.tokens.filter((token) => namedSet.has(token));
391
+ if (evidence.length === 0) {
392
+ continue;
393
+ }
394
+ scored.push({
395
+ name,
396
+ score: evidence.length * sideWeight(runtime, spec.side),
397
+ evidence,
398
+ role: spec.role,
399
+ module: spec.module,
400
+ side: spec.side,
401
+ });
402
+ }
403
+ return scored.sort((a, b) => b.score - a.score || b.evidence.length - a.evidence.length);
404
+ }
405
+
406
+ function pascalUtilName(fileName: string): string {
407
+ let base = path.basename(fileName).replace(EXT_REGEX, "");
408
+ base = base.replace(/\.legacy\.(server|client|plugin)$/i, "");
409
+ base = base.replace(/\.(server|client|plugin)$/i, "");
410
+ if (!base) {
411
+ return "ModuleUtil";
412
+ }
413
+ const pascal = base
414
+ .split(/[-_]+/)
415
+ .filter(Boolean)
416
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
417
+ .join("");
418
+ return /Util$/.test(pascal) ? pascal : `${pascal}Util`;
419
+ }
420
+
421
+ function collectInjectedServices(named: Set<string>, evidence: string[]): string[] {
422
+ const found = new Set<string>();
423
+ for (const token of [...evidence, ...named]) {
424
+ if (ROBLOX_SERVICES.has(token)) {
425
+ found.add(token);
426
+ }
427
+ }
428
+ return [...found].sort();
429
+ }
430
+
431
+ const SUPPORTING_INTENTS = new Set(["players", "cache", "run"]);
432
+
433
+ function pickDominant(intents: ScoredIntent[]): ScoredIntent | undefined {
434
+ const top = intents[0];
435
+ if (!top) {
436
+ return undefined;
437
+ }
438
+ const domain = intents.find(
439
+ (item) => !SUPPORTING_INTENTS.has(item.name) && item.evidence.length >= SCORE_THRESHOLD,
440
+ );
441
+ if (domain && (SUPPORTING_INTENTS.has(top.name) || domain.score >= top.score * 0.8)) {
442
+ return domain;
443
+ }
444
+ return top;
445
+ }
446
+
447
+ function formatIntents(intents: ScoredIntent[]): string {
448
+ return intents.map((item) => `${item.name}:${item.evidence.length}`).join(", ");
449
+ }
450
+
451
+ export function determineArchitecture(fileName: string, ast?: unknown, tree?: unknown): ArchitectureReport {
452
+ const fileTag = parseFileTag(fileName);
453
+ const named = new Set<string>();
454
+ if (ast) {
455
+ for (const name of namesIn(ast)) {
456
+ named.add(name);
457
+ }
458
+ }
459
+ if (tree) {
460
+ for (const name of namesIn(tree)) {
461
+ named.add(name);
462
+ }
463
+ }
464
+
465
+ const intents = scoreIntents(named, fileTag.runtime);
466
+ const top = pickDominant(intents);
467
+ if (!top || top.score < SCORE_THRESHOLD) {
468
+ const suggestedModule = pascalUtilName(fileName);
469
+ return {
470
+ role: "utility",
471
+ suggestedModule,
472
+ fileTag,
473
+ injectedServices: collectInjectedServices(named, top ? top.evidence : []),
474
+ isUtility: true,
475
+ intents,
476
+ reasoning: [
477
+ `tag ${fileTag.key} → ${fileTag.rojo} (${fileTag.emit})`,
478
+ top ? `intents ${formatIntents(intents)} below threshold` : "intents none — utility module",
479
+ `role utility ${suggestedModule}`,
480
+ ],
481
+ };
482
+ }
483
+
484
+ return {
485
+ role: top.role,
486
+ suggestedModule: top.module,
487
+ fileTag,
488
+ injectedServices: collectInjectedServices(named, top.evidence),
489
+ isUtility: false,
490
+ intents,
491
+ reasoning: [
492
+ `tag ${fileTag.key} → ${fileTag.rojo} (${fileTag.emit})`,
493
+ `intents ${formatIntents(intents)}`,
494
+ `role ${top.role} ${top.module}`,
495
+ ],
496
+ };
497
+ }
498
+
499
+ export function understandSystem(fileName: string, ast?: unknown, tree?: unknown): ArchitectureReport {
500
+ return determineArchitecture(fileName, ast, tree);
501
+ }
@@ -0,0 +1,64 @@
1
+ import path from "node:path";
2
+ import type { CompileOptions, CompileServiceResult, TranspilerState } from "./types.js";
3
+ import { ASTCollector } from "./parser/collector.js";
4
+ import { parseCpp } from "./parser/index.js";
5
+ import { LuauCodeEmitter } from "./emitter/luau-codegen.js";
6
+ import { compileService } from "./compile.js";
7
+ import { determineArchitecture } from "./system-understander.js";
8
+ import type { RojoMapper } from "./utils/rojo-mapper.js";
9
+
10
+ function shouldMerge(name: string, contents: string): boolean {
11
+ const file = String(name || "").replace(/\\/g, "/");
12
+ if (/\.json$/i.test(file) || /\.meta\.json$/i.test(file)) {
13
+ return false;
14
+ }
15
+ const trimmed = String(contents || "").trim();
16
+ return !(trimmed.startsWith("{") || trimmed.startsWith("["));
17
+ }
18
+
19
+ export function collectState(source: string, mapper: RojoMapper, options: CompileOptions): TranspilerState {
20
+ const tree = parseCpp(source);
21
+ if (!tree) {
22
+ const state = LuauCodeEmitter.emptyState(options.strict === true);
23
+ return state;
24
+ }
25
+ const collector = new ASTCollector(mapper, {
26
+ fileName: options.relativeName || options.filePath || "input.cpp",
27
+ sourcePath: options.filePath,
28
+ srcDir: options.srcDir,
29
+ outDir: options.outDir,
30
+ strict: options.strict,
31
+ });
32
+ const state = collector.collect(tree.rootNode);
33
+ const report = determineArchitecture(options.relativeName || options.filePath || "input.cpp", null, tree.rootNode);
34
+ for (const service of report.injectedServices) {
35
+ state.robloxServices.add(service);
36
+ }
37
+ for (const line of report.reasoning) {
38
+ state.headerLines.push(`-- ${line}`);
39
+ }
40
+ return state;
41
+ }
42
+
43
+ export function transpileSource(source: string, fileName: string, options: CompileOptions, mapper: RojoMapper): CompileServiceResult {
44
+ const state = collectState(source, mapper, options);
45
+ if (state.strict) {
46
+ options.strict = true;
47
+ }
48
+ const compiled = compileService(source, fileName, options);
49
+ return {
50
+ ...compiled,
51
+ files: compiled.files.map((file) => ({
52
+ ...file,
53
+ contents: shouldMerge(file.name, file.contents) ? LuauCodeEmitter.merge(state, file.contents) : file.contents,
54
+ })),
55
+ };
56
+ }
57
+
58
+ export function outputNameFor(inputFile: string, outputPath: string): string {
59
+ if (/\.luau$/i.test(outputPath)) {
60
+ return outputPath;
61
+ }
62
+ const base = path.basename(inputFile).replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau");
63
+ return path.join(outputPath, base);
64
+ }
@@ -0,0 +1,4 @@
1
+ declare module "tree-sitter-cpp" {
2
+ const language: unknown;
3
+ export default language;
4
+ }
package/src/types.ts ADDED
@@ -0,0 +1,81 @@
1
+ export interface TranspilerState {
2
+ robloxServices: Set<string>;
3
+ moduleRequires: Map<string, string>;
4
+ customTypes: string[];
5
+ constants: string[];
6
+ functions: string[];
7
+ headerLines: string[];
8
+ strict: boolean;
9
+ }
10
+
11
+ export interface RojoConfig {
12
+ name?: string;
13
+ tree?: Record<string, unknown>;
14
+ }
15
+
16
+ export interface RojoPathBinding {
17
+ robloxPath: string;
18
+ fsPath: string;
19
+ }
20
+
21
+ export interface RequireBinding {
22
+ alias: string;
23
+ path: string;
24
+ }
25
+
26
+ export interface CompileOptions {
27
+ strict?: boolean;
28
+ architecture?: boolean;
29
+ rootDir?: string;
30
+ outDir?: string;
31
+ filePath?: string;
32
+ relativeName?: string;
33
+ outName?: string;
34
+ srcDir?: string;
35
+ includeDirs?: string[];
36
+ moduleIncludes?: unknown[];
37
+ skipInit?: boolean;
38
+ skipHeader?: boolean;
39
+ headerImplName?: string;
40
+ }
41
+
42
+ export interface ProjectConfig {
43
+ rootDir: string;
44
+ outDir: string;
45
+ strict: boolean;
46
+ architecture: boolean;
47
+ }
48
+
49
+ export interface BuildOptions {
50
+ exitOnError?: boolean;
51
+ holdOnError?: boolean;
52
+ syncVendor?: boolean;
53
+ format?: boolean;
54
+ analyze?: boolean;
55
+ rojo?: string;
56
+ }
57
+
58
+ export interface BuildResult {
59
+ failed: number;
60
+ written: Set<string>;
61
+ }
62
+
63
+ export interface CompiledArtifact {
64
+ name: string;
65
+ contents: string;
66
+ }
67
+
68
+ export interface CompileServiceResult {
69
+ kind?: string;
70
+ plan?: unknown;
71
+ files: CompiledArtifact[];
72
+ stale?: string[];
73
+ }
74
+
75
+ export interface CollectorContext {
76
+ fileName: string;
77
+ sourcePath?: string;
78
+ srcDir?: string;
79
+ outDir?: string;
80
+ strict?: boolean;
81
+ }