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
package/src/compile.ts ADDED
@@ -0,0 +1,118 @@
1
+ import path from "node:path";
2
+ import { parse } from "./parse.js";
3
+ import { emit } from "./emit.js";
4
+ import { preprocess, isHeaderFile, toLuauPath, siblingImplementation, siblingHeader, implOutName } from "./preprocess.js";
5
+ import { collectLibraries, insertPreamble } from "./libs.js";
6
+ import { planOutput } from "./architecture.js";
7
+ import { emitHeaderModule, primaryHeaderName } from "./headers.js";
8
+ import type { AstProgram, ModuleInclude, PreprocessOptions } from "./ast.js";
9
+ import type { CompileOptions, CompileServiceResult } from "./types.js";
10
+
11
+ function shouldAttachRequires(name: string, contents: string): boolean {
12
+ const file = String(name || "").replace(/\\/g, "/");
13
+ if (/\.json$/i.test(file) || /\.meta\.json$/i.test(file)) {
14
+ return false;
15
+ }
16
+ const trimmed = String(contents || "").trim();
17
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
18
+ return false;
19
+ }
20
+ return true;
21
+ }
22
+
23
+ function attachRequires(contents: string, source: string, ast: AstProgram, options: PreprocessOptions, outName: string): string {
24
+ if (!shouldAttachRequires(outName, contents)) {
25
+ return contents;
26
+ }
27
+ const libraries = String(contents).includes("CluauppLibs") ? [] : collectLibraries(source, ast);
28
+ return insertPreamble(contents, options.moduleIncludes || [], libraries, outName);
29
+ }
30
+
31
+ function posixRel(from: string, file: string): string {
32
+ return path.relative(from, file).replace(/\\/g, "/");
33
+ }
34
+
35
+ function wireHeaderImpl(ast: AstProgram, fileName: string, options: PreprocessOptions): void {
36
+ const headerPath = options.filePath || null;
37
+ const impl = headerPath ? siblingImplementation(headerPath) : null;
38
+ if (!impl) {
39
+ return;
40
+ }
41
+ const typeName = primaryHeaderName(ast, options.relativeName || fileName);
42
+ const implRel = options.srcDir ? posixRel(options.srcDir, impl) : path.basename(impl).replace(/\\/g, "/");
43
+ const implOut = implOutName(implRel);
44
+ options.headerImplName = `${typeName}Module`;
45
+ options.moduleIncludes = options.moduleIncludes || [];
46
+ if (!options.moduleIncludes.some((spec: ModuleInclude) => spec.name === options.headerImplName)) {
47
+ options.moduleIncludes.push({
48
+ name: options.headerImplName,
49
+ outRel: implOut,
50
+ exports: { consts: [], structs: [] },
51
+ });
52
+ }
53
+ }
54
+
55
+ function compileHeader(source: string, fileName: string, options: PreprocessOptions, ast: AstProgram): CompileServiceResult {
56
+ wireHeaderImpl(ast, fileName, options);
57
+ const rel = (options.relativeName || fileName || "header.h").replace(/\\/g, "/");
58
+ const outName = toLuauPath(rel);
59
+ const luau = attachRequires(emitHeaderModule(ast, options), source, ast, options, outName);
60
+ return {
61
+ kind: "module",
62
+ plan: { kind: "module", strict: options.strict === true },
63
+ files: [{ name: outName, contents: luau }],
64
+ stale: [],
65
+ };
66
+ }
67
+
68
+ export function compileSource(source: string, fileName?: string, options: CompileOptions = {}): string {
69
+ const opts = options as PreprocessOptions;
70
+ opts.moduleIncludes = opts.moduleIncludes || [];
71
+ const prepared = preprocess(source, opts.filePath || null, opts);
72
+ const ast = parse(prepared, fileName || "input.cpp");
73
+ if (isHeaderFile(fileName || "") || isHeaderFile(opts.filePath || "")) {
74
+ return compileHeader(source, fileName || "header.h", opts, ast).files[0].contents;
75
+ }
76
+ const luau = emit(ast, opts);
77
+ return attachRequires(luau, source, ast, opts, opts.outName || fileName || "input.cpp");
78
+ }
79
+
80
+ export function compileService(source: string, fileName?: string, options: CompileOptions = {}): CompileServiceResult {
81
+ const opts = options as PreprocessOptions;
82
+ opts.moduleIncludes = opts.moduleIncludes || [];
83
+ const prepared = preprocess(source, opts.filePath || null, opts);
84
+ const ast = parse(prepared, fileName || "input.cpp");
85
+ if (isHeaderFile(fileName || "") || isHeaderFile(opts.filePath || "") || isHeaderFile(opts.relativeName || "")) {
86
+ return compileHeader(source, fileName || "header.h", opts, ast);
87
+ }
88
+ const implHeader = opts.filePath ? siblingHeader(opts.filePath) : null;
89
+ if (implHeader) {
90
+ opts.skipInit = true;
91
+ }
92
+ const planned = planOutput(ast, fileName || "input.cpp", {
93
+ ...opts,
94
+ relativeName: opts.relativeName || fileName,
95
+ source: prepared,
96
+ siblingHeader: implHeader,
97
+ });
98
+ if (planned.files) {
99
+ return {
100
+ ...planned,
101
+ files: planned.files.map((file: { name: string; contents: string }) => ({
102
+ ...file,
103
+ contents: attachRequires(file.contents, source, ast, opts, file.name),
104
+ })),
105
+ };
106
+ }
107
+ const luau = attachRequires(emit(ast, opts), source, ast, opts, planned.outName || opts.outName || fileName || "input.cpp");
108
+ const name =
109
+ planned.outName ||
110
+ opts.outName ||
111
+ String(fileName || "input.cpp").replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau");
112
+ return {
113
+ kind: planned.kind || "flat",
114
+ plan: planned.plan,
115
+ files: [{ name, contents: luau }],
116
+ stale: planned.stale || [],
117
+ };
118
+ }
@@ -0,0 +1,182 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ export const CLANGD_ID = "llvm-vs-code-extensions.vscode-clangd";
7
+
8
+ export type InstallResult = {
9
+ status: string;
10
+ id?: string;
11
+ path?: string;
12
+ how?: string;
13
+ detail?: string;
14
+ hint?: string;
15
+ };
16
+
17
+ function which(command: string): string | null {
18
+ const finder = process.platform === "win32" ? "where" : "which";
19
+ const result = spawnSync(finder, [command], { encoding: "utf8", windowsHide: true });
20
+ if (result.status !== 0) {
21
+ return null;
22
+ }
23
+ return String(result.stdout || "")
24
+ .split(/\r?\n/)
25
+ .map((line) => line.trim())
26
+ .find((line) => line && !line.toLowerCase().includes("info:")) || null;
27
+ }
28
+
29
+ export function editorBin(): string | null {
30
+ const found = which("cursor") || which("code");
31
+ if (found) {
32
+ return found;
33
+ }
34
+ const local = process.env.LOCALAPPDATA || "";
35
+ const fallbacks = [
36
+ path.join(local, "Programs", "cursor", "resources", "app", "bin", "cursor.cmd"),
37
+ path.join(local, "Programs", "Microsoft VS Code", "bin", "code.cmd"),
38
+ "/usr/bin/cursor",
39
+ "/usr/local/bin/cursor",
40
+ "/usr/bin/code",
41
+ "/usr/local/bin/code",
42
+ ];
43
+ return fallbacks.find((file) => fs.existsSync(file)) || null;
44
+ }
45
+
46
+ function extensionHomes(): string[] {
47
+ return [path.join(os.homedir(), ".cursor", "extensions"), path.join(os.homedir(), ".vscode", "extensions")];
48
+ }
49
+
50
+ export function isExtensionInstalled(id: string): boolean {
51
+ for (const home of extensionHomes()) {
52
+ if (!fs.existsSync(home)) {
53
+ continue;
54
+ }
55
+ try {
56
+ if (fs.readdirSync(home).some((name) => name.startsWith(`${id}-`))) {
57
+ return true;
58
+ }
59
+ } catch {
60
+ // ignore
61
+ }
62
+ }
63
+ const bin = editorBin();
64
+ if (!bin) {
65
+ return false;
66
+ }
67
+ const listed = spawnSync(bin, ["--list-extensions"], { encoding: "utf8", windowsHide: true, timeout: 20000 });
68
+ return String(listed.stdout || "").split(/\r?\n/).includes(id);
69
+ }
70
+
71
+ function llvmCandidates(): string[] {
72
+ if (process.platform === "win32") {
73
+ return [
74
+ "C:/Program Files/LLVM/bin/clang++.exe",
75
+ "C:/Program Files (x86)/LLVM/bin/clang++.exe",
76
+ path.join(process.env.ProgramFiles || "C:/Program Files", "LLVM", "bin", "clang++.exe"),
77
+ "C:/msys64/ucrt64/bin/clang++.exe",
78
+ "C:/msys64/mingw64/bin/clang++.exe",
79
+ ];
80
+ }
81
+ return ["/usr/bin/clang++", "/usr/local/bin/clang++", "/opt/homebrew/bin/clang++"];
82
+ }
83
+
84
+ export function findClangPlusPlus(): string | null {
85
+ for (const candidate of llvmCandidates()) {
86
+ if (candidate && fs.existsSync(candidate)) {
87
+ return candidate.replace(/\\/g, "/");
88
+ }
89
+ }
90
+ return which("clang++");
91
+ }
92
+
93
+ export function ensureLlvm(): InstallResult {
94
+ const found = findClangPlusPlus();
95
+ if (found) {
96
+ return { status: "already", path: found };
97
+ }
98
+ if (process.platform === "win32" && which("winget")) {
99
+ console.log("cluaupp: installing LLVM.LLVM (clang++ / clangd) via winget");
100
+ const result = spawnSync(
101
+ "winget",
102
+ ["install", "--id", "LLVM.LLVM", "-e", "--accept-package-agreements", "--accept-source-agreements"],
103
+ { encoding: "utf8", windowsHide: true, timeout: 300000 },
104
+ );
105
+ const after = findClangPlusPlus();
106
+ if (after) {
107
+ return { status: "installed", path: after, how: "winget" };
108
+ }
109
+ return {
110
+ status: "missing",
111
+ detail: String(result.stderr || result.stdout || "").trim() || "winget did not install LLVM",
112
+ hint: "winget install --id LLVM.LLVM -e",
113
+ };
114
+ }
115
+ const hint =
116
+ process.platform === "darwin"
117
+ ? "brew install llvm"
118
+ : process.platform === "win32"
119
+ ? "winget install --id LLVM.LLVM -e"
120
+ : "sudo apt install clang";
121
+ return { status: "missing", hint };
122
+ }
123
+
124
+ function spawnInstall(bin: string, target: string) {
125
+ const result = spawnSync(bin, ["--install-extension", target, "--force"], {
126
+ encoding: "utf8",
127
+ windowsHide: true,
128
+ timeout: 180000,
129
+ });
130
+ return {
131
+ ok: result.status === 0 && !/not found/i.test(String(result.stderr || "") + String(result.stdout || "")),
132
+ stdout: String(result.stdout || ""),
133
+ stderr: String(result.stderr || ""),
134
+ status: result.status,
135
+ };
136
+ }
137
+
138
+ export async function installMarketplaceExtension(id: string, label?: string): Promise<InstallResult> {
139
+ if (isExtensionInstalled(id)) {
140
+ return { status: "already", id };
141
+ }
142
+ const bin = editorBin();
143
+ if (!bin) {
144
+ return { status: "missing-editor", id };
145
+ }
146
+ console.log("cluaupp: installing", label || id);
147
+ const marketplace = spawnInstall(bin, id);
148
+ if (marketplace.ok) {
149
+ return { status: "installed", id, how: "marketplace" };
150
+ }
151
+ return { status: "failed", id, detail: marketplace.stderr || marketplace.stdout };
152
+ }
153
+
154
+ export function reportInstall(result: InstallResult | null | undefined, label?: string, extraMissing?: string): void {
155
+ if (!result) {
156
+ return;
157
+ }
158
+ const name = label || result.id;
159
+ if (result.status === "already") {
160
+ console.log("cluaupp:", name, "already installed");
161
+ return;
162
+ }
163
+ if (result.status === "installed") {
164
+ console.log("cluaupp: installed", name, result.how ? `(${result.how})` : "");
165
+ return;
166
+ }
167
+ if (result.status === "missing-editor") {
168
+ console.error("cluaupp: Cursor/VS Code CLI not found. Install", name, "from the marketplace.");
169
+ return;
170
+ }
171
+ if (result.status === "missing") {
172
+ console.error("cluaupp: LLVM clang++ not found.", result.hint || extraMissing || "");
173
+ if (result.detail) {
174
+ console.error(" ", result.detail.split(/\r?\n/)[0]);
175
+ }
176
+ return;
177
+ }
178
+ console.error("cluaupp: could not install", name + ":", result.detail || "unknown error");
179
+ if (extraMissing) {
180
+ console.error(" ", extraMissing);
181
+ }
182
+ }
@@ -1,10 +1,9 @@
1
- "use strict";
2
-
3
- const { isInstanceType, isMethod, isDatatype, luauType } = require("./api");
4
- const { isLibraryType, isLibraryMethod, MODULE_COLON } = require("./libs");
1
+ // @ts-nocheck
2
+ import { isInstanceType, isMethod, isDatatype, luauType } from "./api.js";
3
+ import { isLibraryType, isLibraryMethod, MODULE_COLON } from "./libs.js";
5
4
 
6
5
  function emit(ast, options = {}) {
7
- const strict = options.strict !== false;
6
+ const strict = options.strict === true;
8
7
  const lines = [];
9
8
  if (!options.skipHeader) {
10
9
  if (strict) {
@@ -529,4 +528,4 @@ function emit(ast, options = {}) {
529
528
  return lines.join("\n");
530
529
  }
531
530
 
532
- module.exports = { emit };
531
+ export { emit };
@@ -0,0 +1,187 @@
1
+ import type { TranspilerState } from "../types.js";
2
+
3
+ const GET_SERVICE = /^const\s+([A-Za-z_][\w]*)\s*=\s*game:GetService\("([A-Za-z_][\w]*)"\)\s*$/;
4
+ const REQUIRE_LINE = /^const\s+([A-Za-z_][\w]*)\s*=\s*require\((.+)\)\s*$/;
5
+
6
+ export class LuauCodeEmitter {
7
+ public static emptyState(strict = false): TranspilerState {
8
+ return {
9
+ robloxServices: new Set<string>(),
10
+ moduleRequires: new Map<string, string>(),
11
+ customTypes: [],
12
+ constants: [],
13
+ functions: [],
14
+ headerLines: [],
15
+ strict,
16
+ };
17
+ }
18
+
19
+ public static emit(state: TranspilerState): string {
20
+ const output: string[] = [];
21
+ if (state.strict) {
22
+ output.push("--!strict");
23
+ }
24
+ if (state.headerLines.length > 0) {
25
+ output.push(...state.headerLines);
26
+ } else {
27
+ output.push("-- Compiled by Cluaupp — C++ × Luau");
28
+ }
29
+ output.push("");
30
+
31
+ if (state.robloxServices.size > 0) {
32
+ for (const service of Array.from(state.robloxServices).sort()) {
33
+ output.push(`const ${service} = game:GetService("${service}")`);
34
+ }
35
+ output.push("");
36
+ }
37
+
38
+ if (state.moduleRequires.size > 0) {
39
+ for (const [alias, requirePath] of Array.from(state.moduleRequires.entries()).sort(([a], [b]) => a.localeCompare(b))) {
40
+ output.push(`const ${alias} = require(${requirePath})`);
41
+ }
42
+ output.push("");
43
+ }
44
+
45
+ if (state.customTypes.length > 0) {
46
+ output.push(...state.customTypes);
47
+ output.push("");
48
+ }
49
+
50
+ if (state.constants.length > 0) {
51
+ output.push(...state.constants);
52
+ output.push("");
53
+ }
54
+
55
+ if (state.functions.length > 0) {
56
+ output.push(...state.functions);
57
+ } else {
58
+ output.push("-- Nenhum método ou lógica detectado para transcompilação.");
59
+ }
60
+
61
+ return output.join("\n").replace(/\n{3,}/g, "\n\n") + "\n";
62
+ }
63
+
64
+ public static merge(state: TranspilerState, compiledLuau: string): string {
65
+ const trimmed = String(compiledLuau || "").trim();
66
+ if (!trimmed || trimmed.startsWith("{") || trimmed.startsWith("[")) {
67
+ return compiledLuau;
68
+ }
69
+
70
+ const harvested = harvestPreamble(compiledLuau);
71
+ for (const service of harvested.services) {
72
+ state.robloxServices.add(service);
73
+ }
74
+ for (const [alias, requirePath] of harvested.requires) {
75
+ if (!state.moduleRequires.has(alias)) {
76
+ state.moduleRequires.set(alias, requirePath);
77
+ }
78
+ }
79
+
80
+ const output: string[] = [];
81
+ output.push(...(harvested.header.length > 0 ? harvested.header : ["-- Compiled by Cluaupp — C++ × Luau"]));
82
+ for (const line of state.headerLines) {
83
+ if (line && !output.includes(line)) {
84
+ output.push(line);
85
+ }
86
+ }
87
+ if (state.strict && !output.some((line) => line.startsWith("--!strict"))) {
88
+ output.unshift("--!strict");
89
+ }
90
+ output.push("");
91
+
92
+ if (state.robloxServices.size > 0) {
93
+ for (const service of Array.from(state.robloxServices).sort()) {
94
+ output.push(`const ${service} = game:GetService("${service}")`);
95
+ }
96
+ output.push("");
97
+ }
98
+
99
+ if (state.moduleRequires.size > 0) {
100
+ for (const [alias, requirePath] of Array.from(state.moduleRequires.entries()).sort(([a], [b]) => a.localeCompare(b))) {
101
+ output.push(`const ${alias} = require(${requirePath})`);
102
+ }
103
+ output.push("");
104
+ }
105
+
106
+ const rest = harvested.body.trim();
107
+ const extraTypes = state.customTypes.filter((line) => !bodyHasDecl(rest, line, /^type\s+(\w+)/));
108
+ const extraConsts = state.constants.filter((line) => !bodyHasDecl(rest, line, /^const\s+(\w+)/));
109
+ if (extraTypes.length > 0) {
110
+ output.push(...extraTypes, "");
111
+ }
112
+ if (extraConsts.length > 0) {
113
+ output.push(...extraConsts, "");
114
+ }
115
+ if (rest) {
116
+ output.push(rest);
117
+ } else if (state.customTypes.length || state.constants.length || state.functions.length) {
118
+ if (state.customTypes.length) {
119
+ output.push(...state.customTypes, "");
120
+ }
121
+ if (state.constants.length) {
122
+ output.push(...state.constants, "");
123
+ }
124
+ if (state.functions.length) {
125
+ output.push(...state.functions);
126
+ }
127
+ }
128
+
129
+ return `${output.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
130
+ }
131
+ }
132
+
133
+ function harvestPreamble(luau: string): {
134
+ header: string[];
135
+ services: Set<string>;
136
+ requires: Map<string, string>;
137
+ body: string;
138
+ } {
139
+ const lines = luau.split(/\r?\n/);
140
+ const header: string[] = [];
141
+ const services = new Set<string>();
142
+ const requires = new Map<string, string>();
143
+ let i = 0;
144
+
145
+ while (i < lines.length && (lines[i].startsWith("--") || lines[i].trim() === "")) {
146
+ if (lines[i].startsWith("--")) {
147
+ header.push(lines[i]);
148
+ }
149
+ i += 1;
150
+ }
151
+
152
+ while (i < lines.length) {
153
+ const line = lines[i];
154
+ if (line.trim() === "") {
155
+ i += 1;
156
+ continue;
157
+ }
158
+ const service = line.match(GET_SERVICE);
159
+ if (service && service[1] === service[2]) {
160
+ services.add(service[1]);
161
+ i += 1;
162
+ continue;
163
+ }
164
+ const required = line.match(REQUIRE_LINE);
165
+ if (required) {
166
+ requires.set(required[1], required[2]);
167
+ i += 1;
168
+ continue;
169
+ }
170
+ break;
171
+ }
172
+
173
+ return {
174
+ header,
175
+ services,
176
+ requires,
177
+ body: lines.slice(i).join("\n"),
178
+ };
179
+ }
180
+
181
+ function bodyHasDecl(body: string, line: string, pattern: RegExp): boolean {
182
+ const match = line.match(pattern);
183
+ if (!match) {
184
+ return body.includes(line);
185
+ }
186
+ return new RegExp(`^(?:export\\s+)?(?:type|const)\\s+${match[1]}\\b`, "m").test(body);
187
+ }
@@ -0,0 +1,168 @@
1
+ import type Parser from "tree-sitter";
2
+ import { compileSource } from "../compile.js";
3
+
4
+ type SyntaxNode = Parser.SyntaxNode;
5
+
6
+ const TYPE_MAP: Record<string, string> = {
7
+ "std::string": "string",
8
+ "std::string_view": "string",
9
+ string: "string",
10
+ string_view: "string",
11
+ bool: "boolean",
12
+ int: "number",
13
+ int8_t: "number",
14
+ int16_t: "number",
15
+ int32_t: "number",
16
+ int64_t: "number",
17
+ uint8_t: "number",
18
+ uint16_t: "number",
19
+ uint32_t: "number",
20
+ uint64_t: "number",
21
+ float: "number",
22
+ double: "number",
23
+ size_t: "number",
24
+ void: "()",
25
+ auto: "any",
26
+ };
27
+
28
+ export function translateCppType(cppType: string): string {
29
+ const trimmed = cppType
30
+ .replace(/\b(?:const|constexpr|volatile|static|inline|restrict)\b/g, "")
31
+ .replace(/\s+/g, " ")
32
+ .replace(/&/g, "")
33
+ .replace(/\*/g, "")
34
+ .trim();
35
+ if (!trimmed) {
36
+ return "any";
37
+ }
38
+ if (TYPE_MAP[trimmed]) {
39
+ return TYPE_MAP[trimmed];
40
+ }
41
+ const generic = trimmed.match(/^(?:std::)?(?:vector|array|span|LuaArray)\s*<\s*([^>]+)\s*>$/);
42
+ if (generic) {
43
+ return `{${translateCppType(generic[1])}}`;
44
+ }
45
+ const optional = trimmed.match(/^(?:std::)?optional\s*<\s*([^>]+)\s*>$/);
46
+ if (optional) {
47
+ return `${translateCppType(optional[1])}?`;
48
+ }
49
+ return trimmed.replace(/^::/, "");
50
+ }
51
+
52
+ export function translateGetService(node: SyntaxNode): string | null {
53
+ const functionNode = node.childForFieldName("function") || node.namedChildren[0];
54
+ if (!functionNode) {
55
+ return null;
56
+ }
57
+ const text = functionNode.text.replace(/\s+/g, "");
58
+ const generic = text.match(/GetService<(\w+)>/);
59
+ if (generic) {
60
+ return generic[1];
61
+ }
62
+ if (!text.includes("GetService")) {
63
+ return null;
64
+ }
65
+ const args = functionNode.namedChildren.find((child) => child.type === "template_argument_list");
66
+ if (args) {
67
+ const ident = args.namedChildren.find((child) => child.type === "type_identifier" || child.type === "identifier");
68
+ if (ident) {
69
+ return ident.text;
70
+ }
71
+ }
72
+ const callText = node.text.replace(/\s+/g, "");
73
+ const match = callText.match(/GetService<(\w+)>/);
74
+ return match ? match[1] : null;
75
+ }
76
+
77
+ export function translateConstant(node: SyntaxNode): string | null {
78
+ const declarator = node.childForFieldName("declarator");
79
+ if (!declarator) {
80
+ return null;
81
+ }
82
+ const name = identifierOf(declarator);
83
+ const valueNode =
84
+ node.childForFieldName("value") ||
85
+ declarator.childForFieldName("value") ||
86
+ namedOf(declarator, "initializer") ||
87
+ node.namedChildren[node.namedChildren.length - 1];
88
+ if (!name || !valueNode || valueNode === declarator) {
89
+ return null;
90
+ }
91
+ let value = valueNode.text.replace(/;$/, "").trim();
92
+ if (value.startsWith("=")) {
93
+ value = value.slice(1).trim();
94
+ }
95
+ if (!value) {
96
+ return null;
97
+ }
98
+ return `const ${name} = ${value}`;
99
+ }
100
+
101
+ export function translateAlias(node: SyntaxNode, mapType: (cppType: string) => string): string | null {
102
+ const nameNode = node.childForFieldName("name") || namedOf(node, "type_identifier") || node.namedChildren[1];
103
+ const typeNode = node.childForFieldName("type") || namedOf(node, "type_descriptor") || node.namedChildren[0];
104
+ if (!nameNode || !typeNode) {
105
+ return null;
106
+ }
107
+ return `type ${nameNode.text} = ${mapType(typeNode.text)}`;
108
+ }
109
+
110
+ export function translateStruct(node: SyntaxNode, mapType: (cppType: string) => string): string | null {
111
+ const nameNode = node.childForFieldName("name") || namedOf(node, "type_identifier");
112
+ const body = node.childForFieldName("body") || namedOf(node, "field_declaration_list");
113
+ if (!nameNode) {
114
+ return null;
115
+ }
116
+ if (!body) {
117
+ return `type ${nameNode.text} = {}`;
118
+ }
119
+ const fields: string[] = [];
120
+ for (const field of body.namedChildren) {
121
+ if (field.type !== "field_declaration") {
122
+ continue;
123
+ }
124
+ const fieldType = field.childForFieldName("type");
125
+ const fieldDecl = field.childForFieldName("declarator");
126
+ const fieldName = fieldDecl ? identifierOf(fieldDecl) : namedOf(field, "field_identifier")?.text;
127
+ if (!fieldName) {
128
+ continue;
129
+ }
130
+ fields.push(`\t${fieldName}: ${mapType(fieldType?.text || "any")},`);
131
+ }
132
+ if (fields.length === 0) {
133
+ return `type ${nameNode.text} = {}`;
134
+ }
135
+ return `type ${nameNode.text} = {\n${fields.join("\n")}\n}`;
136
+ }
137
+
138
+ export function translateFunction(node: SyntaxNode, fileName: string): string | null {
139
+ try {
140
+ const luau = compileSource(node.text, fileName, { skipHeader: true });
141
+ const trimmed = String(luau || "").trim();
142
+ return trimmed || null;
143
+ } catch {
144
+ const name = identifierOf(node.childForFieldName("declarator") || node) || "fn";
145
+ return `-- function ${name} (não traduzida)`;
146
+ }
147
+ }
148
+
149
+ function identifierOf(node: SyntaxNode): string | null {
150
+ if (node.type === "identifier" || node.type === "field_identifier" || node.type === "type_identifier") {
151
+ return node.text;
152
+ }
153
+ const direct = node.childForFieldName("declarator") || namedOf(node, "identifier") || namedOf(node, "field_identifier");
154
+ if (direct) {
155
+ return identifierOf(direct);
156
+ }
157
+ for (const child of node.namedChildren) {
158
+ const found = identifierOf(child);
159
+ if (found) {
160
+ return found;
161
+ }
162
+ }
163
+ return null;
164
+ }
165
+
166
+ function namedOf(node: SyntaxNode, type: string): SyntaxNode | null {
167
+ return node.namedChildren.find((child) => child.type === type) || null;
168
+ }