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.
- package/CHANGELOG.md +21 -0
- package/README.md +12 -7
- package/bin/cluau.js +1 -1
- package/bin/cluaupp.js +1 -1
- package/docs/README.md +1 -1
- package/docs/architecture.md +52 -77
- package/docs/cli.md +29 -9
- package/docs/comparison.md +3 -3
- package/docs/config.md +6 -6
- package/docs/cpp-organization.md +2 -2
- package/docs/examples/index.md +2 -0
- package/docs/getting-started.md +7 -5
- package/docs/intellisense.md +49 -31
- package/docs/intro.md +2 -2
- package/docs/oop/file-tags.md +16 -17
- package/docs/oop/index.md +2 -2
- package/docs/oop/modules.md +3 -1
- package/docs/oop/services.md +7 -14
- package/docs/syntax.md +6 -5
- package/editors/vscode/extension.js +97 -146
- package/editors/vscode/package.json +6 -6
- package/examples/README.md +18 -0
- package/examples/game/.clangd +25 -0
- package/examples/game/.vscode/c_cpp_properties.json +27 -0
- package/examples/game/.vscode/extensions.json +5 -0
- package/examples/game/.vscode/settings.json +27 -0
- package/examples/game/Packages/.gitkeep +0 -0
- package/examples/game/cluaupp.config.json +6 -0
- package/examples/game/compile_flags.txt +6 -0
- package/examples/game/default.project.json +39 -0
- package/examples/game/package.json +9 -0
- package/examples/game/rokit.toml +5 -0
- package/examples/game/src/client/hud.client.cpp +12 -0
- package/{src → examples/game/src}/client/init.client.cpp +1 -0
- package/examples/game/src/server/combat.server.cpp +23 -0
- package/{src → examples/game/src}/server/leaderstats.server.cpp +1 -0
- package/examples/game/wally.toml +11 -0
- package/generated/api.js +51 -0
- package/generated/architecture.js +473 -0
- package/generated/ast.js +2 -0
- package/generated/cli.js +191 -0
- package/generated/compile.js +115 -0
- package/generated/editor-install.js +170 -0
- package/generated/emit.js +503 -0
- package/generated/emitter/luau-codegen.js +167 -0
- package/generated/emitter/translators.js +164 -0
- package/generated/headers.js +220 -0
- package/generated/index.html +49 -0
- package/generated/intellisense.js +277 -0
- package/generated/layout.js +112 -0
- package/generated/lex.js +146 -0
- package/generated/libs.js +484 -0
- package/generated/lsp.js +55 -0
- package/generated/package-info.js +11 -0
- package/generated/parse.js +607 -0
- package/generated/parser/collector.js +100 -0
- package/generated/parser/index.js +30 -0
- package/generated/preprocess.js +181 -0
- package/generated/system-understander.js +443 -0
- package/generated/transpile.js +66 -0
- package/generated/types.js +2 -0
- package/generated/understand.js +298 -0
- package/generated/utils/process-orchestrator.js +63 -0
- package/generated/utils/project.js +491 -0
- package/generated/utils/rojo-mapper.js +181 -0
- package/generated/utils/safe-paths.js +104 -0
- package/package.json +25 -8
- package/src/api.ts +53 -0
- package/src/{architecture.js → architecture.ts} +80 -33
- package/src/ast.ts +37 -0
- package/src/cli.ts +214 -0
- package/src/compile.ts +118 -0
- package/src/editor-install.ts +182 -0
- package/src/{emit.js → emit.ts} +5 -6
- package/src/emitter/luau-codegen.ts +187 -0
- package/src/emitter/translators.ts +168 -0
- package/src/headers.ts +233 -0
- package/src/intellisense.ts +286 -0
- package/src/{layout.js → layout.ts} +114 -129
- package/src/{lex.js → lex.ts} +4 -6
- package/src/{libs.js → libs.ts} +119 -46
- package/src/lsp.ts +60 -0
- package/src/package-info.ts +7 -0
- package/src/{parse.js → parse.ts} +3 -4
- package/src/parser/collector.ts +115 -0
- package/src/parser/index.ts +27 -0
- package/src/{preprocess.js → preprocess.ts} +64 -32
- package/src/system-understander.ts +501 -0
- package/src/transpile.ts +64 -0
- package/src/tree-sitter-cpp.d.ts +4 -0
- package/src/types.ts +81 -0
- package/src/{understand.js → understand.ts} +24 -213
- package/src/utils/process-orchestrator.ts +72 -0
- package/src/utils/project.ts +506 -0
- package/src/utils/rojo-mapper.ts +190 -0
- package/src/utils/safe-paths.ts +98 -0
- package/templates/game/.clangd +9 -0
- package/templates/game/.vscode/c_cpp_properties.json +1 -1
- package/templates/game/.vscode/extensions.json +5 -6
- package/templates/game/.vscode/settings.json +7 -6
- package/templates/game/cluaupp.config.json +2 -2
- package/templates/game/compile_flags.txt +1 -0
- package/templates/game/rokit.toml +5 -0
- package/templates/game/src/client/init.client.cpp +1 -0
- package/templates/game/src/server/leaderstats.server.cpp +1 -0
- package/src/api.js +0 -57
- package/src/cli.js +0 -481
- package/src/compile.js +0 -56
- package/src/editor-install.js +0 -218
- package/src/intellisense.js +0 -1368
- package/src/lsp.js +0 -227
- /package/{src → examples/game/src}/shared/config.cpp +0 -0
- /package/{src → examples/game/src}/shared/config.h +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PathEscapeError = void 0;
|
|
7
|
+
exports.posixRel = posixRel;
|
|
8
|
+
exports.isInside = isInside;
|
|
9
|
+
exports.realPath = realPath;
|
|
10
|
+
exports.assertInside = assertInside;
|
|
11
|
+
exports.safeRelPath = safeRelPath;
|
|
12
|
+
exports.safeProjectSubdir = safeProjectSubdir;
|
|
13
|
+
exports.assertInitDest = assertInitDest;
|
|
14
|
+
exports.childEnv = childEnv;
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
17
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
18
|
+
const BLOCKED_DIRS = new Set([".", "..", "node_modules", ".git"]);
|
|
19
|
+
class PathEscapeError extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "PathEscapeError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.PathEscapeError = PathEscapeError;
|
|
26
|
+
function posixRel(rel) {
|
|
27
|
+
return String(rel || "").replace(/\\/g, "/");
|
|
28
|
+
}
|
|
29
|
+
function isInside(root, target) {
|
|
30
|
+
const absRoot = node_path_1.default.resolve(root);
|
|
31
|
+
const absTarget = node_path_1.default.resolve(target);
|
|
32
|
+
const rel = node_path_1.default.relative(absRoot, absTarget);
|
|
33
|
+
return rel === "" || (!rel.startsWith("..") && !node_path_1.default.isAbsolute(rel));
|
|
34
|
+
}
|
|
35
|
+
function realPath(target) {
|
|
36
|
+
try {
|
|
37
|
+
return node_fs_1.default.realpathSync(target);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return node_path_1.default.resolve(target);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function assertInside(root, target, label) {
|
|
44
|
+
const absRoot = realPath(root);
|
|
45
|
+
const absTarget = node_path_1.default.resolve(target);
|
|
46
|
+
if (!isInside(absRoot, absTarget)) {
|
|
47
|
+
throw new PathEscapeError(`cluaupp: ${label} escapes the project folder`);
|
|
48
|
+
}
|
|
49
|
+
return absTarget;
|
|
50
|
+
}
|
|
51
|
+
function safeRelPath(rel) {
|
|
52
|
+
const normalized = posixRel(rel).replace(/^\.\/+/, "");
|
|
53
|
+
if (!normalized || node_path_1.default.isAbsolute(normalized) || /^[a-zA-Z]:/.test(normalized)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const parts = normalized.split("/").filter((part) => part && part !== ".");
|
|
57
|
+
if (parts.length === 0 || parts.some((part) => part === ".." || part === ".git")) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return parts.join("/");
|
|
61
|
+
}
|
|
62
|
+
function safeProjectSubdir(value, fallback, label) {
|
|
63
|
+
const cleaned = safeRelPath(value);
|
|
64
|
+
if (!cleaned || BLOCKED_DIRS.has(cleaned.split("/")[0] || "")) {
|
|
65
|
+
throw new PathEscapeError(`cluaupp: invalid ${label} ${JSON.stringify(value)}`);
|
|
66
|
+
}
|
|
67
|
+
return cleaned;
|
|
68
|
+
}
|
|
69
|
+
function assertInitDest(dest) {
|
|
70
|
+
const resolved = node_path_1.default.resolve(dest);
|
|
71
|
+
const root = node_path_1.default.parse(resolved).root;
|
|
72
|
+
if (resolved === root) {
|
|
73
|
+
throw new PathEscapeError("cluaupp: refused to init at the filesystem root");
|
|
74
|
+
}
|
|
75
|
+
const home = node_os_1.default.homedir();
|
|
76
|
+
if (home && node_path_1.default.resolve(home) === resolved) {
|
|
77
|
+
throw new PathEscapeError("cluaupp: refused to init in the home directory itself");
|
|
78
|
+
}
|
|
79
|
+
return resolved;
|
|
80
|
+
}
|
|
81
|
+
function childEnv() {
|
|
82
|
+
const env = {};
|
|
83
|
+
for (const key of [
|
|
84
|
+
"PATH",
|
|
85
|
+
"Path",
|
|
86
|
+
"PATHEXT",
|
|
87
|
+
"SYSTEMROOT",
|
|
88
|
+
"SystemRoot",
|
|
89
|
+
"COMSPEC",
|
|
90
|
+
"ComSpec",
|
|
91
|
+
"HOME",
|
|
92
|
+
"USERPROFILE",
|
|
93
|
+
"TMP",
|
|
94
|
+
"TEMP",
|
|
95
|
+
"TMPDIR",
|
|
96
|
+
"LANG",
|
|
97
|
+
"LC_ALL",
|
|
98
|
+
]) {
|
|
99
|
+
if (process.env[key]) {
|
|
100
|
+
env[key] = process.env[key];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return env;
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cluaupp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Cluaupp — the definitive merge of C++ and modern Luau. Source-to-source transpiler with first-class Roblox APIs.",
|
|
5
5
|
"author": "KartzDev",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,16 +8,18 @@
|
|
|
8
8
|
"cluaupp": "bin/cluaupp.js",
|
|
9
9
|
"cluau": "bin/cluaupp.js"
|
|
10
10
|
},
|
|
11
|
-
"main": "
|
|
11
|
+
"main": "./generated/compile.js",
|
|
12
12
|
"exports": {
|
|
13
|
-
".": "./
|
|
13
|
+
".": "./generated/compile.js",
|
|
14
14
|
"./package.json": "./package.json"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"bin",
|
|
18
18
|
"src",
|
|
19
|
+
"generated",
|
|
19
20
|
"include",
|
|
20
21
|
"templates",
|
|
22
|
+
"examples",
|
|
21
23
|
"editors",
|
|
22
24
|
"runtime",
|
|
23
25
|
"README.md",
|
|
@@ -28,9 +30,11 @@
|
|
|
28
30
|
"scripts": {
|
|
29
31
|
"cluaupp": "node bin/cluaupp.js",
|
|
30
32
|
"cluau": "node bin/cluaupp.js",
|
|
31
|
-
"build": "
|
|
32
|
-
"
|
|
33
|
-
"
|
|
33
|
+
"build": "tsc",
|
|
34
|
+
"build:cli": "tsc",
|
|
35
|
+
"dev:cli": "tsx src/cli.ts",
|
|
36
|
+
"prepare": "tsc",
|
|
37
|
+
"test": "node test/out.test.js && node test/types.test.js && node test/modules.test.js && node test/security.test.js && node test/understander.test.js",
|
|
34
38
|
"generate-api": "node scripts/generate-api.js",
|
|
35
39
|
"check-highlight": "node scripts/check-highlight.js",
|
|
36
40
|
"vendor-libs": "node scripts/vendor-libs.js",
|
|
@@ -39,7 +43,7 @@
|
|
|
39
43
|
"site": "node scripts/generate-api.js && node scripts/check-highlight.js",
|
|
40
44
|
"dev": "node scripts/dev.js",
|
|
41
45
|
"stop": "node scripts/stop.js",
|
|
42
|
-
"prepublishOnly": "npm test"
|
|
46
|
+
"prepublishOnly": "tsc && npm test"
|
|
43
47
|
},
|
|
44
48
|
"engines": {
|
|
45
49
|
"node": ">=18"
|
|
@@ -62,5 +66,18 @@
|
|
|
62
66
|
"bugs": {
|
|
63
67
|
"url": "https://github.com/KartzRbx/Cluaupp/issues"
|
|
64
68
|
},
|
|
65
|
-
"homepage": "https://github.com/KartzRbx/Cluaupp#readme"
|
|
69
|
+
"homepage": "https://github.com/KartzRbx/Cluaupp#readme",
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"commander": "^15.0.0",
|
|
72
|
+
"comment-json": "^5.0.0",
|
|
73
|
+
"tree-sitter": "^0.21.1",
|
|
74
|
+
"tree-sitter-cpp": "^0.23.4",
|
|
75
|
+
"vscode-languageserver": "^10.1.1",
|
|
76
|
+
"vscode-languageserver-textdocument": "^1.0.14"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@types/node": "^22.20.3",
|
|
80
|
+
"tsx": "^4.23.13",
|
|
81
|
+
"typescript": "^7.0.2"
|
|
82
|
+
}
|
|
66
83
|
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const projectRoot = path.resolve(__dirname, "..");
|
|
5
|
+
const requireGenerated = createRequire(path.join(projectRoot, "src", "api.generated.js"));
|
|
6
|
+
const generated = requireGenerated("./api.generated.js") as {
|
|
7
|
+
INSTANCE_TYPES: string[];
|
|
8
|
+
SERVICES: string[];
|
|
9
|
+
METHODS: string[];
|
|
10
|
+
DATATYPES: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const INSTANCE_TYPES = new Set(generated.INSTANCE_TYPES);
|
|
14
|
+
export const SERVICES = new Set(generated.SERVICES);
|
|
15
|
+
export const METHODS = new Set(generated.METHODS);
|
|
16
|
+
export const DATATYPES = new Set(generated.DATATYPES);
|
|
17
|
+
|
|
18
|
+
const LUAU_TYPES: Record<string, string | null> = {
|
|
19
|
+
void: "()",
|
|
20
|
+
int: "number",
|
|
21
|
+
float: "number",
|
|
22
|
+
double: "number",
|
|
23
|
+
bool: "boolean",
|
|
24
|
+
string: "string",
|
|
25
|
+
auto: null,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function isInstanceType(name: string): boolean {
|
|
29
|
+
return INSTANCE_TYPES.has(name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isService(name: string): boolean {
|
|
33
|
+
return SERVICES.has(name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isMethod(name: string): boolean {
|
|
37
|
+
return METHODS.has(name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isDatatype(name: string): boolean {
|
|
41
|
+
return DATATYPES.has(name);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function luauType(name: string | null | undefined): string | null {
|
|
45
|
+
if (!name) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const cleaned = String(name).replace(/\*+$/, "").replace(/^const\s+/, "").replace(/^Enum::/, "Enum.");
|
|
49
|
+
if (cleaned in LUAU_TYPES) {
|
|
50
|
+
return LUAU_TYPES[cleaned];
|
|
51
|
+
}
|
|
52
|
+
return cleaned;
|
|
53
|
+
}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const {
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { emit } from "./emit.js";
|
|
4
|
+
import { requireCluauppLib } from "./libs.js";
|
|
5
|
+
import { organizeDecls, janitorNames, emitSection, joinBlocks, SECTION, ENTRY_FN } from "./layout.js";
|
|
6
|
+
import {
|
|
8
7
|
VALUE_CLASSES,
|
|
9
8
|
analyze,
|
|
10
9
|
toPascalServiceName,
|
|
11
10
|
legacyOutName,
|
|
12
|
-
|
|
11
|
+
modernScriptOutName,
|
|
12
|
+
} from "./understand.js";
|
|
13
|
+
import { implOutName } from "./preprocess.js";
|
|
13
14
|
|
|
14
15
|
function header(plan) {
|
|
15
|
-
const lines = [
|
|
16
|
+
const lines = [];
|
|
17
|
+
if (plan && plan.strict) {
|
|
18
|
+
lines.push("--!strict");
|
|
19
|
+
}
|
|
20
|
+
lines.push("-- Compiled by Cluaupp — C++ × Luau");
|
|
16
21
|
if (plan && plan.reasoning) {
|
|
17
22
|
for (const line of plan.reasoning) {
|
|
18
23
|
lines.push(`-- ${line}`);
|
|
@@ -328,9 +333,7 @@ function emitDomainController(plan, ast, options) {
|
|
|
328
333
|
return decl.type === "decl" || decl.type === "proto" || decl.type === "function" || decl.type === "expr";
|
|
329
334
|
});
|
|
330
335
|
const groups = organizeDecls(keep);
|
|
331
|
-
const subset = { type: "program", body: keep };
|
|
332
336
|
const name = plan.roles.domain;
|
|
333
|
-
const libs = collectLibraries(options.source || "", subset);
|
|
334
337
|
const entry = keep.find((decl) => decl.type === "function" && ENTRY_FN.test(decl.name || ""));
|
|
335
338
|
const bootExprs = keep.filter((decl) => decl.type === "expr");
|
|
336
339
|
const janitors = janitorNames(keep);
|
|
@@ -356,7 +359,7 @@ function emitDomainController(plan, ast, options) {
|
|
|
356
359
|
const startFn = `function ${name}.Start()\n ${name}.Stop()\n${boot}\nend`;
|
|
357
360
|
const stopFn = `function ${name}.Stop()\n${stopLines.join("\n")}\nend`;
|
|
358
361
|
const body = `${header(plan).trimEnd()}\n\n${emitSection(SECTION.api, emitDecls(groups.api, options))}${emitSection(SECTION.constants, emitDecls(groups.constants, options))}${emitSection(SECTION.variables, joinBlocks([emitDecls(groups.variables, options), `local ${name} = {}`]))}${emitSection(SECTION.support, emitDecls(groups.support, options))}${emitSection(SECTION.principal, joinBlocks([emitDecls(groups.principal, options), startFn]))}${emitSection(SECTION.cleanup, joinBlocks([emitDecls(groups.cleanup, options), stopFn]))}${emitSection(SECTION.returns, `return ${name}`)}`;
|
|
359
|
-
return
|
|
362
|
+
return body;
|
|
360
363
|
}
|
|
361
364
|
|
|
362
365
|
function emitModule(plan, ast, options) {
|
|
@@ -366,7 +369,6 @@ function emitModule(plan, ast, options) {
|
|
|
366
369
|
function emitScriptMeta(runContext) {
|
|
367
370
|
return `${JSON.stringify(
|
|
368
371
|
{
|
|
369
|
-
className: "Script",
|
|
370
372
|
properties: {
|
|
371
373
|
RunContext: `Enum.RunContext.${runContext}`,
|
|
372
374
|
},
|
|
@@ -380,7 +382,7 @@ function serviceBootName(plan) {
|
|
|
380
382
|
if (plan.isClient) {
|
|
381
383
|
return "init.client.luau";
|
|
382
384
|
}
|
|
383
|
-
return "init.luau";
|
|
385
|
+
return "init.server.luau";
|
|
384
386
|
}
|
|
385
387
|
|
|
386
388
|
function serviceFiles(plan, ast, options, dir) {
|
|
@@ -404,33 +406,75 @@ function serviceFiles(plan, ast, options, dir) {
|
|
|
404
406
|
return files;
|
|
405
407
|
}
|
|
406
408
|
|
|
409
|
+
function serviceFolderStale(plan, rel) {
|
|
410
|
+
const dir = path.posix.dirname(rel).replace(/^\.$/, "");
|
|
411
|
+
const serviceDir = dir && dir !== "." ? dir : plan.isClient ? "client" : "server";
|
|
412
|
+
const folder = `${serviceDir}/${plan.serviceName}`;
|
|
413
|
+
return [
|
|
414
|
+
folder,
|
|
415
|
+
`${folder}/init.luau`,
|
|
416
|
+
`${folder}/init.server.luau`,
|
|
417
|
+
`${folder}/init.client.luau`,
|
|
418
|
+
`${folder}/init.meta.json`,
|
|
419
|
+
`${folder}/Main.luau`,
|
|
420
|
+
`${folder}/PlayersManager.luau`,
|
|
421
|
+
`${folder}/CacheController.luau`,
|
|
422
|
+
`${folder}/${plan.typesName}.luau`,
|
|
423
|
+
plan.roles && plan.roles.domain ? `${folder}/${plan.roles.domain}.luau` : null,
|
|
424
|
+
].filter(Boolean);
|
|
425
|
+
}
|
|
426
|
+
|
|
407
427
|
function planOutput(ast, fileName, options = {}) {
|
|
408
428
|
const plan = analyze(ast, fileName);
|
|
429
|
+
plan.strict = options.strict === true;
|
|
409
430
|
const rel = (options.relativeName || fileName).replace(/\\/g, "/");
|
|
410
431
|
const dir = path.dirname(rel).replace(/^\.$/, "");
|
|
411
432
|
const outDir = dir && dir !== "." ? dir : "";
|
|
412
433
|
const prefix = outDir ? `${outDir}/` : "";
|
|
434
|
+
const stale = serviceFolderStale(plan, rel);
|
|
413
435
|
|
|
414
|
-
if (options.
|
|
415
|
-
return {
|
|
436
|
+
if (options.siblingHeader) {
|
|
437
|
+
return {
|
|
438
|
+
kind: "module",
|
|
439
|
+
plan,
|
|
440
|
+
files: [{ name: implOutName(rel), contents: emitModule(plan, ast, { ...options, skipInit: true }) }],
|
|
441
|
+
stale: [],
|
|
442
|
+
};
|
|
416
443
|
}
|
|
417
444
|
|
|
418
|
-
if (
|
|
419
|
-
|
|
445
|
+
if (options.architecture === true) {
|
|
446
|
+
if (plan.kind === "legacy") {
|
|
447
|
+
return { kind: "legacy", plan, files: null, stale: [], outName: legacyOutName(rel) };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (plan.kind === "service") {
|
|
451
|
+
const serviceDir = outDir || (plan.isClient ? "client" : "server");
|
|
452
|
+
const folder = `${serviceDir}/${plan.serviceName}`;
|
|
453
|
+
const serviceStale = plan.isClient
|
|
454
|
+
? [`${folder}/init.luau`, `${folder}/init.server.luau`, `${folder}/init.meta.json`]
|
|
455
|
+
: [`${folder}/init.luau`, `${folder}/init.client.luau`];
|
|
456
|
+
return {
|
|
457
|
+
kind: "service",
|
|
458
|
+
plan,
|
|
459
|
+
files: serviceFiles(plan, ast, options, serviceDir),
|
|
460
|
+
stale: serviceStale,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (plan.kind === "config" || plan.kind === "module") {
|
|
465
|
+
return {
|
|
466
|
+
kind: plan.kind,
|
|
467
|
+
plan,
|
|
468
|
+
files: [{ name: `${prefix}${plan.serviceName}.luau`, contents: emitModule(plan, ast, options) }],
|
|
469
|
+
stale: [rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau")],
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return { kind: "flat", plan, files: null, stale: [], outName: modernScriptOutName(rel) };
|
|
420
474
|
}
|
|
421
475
|
|
|
422
|
-
if (plan.kind === "
|
|
423
|
-
|
|
424
|
-
const folder = `${serviceDir}/${plan.serviceName}`;
|
|
425
|
-
const stale = plan.isClient
|
|
426
|
-
? [`${folder}/init.luau`, `${folder}/init.server.luau`, `${folder}/init.meta.json`]
|
|
427
|
-
: [`${folder}/init.server.luau`, `${folder}/init.client.luau`];
|
|
428
|
-
return {
|
|
429
|
-
kind: "service",
|
|
430
|
-
plan,
|
|
431
|
-
files: serviceFiles(plan, ast, options, serviceDir),
|
|
432
|
-
stale,
|
|
433
|
-
};
|
|
476
|
+
if (plan.kind === "legacy") {
|
|
477
|
+
return { kind: "legacy", plan, files: null, stale, outName: legacyOutName(rel) };
|
|
434
478
|
}
|
|
435
479
|
|
|
436
480
|
if (plan.kind === "config" || plan.kind === "module") {
|
|
@@ -438,14 +482,17 @@ function planOutput(ast, fileName, options = {}) {
|
|
|
438
482
|
kind: plan.kind,
|
|
439
483
|
plan,
|
|
440
484
|
files: [{ name: `${prefix}${plan.serviceName}.luau`, contents: emitModule(plan, ast, options) }],
|
|
441
|
-
stale: [rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau")],
|
|
485
|
+
stale: [rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau"), ...stale],
|
|
442
486
|
};
|
|
443
487
|
}
|
|
444
488
|
|
|
445
|
-
|
|
489
|
+
const outName = modernScriptOutName(rel);
|
|
490
|
+
const stripped = rel.replace(/\.(server|client|plugin)\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau");
|
|
491
|
+
const extraStale = stripped !== outName ? [stripped] : [];
|
|
492
|
+
return { kind: "flat", plan, files: null, stale: [...stale, ...extraStale], outName };
|
|
446
493
|
}
|
|
447
494
|
|
|
448
|
-
|
|
495
|
+
export {
|
|
449
496
|
analyze,
|
|
450
497
|
planOutput,
|
|
451
498
|
toPascalServiceName,
|
package/src/ast.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CompileOptions } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export type Token = {
|
|
4
|
+
type: string;
|
|
5
|
+
value: string;
|
|
6
|
+
line: number;
|
|
7
|
+
col: number;
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type AstNode = {
|
|
13
|
+
type: string;
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type AstProgram = {
|
|
18
|
+
type: string;
|
|
19
|
+
body: AstNode[];
|
|
20
|
+
fileName: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ModuleInclude = {
|
|
24
|
+
name: string;
|
|
25
|
+
outRel: string;
|
|
26
|
+
header?: string;
|
|
27
|
+
impl?: string | null;
|
|
28
|
+
exports?: { consts?: string[]; structs?: string[]; hasProtos?: boolean };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type PreprocessOptions = CompileOptions & {
|
|
32
|
+
pragmaResolved?: boolean;
|
|
33
|
+
seen?: Set<string>;
|
|
34
|
+
moduleIncludes?: ModuleInclude[];
|
|
35
|
+
siblingHeader?: string | null;
|
|
36
|
+
source?: string;
|
|
37
|
+
};
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pkg } from "./package-info.js";
|
|
5
|
+
import { start as startLsp } from "./lsp.js";
|
|
6
|
+
import { toLuauPath } from "./preprocess.js";
|
|
7
|
+
import { installEditorSupport, syncEditorSupport } from "./intellisense.js";
|
|
8
|
+
import { RojoMapper } from "./utils/rojo-mapper.js";
|
|
9
|
+
import { ProcessOrchestrator } from "./utils/process-orchestrator.js";
|
|
10
|
+
import { build as buildProject, init, loadConfig, watch, collectCpp } from "./utils/project.js";
|
|
11
|
+
import { transpileSource } from "./transpile.js";
|
|
12
|
+
import type { BuildOptions, BuildResult } from "./types.js";
|
|
13
|
+
|
|
14
|
+
const program = new Command();
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.name("cluaupp")
|
|
18
|
+
.description("Definitive transpiler from C++ subset to structured Luau")
|
|
19
|
+
.version(pkg.version, "-v, --version", "print version")
|
|
20
|
+
.showHelpAfterError()
|
|
21
|
+
.action(() => {
|
|
22
|
+
program.outputHelp();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command("init")
|
|
27
|
+
.description("create a game (src/server, src/client, src/shared)")
|
|
28
|
+
.argument("[folder]", "destination folder", ".")
|
|
29
|
+
.action(async (folder: string) => {
|
|
30
|
+
try {
|
|
31
|
+
await init(path.resolve(process.cwd(), folder));
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(err instanceof Error ? err.message : err);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.command("build")
|
|
40
|
+
.description("Transpila um arquivo, diretório ou projeto C++ para Luau")
|
|
41
|
+
.argument("[folder]", "project folder", ".")
|
|
42
|
+
.option("-i, --input <path>", "Arquivo ou diretório C++ de entrada")
|
|
43
|
+
.option("-o, --output <path>", "Arquivo ou diretório Luau de saída")
|
|
44
|
+
.option("-r, --rojo <path>", "Caminho para o default.project.json do Rojo", "./default.project.json")
|
|
45
|
+
.option("--strict", "Emitir --!strict")
|
|
46
|
+
.option("--format", "Rodar StyLua no output")
|
|
47
|
+
.option("--analyze", "Rodar luau-analyze no output")
|
|
48
|
+
.action(async (folder: string, options: {
|
|
49
|
+
input?: string;
|
|
50
|
+
output?: string;
|
|
51
|
+
rojo: string;
|
|
52
|
+
strict?: boolean;
|
|
53
|
+
format?: boolean;
|
|
54
|
+
analyze?: boolean;
|
|
55
|
+
}) => {
|
|
56
|
+
if (options.input) {
|
|
57
|
+
await buildInput(options);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
buildProject(path.resolve(process.cwd(), folder), {
|
|
62
|
+
format: options.format === true,
|
|
63
|
+
analyze: options.analyze === true,
|
|
64
|
+
rojo: options.rojo,
|
|
65
|
+
});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error(err instanceof Error ? err.message : err);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
program
|
|
73
|
+
.command("watch")
|
|
74
|
+
.description("rebuild on save")
|
|
75
|
+
.argument("[folder]", "project folder", ".")
|
|
76
|
+
.option("-r, --rojo <path>", "Caminho para o default.project.json do Rojo", "./default.project.json")
|
|
77
|
+
.option("--format", "Rodar StyLua no output")
|
|
78
|
+
.action((folder: string, options: { rojo?: string; format?: boolean }) => {
|
|
79
|
+
try {
|
|
80
|
+
watch(path.resolve(process.cwd(), folder), {
|
|
81
|
+
format: options.format === true,
|
|
82
|
+
rojo: options.rojo,
|
|
83
|
+
});
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error(err instanceof Error ? err.message : err);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.command("lsp")
|
|
92
|
+
.description("Cluaupp subset diagnostics over stdio (JSON-RPC). C++ completion is clangd.")
|
|
93
|
+
.argument("[folder]", "project folder", ".")
|
|
94
|
+
.action((folder: string) => {
|
|
95
|
+
startLsp({ projectRoot: path.resolve(process.cwd(), folder) });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
program
|
|
99
|
+
.command("intellisense")
|
|
100
|
+
.alias("intelisense")
|
|
101
|
+
.description("install LLVM clangd and write compile_commands.json")
|
|
102
|
+
.argument("[folder]", "project folder", ".")
|
|
103
|
+
.action(async (folder: string) => {
|
|
104
|
+
try {
|
|
105
|
+
const root = path.resolve(process.cwd(), folder);
|
|
106
|
+
syncEditorSupport(root, loadConfig(root));
|
|
107
|
+
const installed = await installEditorSupport();
|
|
108
|
+
console.log("cluaupp: compile_commands.json, .clangd, and .vscode updated in", root);
|
|
109
|
+
console.log("cluaupp: C++ completion is clangd (include/cluaupp/roblox.hpp)");
|
|
110
|
+
if (installed.clangd || installed.llvm) {
|
|
111
|
+
console.log("cluaupp: reload Cursor (Ctrl+Shift+P → Developer: Reload Window)");
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error(err instanceof Error ? err.message : err);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
async function buildInput(options: {
|
|
120
|
+
input?: string;
|
|
121
|
+
output?: string;
|
|
122
|
+
rojo: string;
|
|
123
|
+
strict?: boolean;
|
|
124
|
+
format?: boolean;
|
|
125
|
+
analyze?: boolean;
|
|
126
|
+
}): Promise<void> {
|
|
127
|
+
if (!options.input || !options.output) {
|
|
128
|
+
console.error("[Erro] --input e --output são obrigatórios neste modo.");
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const inputPath = path.resolve(options.input);
|
|
133
|
+
const outputPath = path.resolve(options.output);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await fs.stat(inputPath);
|
|
137
|
+
} catch {
|
|
138
|
+
console.error(`[Erro] O caminho de entrada especificado não existe: ${inputPath}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log("🏁 Inicializando Pipeline do Compilador Cluaupp V2...");
|
|
143
|
+
|
|
144
|
+
const mapper = new RojoMapper(path.resolve(options.rojo), path.dirname(inputPath));
|
|
145
|
+
await mapper.load();
|
|
146
|
+
|
|
147
|
+
const inputs = (await fs.stat(inputPath)).isDirectory()
|
|
148
|
+
? collectCpp(inputPath)
|
|
149
|
+
: [inputPath];
|
|
150
|
+
|
|
151
|
+
if (inputs.length === 0) {
|
|
152
|
+
console.error("no .cpp/.h/.hpp files in", inputPath);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const outputIsFile = /\.luau$/i.test(outputPath);
|
|
157
|
+
if (outputIsFile && inputs.length > 1) {
|
|
158
|
+
console.error("[Erro] --output deve ser um diretório quando a entrada contém vários arquivos.");
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const file of inputs) {
|
|
163
|
+
const sourceCode = await fs.readFile(file, "utf8");
|
|
164
|
+
const rel = path.basename(file);
|
|
165
|
+
const compiled = transpileSource(sourceCode, rel, {
|
|
166
|
+
strict: options.strict === true,
|
|
167
|
+
filePath: file,
|
|
168
|
+
relativeName: rel,
|
|
169
|
+
srcDir: path.dirname(file),
|
|
170
|
+
includeDirs: [path.dirname(file)],
|
|
171
|
+
}, mapper);
|
|
172
|
+
|
|
173
|
+
const dest = outputIsFile ? outputPath : path.join(outputPath, path.basename(toLuauPath(rel)));
|
|
174
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
175
|
+
const luau = compiled.files[0]?.contents ?? "";
|
|
176
|
+
await fs.writeFile(dest, luau, "utf8");
|
|
177
|
+
|
|
178
|
+
if (options.format === true) {
|
|
179
|
+
await ProcessOrchestrator.formatWithStyLua(dest);
|
|
180
|
+
}
|
|
181
|
+
if (options.analyze === true) {
|
|
182
|
+
const analysisReport = await ProcessOrchestrator.analyzeWithLuau(dest);
|
|
183
|
+
if (analysisReport) {
|
|
184
|
+
console.log("\nRelatório de Análise Estática do Luau:\n", analysisReport);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
console.log(`Transpilação concluída! ${file} → ${dest}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function build(root: string, options: BuildOptions = {}): BuildResult {
|
|
192
|
+
return buildProject(root, options);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export { watch, init, program };
|
|
196
|
+
|
|
197
|
+
export function dispatch(args: string[]): Promise<Command> {
|
|
198
|
+
return program.parseAsync(["node", "cluaupp", ...args]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isMain(): boolean {
|
|
202
|
+
const entry = process.argv[1];
|
|
203
|
+
if (!entry) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return ["cli.js", "cli.ts", "cluaupp.js", "cluau.js"].includes(path.basename(entry));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (isMain()) {
|
|
210
|
+
program.parseAsync(process.argv).catch((err: unknown) => {
|
|
211
|
+
console.error(err instanceof Error ? err.message : err);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
});
|
|
214
|
+
}
|