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,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.1.4",
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": "src/compile.js",
11
+ "main": "./generated/compile.js",
12
12
  "exports": {
13
- ".": "./src/compile.js",
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": "node bin/cluaupp.js build",
32
- "watch": "node bin/cluaupp.js watch",
33
- "test": "node test/leaderstats.test.js && node test/understand.test.js && node test/datatypes.test.js && node test/headers.test.js && node test/libs.test.js && node test/runtime-libs.test.js && node test/intellisense.test.js && node test/intellisense-complete.test.js && node test/dataservice.test.js && node test/datacontroller-signal.test.js && node test/cout.test.js && node test/prune.test.js && node test/hold.test.js && node test/vendor-stable.test.js && node test/switch.test.js && node test/qualified.test.js && node test/module-require.test.js",
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
- "use strict";
2
-
3
- const path = require("path");
4
- const { emit } = require("./emit");
5
- const { collectLibraries, insertRequires, requireCluauppLib } = require("./libs");
6
- const { organizeDecls, janitorNames, emitSection, joinBlocks, SECTION, ENTRY_FN } = require("./layout");
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
- } = require("./understand");
11
+ modernScriptOutName,
12
+ } from "./understand.js";
13
+ import { implOutName } from "./preprocess.js";
13
14
 
14
15
  function header(plan) {
15
- const lines = ["--!strict", "-- Compiled by Cluaupp — C++ × Luau"];
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 insertRequires(body, libs);
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.architecture === false) {
415
- return { kind: "flat", plan, files: null };
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 (plan.kind === "legacy") {
419
- return { kind: "legacy", plan, files: null, stale: [], outName: legacyOutName(rel) };
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 === "service") {
423
- const serviceDir = outDir || (plan.isClient ? "client" : "server");
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
- return { kind: "flat", plan, files: null, stale: [] };
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
- module.exports = {
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
+ }