cluaupp 0.1.5 → 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 +12 -0
- package/README.md +6 -1
- package/bin/cluau.js +1 -1
- package/bin/cluaupp.js +1 -1
- package/docs/architecture.md +26 -1
- package/docs/cli.md +27 -6
- package/docs/examples/index.md +2 -0
- package/docs/getting-started.md +5 -4
- package/docs/intellisense.md +49 -31
- package/docs/intro.md +1 -1
- 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} +9 -10
- 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} +4 -5
- package/src/emitter/luau-codegen.ts +187 -0
- package/src/emitter/translators.ts +168 -0
- package/src/{headers.js → headers.ts} +4 -5
- 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} +9 -4
- 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} +22 -37
- 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} +19 -210
- 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/compile_flags.txt +1 -0
- package/templates/game/rokit.toml +5 -0
- package/src/api.js +0 -57
- package/src/cli.js +0 -473
- package/src/compile.js +0 -110
- 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,17 +1,16 @@
|
|
|
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,
|
|
13
|
-
}
|
|
14
|
-
|
|
12
|
+
} from "./understand.js";
|
|
13
|
+
import { implOutName } from "./preprocess.js";
|
|
15
14
|
|
|
16
15
|
function header(plan) {
|
|
17
16
|
const lines = [];
|
|
@@ -493,7 +492,7 @@ function planOutput(ast, fileName, options = {}) {
|
|
|
493
492
|
return { kind: "flat", plan, files: null, stale: [...stale, ...extraStale], outName };
|
|
494
493
|
}
|
|
495
494
|
|
|
496
|
-
|
|
495
|
+
export {
|
|
497
496
|
analyze,
|
|
498
497
|
planOutput,
|
|
499
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
|
+
}
|
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
|
+
}
|