cluaupp 0.2.1 → 0.2.3

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 (45) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/docs/README.md +32 -20
  4. package/docs/cli.md +1 -1
  5. package/docs/cpp-advanced.md +6 -3
  6. package/docs/cpp-types.md +2 -0
  7. package/docs/examples/hud.md +1 -1
  8. package/docs/examples/index.md +1 -1
  9. package/docs/examples/leaderstats.md +44 -115
  10. package/docs/getting-started.md +4 -2
  11. package/docs/intellisense.md +4 -5
  12. package/docs/libraries/dataservice.md +4 -1
  13. package/docs/libraries/index.md +2 -1
  14. package/docs/libraries/more.md +3 -1
  15. package/docs/libraries/ui.md +76 -0
  16. package/docs/oop/index.md +20 -7
  17. package/docs/roblox-api.md +4 -56
  18. package/docs/syntax.md +112 -55
  19. package/editors/vscode/package.json +25 -25
  20. package/examples/game/.vscode/extensions.json +3 -0
  21. package/examples/game/.vscode/settings.json +0 -9
  22. package/examples/game/src/shared/config.h +2 -0
  23. package/generated/api.js +15 -1
  24. package/generated/compile.js +38 -0
  25. package/generated/emit.js +79 -3
  26. package/generated/intellisense.js +75 -80
  27. package/generated/lex.js +3 -0
  28. package/generated/parse.js +119 -24
  29. package/generated/parser/collector.js +37 -1
  30. package/generated/utils/rojo-mapper.js +23 -3
  31. package/include/cluaupp/datatypes.hpp +4 -0
  32. package/package.json +98 -87
  33. package/src/api.ts +15 -1
  34. package/src/compile.ts +43 -1
  35. package/src/emit.ts +81 -3
  36. package/src/intellisense.ts +80 -80
  37. package/src/lex.ts +3 -0
  38. package/src/parse.ts +124 -25
  39. package/src/parser/collector.ts +35 -2
  40. package/src/utils/rojo-mapper.ts +26 -3
  41. package/templates/game/.vscode/extensions.json +3 -0
  42. package/templates/game/.vscode/settings.json +0 -9
  43. package/templates/game/src/shared/config.h +2 -0
  44. package/examples/game/.vscode/c_cpp_properties.json +0 -27
  45. package/templates/game/.vscode/c_cpp_properties.json +0 -27
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.intelliSenseMode = intelliSenseMode;
7
6
  exports.clangPath = clangPath;
8
7
  exports.writeCompileCommands = writeCompileCommands;
9
8
  exports.writeClangd = writeClangd;
@@ -65,32 +64,6 @@ function compileCommandEntry(root, file) {
65
64
  arguments: [clangPath(), ...clangCompileArgs(root), "-c", absFile],
66
65
  };
67
66
  }
68
- function mergeJson(file, patch) {
69
- let current = {};
70
- if (node_fs_1.default.existsSync(file)) {
71
- try {
72
- current = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
73
- }
74
- catch {
75
- current = {};
76
- }
77
- }
78
- const next = { ...current, ...patch };
79
- for (const key of Object.keys(patch)) {
80
- const value = patch[key];
81
- const existing = current[key];
82
- if (value && typeof value === "object" && !Array.isArray(value) && existing && typeof existing === "object" && !Array.isArray(existing)) {
83
- next[key] = { ...existing, ...value };
84
- }
85
- }
86
- const json = `${JSON.stringify(next, null, "\t")}\n`;
87
- if (node_fs_1.default.existsSync(file) && node_fs_1.default.readFileSync(file, "utf8") === json) {
88
- return false;
89
- }
90
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
91
- node_fs_1.default.writeFileSync(file, json, "utf8");
92
- return true;
93
- }
94
67
  function copyDir(from, to) {
95
68
  node_fs_1.default.mkdirSync(to, { recursive: true });
96
69
  for (const entry of node_fs_1.default.readdirSync(from, { withFileTypes: true })) {
@@ -104,6 +77,55 @@ function copyDir(from, to) {
104
77
  }
105
78
  }
106
79
  }
80
+ function uniqueStrings(...lists) {
81
+ const seen = new Set();
82
+ const out = [];
83
+ for (const list of lists) {
84
+ if (!Array.isArray(list)) {
85
+ continue;
86
+ }
87
+ for (const item of list) {
88
+ if (typeof item === "string" && item && !seen.has(item)) {
89
+ seen.add(item);
90
+ out.push(item);
91
+ }
92
+ }
93
+ }
94
+ return out;
95
+ }
96
+ function stripCppToolsSettings(current) {
97
+ const next = {};
98
+ for (const [key, value] of Object.entries(current)) {
99
+ if (key === "C_Cpp" || key.startsWith("C_Cpp.")) {
100
+ continue;
101
+ }
102
+ next[key] = value;
103
+ }
104
+ return next;
105
+ }
106
+ function readJsonObject(file) {
107
+ if (!node_fs_1.default.existsSync(file)) {
108
+ return {};
109
+ }
110
+ try {
111
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
112
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
113
+ return parsed;
114
+ }
115
+ }
116
+ catch {
117
+ // ignore corrupt editor json
118
+ }
119
+ return {};
120
+ }
121
+ function writeJson(file, value) {
122
+ const json = `${JSON.stringify(value, null, "\t")}\n`;
123
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
124
+ if (node_fs_1.default.existsSync(file) && node_fs_1.default.readFileSync(file, "utf8") === json) {
125
+ return;
126
+ }
127
+ node_fs_1.default.writeFileSync(file, json, "utf8");
128
+ }
107
129
  function clangdEditorArgs() {
108
130
  return [
109
131
  "--compile-commands-dir=${workspaceFolder}",
@@ -111,16 +133,6 @@ function clangdEditorArgs() {
111
133
  "--query-driver=**/clang++*,**/clang++.exe,**/g++*,**/g++.exe",
112
134
  ];
113
135
  }
114
- function intelliSenseMode() {
115
- const arch = process.arch === "arm64" ? "arm64" : "x64";
116
- if (process.platform === "win32") {
117
- return `windows-clang-${arch}`;
118
- }
119
- if (process.platform === "darwin") {
120
- return `macos-clang-${arch}`;
121
- }
122
- return `linux-clang-${arch}`;
123
- }
124
136
  function clangPath() {
125
137
  return (0, editor_install_js_1.findClangPlusPlus)() || "clang++";
126
138
  }
@@ -167,54 +179,37 @@ Index:
167
179
  return true;
168
180
  }
169
181
  function writeVscode(root) {
170
- const settings = node_path_1.default.join(root, ".vscode", "settings.json");
171
- mergeJson(settings, {
172
- "C_Cpp.intelliSenseEngine": "Disabled",
173
- "C_Cpp.default.cppStandard": "c++20",
174
- "C_Cpp.default.cStandard": "c17",
175
- "C_Cpp.default.compilerPath": clangPath(),
176
- "C_Cpp.default.intelliSenseMode": intelliSenseMode(),
177
- "C_Cpp.default.includePath": ["${workspaceFolder}/include", "${workspaceFolder}/src"],
182
+ const settingsPath = node_path_1.default.join(root, ".vscode", "settings.json");
183
+ const current = readJsonObject(settingsPath);
184
+ const associations = {
185
+ ...((current["files.associations"] && typeof current["files.associations"] === "object" && !Array.isArray(current["files.associations"]))
186
+ ? current["files.associations"]
187
+ : {}),
188
+ "*.hpp": "cpp",
189
+ "*.h": "cpp",
190
+ "*.server.cpp": "cpp",
191
+ "*.client.cpp": "cpp",
192
+ "*.plugin.cpp": "cpp",
193
+ "*.legacy.cpp": "cpp",
194
+ "*.legacy.server.cpp": "cpp",
195
+ "*.legacy.client.cpp": "cpp",
196
+ };
197
+ writeJson(settingsPath, {
198
+ ...stripCppToolsSettings(current),
178
199
  "clangd.enable": true,
179
200
  "clangd.arguments": clangdEditorArgs(),
180
- "files.associations": {
181
- "*.hpp": "cpp",
182
- "*.h": "cpp",
183
- "*.server.cpp": "cpp",
184
- "*.client.cpp": "cpp",
185
- "*.plugin.cpp": "cpp",
186
- "*.legacy.cpp": "cpp",
187
- "*.legacy.server.cpp": "cpp",
188
- "*.legacy.client.cpp": "cpp",
189
- },
201
+ "files.associations": associations,
190
202
  });
191
- mergeJson(node_path_1.default.join(root, ".vscode", "extensions.json"), {
192
- recommendations: ["llvm-vs-code-extensions.vscode-clangd"],
203
+ const extensionsPath = node_path_1.default.join(root, ".vscode", "extensions.json");
204
+ const extensions = readJsonObject(extensionsPath);
205
+ writeJson(extensionsPath, {
206
+ ...extensions,
207
+ recommendations: uniqueStrings(extensions.recommendations, ["llvm-vs-code-extensions.vscode-clangd"]),
208
+ unwantedRecommendations: uniqueStrings(extensions.unwantedRecommendations, ["ms-vscode.cpptools"]),
193
209
  });
194
210
  const props = node_path_1.default.join(root, ".vscode", "c_cpp_properties.json");
195
- const nextProps = {
196
- configurations: [
197
- {
198
- name: "Cluaupp",
199
- compilerPath: clangPath(),
200
- cStandard: "c17",
201
- cppStandard: "c++20",
202
- intelliSenseMode: intelliSenseMode(),
203
- includePath: ["${workspaceFolder}/include", "${workspaceFolder}/src"],
204
- forcedInclude: ["${workspaceFolder}/include/cluaupp/roblox.hpp"],
205
- compileCommands: "${workspaceFolder}/compile_commands.json",
206
- browse: {
207
- path: ["${workspaceFolder}/include", "${workspaceFolder}/src"],
208
- limitSymbolsToIncludedHeaders: true,
209
- },
210
- },
211
- ],
212
- version: 4,
213
- };
214
- const json = `${JSON.stringify(nextProps, null, "\t")}\n`;
215
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(props), { recursive: true });
216
- if (!node_fs_1.default.existsSync(props) || node_fs_1.default.readFileSync(props, "utf8") !== json) {
217
- node_fs_1.default.writeFileSync(props, json, "utf8");
211
+ if (node_fs_1.default.existsSync(props)) {
212
+ node_fs_1.default.unlinkSync(props);
218
213
  }
219
214
  }
220
215
  function diagnosticsFor(source, fileName, options = {}) {
package/generated/lex.js CHANGED
@@ -33,6 +33,9 @@ const KEYWORDS = new Set([
33
33
  "case",
34
34
  "default",
35
35
  "break",
36
+ "static",
37
+ "constexpr",
38
+ "inline",
36
39
  ]);
37
40
  function tokenize(source) {
38
41
  const tokens = [];
@@ -46,21 +46,124 @@ function parse(source, fileName) {
46
46
  eat("op", ">");
47
47
  return name;
48
48
  };
49
+ const CAST_NAMES = new Set(["static_cast", "const_cast", "reinterpret_cast", "dynamic_cast"]);
50
+ function skipSpecifiers() {
51
+ let isConst = false;
52
+ while (at("kw", "static") || at("kw", "inline") || at("kw", "constexpr") || at("kw", "const")) {
53
+ if (at("kw", "constexpr") || at("kw", "const")) {
54
+ isConst = true;
55
+ }
56
+ i += 1;
57
+ }
58
+ return isConst;
59
+ }
60
+ function looksLikePrimaryStart() {
61
+ if (at("ident") || at("number") || at("string")) {
62
+ return true;
63
+ }
64
+ if (at("kw", "true") || at("kw", "false") || at("kw", "nullptr") || at("kw", "new")) {
65
+ return true;
66
+ }
67
+ return at("op", "(") || at("op", "{") || at("op", "[") || at("op", "-") || at("op", "!");
68
+ }
69
+ function parseLambda() {
70
+ eat("op", "[");
71
+ while (!at("eof") && !at("op", "]")) {
72
+ i += 1;
73
+ }
74
+ eat("op", "]");
75
+ const params = [];
76
+ if (at("op", "(")) {
77
+ i += 1;
78
+ if (!at("op", ")")) {
79
+ params.push(parseParam());
80
+ while (at("op", ",")) {
81
+ i += 1;
82
+ params.push(parseParam());
83
+ }
84
+ }
85
+ eat("op", ")");
86
+ }
87
+ const body = parseBlock();
88
+ return { type: "lambda", params, body };
89
+ }
90
+ function parseNamedCast() {
91
+ i += 1;
92
+ eat("op", "<");
93
+ const valueType = parseType() || "any";
94
+ eat("op", ">");
95
+ const args = parseArgs();
96
+ return { type: "cast", valueType, argument: args[0] || { type: "null" } };
97
+ }
98
+ function parseParenPrimary() {
99
+ eat("op", "(");
100
+ const saved = i;
101
+ skipSpecifiers();
102
+ const valueType = parseType();
103
+ if (valueType && at("op", ")")) {
104
+ i += 1;
105
+ if (looksLikePrimaryStart()) {
106
+ return { type: "cast", valueType, argument: parseUnary() };
107
+ }
108
+ }
109
+ i = saved;
110
+ const expr = parseExpr();
111
+ eat("op", ")");
112
+ return expr;
113
+ }
114
+ function eatGeneric() {
115
+ let text = "";
116
+ let depth = 0;
117
+ while (!at("eof")) {
118
+ const token = peek();
119
+ if (at("op", "<")) {
120
+ depth += 1;
121
+ text += "<";
122
+ i += 1;
123
+ continue;
124
+ }
125
+ if (at("op", ">")) {
126
+ depth -= 1;
127
+ text += ">";
128
+ i += 1;
129
+ if (depth === 0) {
130
+ return text;
131
+ }
132
+ continue;
133
+ }
134
+ if (token.type === "string") {
135
+ text += `"${token.value}"`;
136
+ }
137
+ else {
138
+ text += token.value;
139
+ }
140
+ i += 1;
141
+ }
142
+ return text;
143
+ }
49
144
  function parseType() {
50
145
  if (at("kw", "const")) {
51
146
  i += 1;
52
147
  }
148
+ let name = null;
53
149
  if (at("kw", "auto") || at("kw", "void") || at("kw", "int") || at("kw", "bool") || at("kw", "float") || at("kw", "double")) {
54
- const name = eat("kw").value;
55
- while (at("op", "*")) {
150
+ name = eat("kw").value;
151
+ }
152
+ else if (at("ident")) {
153
+ name = eat("ident").value;
154
+ while (at("op", "::")) {
56
155
  i += 1;
156
+ if (at("ident")) {
157
+ name += `::${eat("ident").value}`;
158
+ }
57
159
  }
58
- return name;
59
160
  }
60
- if (!at("ident")) {
161
+ else {
61
162
  return null;
62
163
  }
63
- const name = eat("ident").value;
164
+ if (at("op", "<")) {
165
+ name += eatGeneric();
166
+ }
64
167
  while (at("op", "*")) {
65
168
  i += 1;
66
169
  }
@@ -129,14 +232,17 @@ function parse(source, fileName) {
129
232
  eat("op", ")");
130
233
  return { type: "getService", service };
131
234
  }
235
+ if (at("ident") && CAST_NAMES.has(peek().value) && peek(1).value === "<") {
236
+ return parseNamedCast();
237
+ }
132
238
  if (at("ident")) {
133
239
  return { type: "ident", name: eat("ident").value };
134
240
  }
241
+ if (at("op", "[")) {
242
+ return parseLambda();
243
+ }
135
244
  if (at("op", "(")) {
136
- i += 1;
137
- const expr = parseExpr();
138
- eat("op", ")");
139
- return expr;
245
+ return parseParenPrimary();
140
246
  }
141
247
  if (at("op", "{")) {
142
248
  return parseInitList();
@@ -309,11 +415,8 @@ function parse(source, fileName) {
309
415
  throw error("only range-for is supported: for (auto* x : list)");
310
416
  }
311
417
  function parseDeclOrExpr() {
312
- const isConst = at("kw", "const");
313
- if (isConst) {
314
- i += 1;
315
- }
316
418
  const saved = i;
419
+ const isConst = skipSpecifiers();
317
420
  const maybeType = parseType();
318
421
  if (maybeType && at("ident") && (peek(1).value === "=" || peek(1).value === ";")) {
319
422
  const name = eat("ident").value;
@@ -325,9 +428,6 @@ function parse(source, fileName) {
325
428
  return { type: "decl", name, valueType: maybeType, value, isConst: isConst || maybeType === "const" };
326
429
  }
327
430
  i = saved;
328
- if (isConst) {
329
- i = saved - 1;
330
- }
331
431
  return { type: "expr", expr: parseExpr() };
332
432
  }
333
433
  function parseStmt() {
@@ -380,6 +480,7 @@ function parse(source, fileName) {
380
480
  };
381
481
  }
382
482
  function parseFunction() {
483
+ skipSpecifiers();
383
484
  const returnType = parseType();
384
485
  if (!returnType || !at("ident")) {
385
486
  throw error("invalid function declaration");
@@ -449,10 +550,7 @@ function parse(source, fileName) {
449
550
  continue;
450
551
  }
451
552
  const saved = i;
452
- const isConst = at("kw", "const");
453
- if (isConst) {
454
- i += 1;
455
- }
553
+ const isConst = skipSpecifiers();
456
554
  const valueType = parseType();
457
555
  if (valueType && at("ident")) {
458
556
  if (peek(1).value === "(") {
@@ -573,10 +671,7 @@ function parse(source, fileName) {
573
671
  continue;
574
672
  }
575
673
  const saved = i;
576
- const isConst = at("kw", "const");
577
- if (isConst) {
578
- i += 1;
579
- }
674
+ const isConst = skipSpecifiers();
580
675
  const maybeType = parseType();
581
676
  if (maybeType && at("ident")) {
582
677
  const next = peek(1);
@@ -1,7 +1,12 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.ASTCollector = void 0;
4
7
  exports.emptyState = emptyState;
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const preprocess_js_1 = require("../preprocess.js");
5
10
  const translators_js_1 = require("../emitter/translators.js");
6
11
  function emptyState(strict = false) {
7
12
  return {
@@ -16,9 +21,11 @@ function emptyState(strict = false) {
16
21
  }
17
22
  class ASTCollector {
18
23
  mapper;
24
+ context;
19
25
  state;
20
26
  constructor(mapper, context = { fileName: "input.cpp" }) {
21
27
  this.mapper = mapper;
28
+ this.context = context;
22
29
  this.state = emptyState(context.strict === true);
23
30
  }
24
31
  collect(rootNode) {
@@ -58,12 +65,41 @@ class ASTCollector {
58
65
  }
59
66
  }
60
67
  }
68
+ includeTarget(raw) {
69
+ const from = this.context.sourcePath;
70
+ if (!from) {
71
+ return null;
72
+ }
73
+ const cleaned = raw.replace(/[<>'"]/g, "").trim();
74
+ if (!cleaned || raw.includes("<")) {
75
+ return null;
76
+ }
77
+ return node_path_1.default.resolve(node_path_1.default.dirname(from), cleaned);
78
+ }
79
+ isOwnHeader(raw) {
80
+ const from = this.context.sourcePath;
81
+ if (!from) {
82
+ return false;
83
+ }
84
+ const target = this.includeTarget(raw);
85
+ if (!target) {
86
+ return false;
87
+ }
88
+ if (node_path_1.default.resolve(from) === target) {
89
+ return true;
90
+ }
91
+ const header = (0, preprocess_js_1.siblingHeader)(from);
92
+ return Boolean(header && node_path_1.default.resolve(header) === target);
93
+ }
61
94
  collectInclude(node) {
62
95
  const pathNode = node.childForFieldName("path") || node.namedChildren[0] || node.child(1);
63
96
  if (!pathNode) {
64
97
  return;
65
98
  }
66
- const resolved = this.mapper.resolveIncludeToRequire(pathNode.text);
99
+ if (this.isOwnHeader(pathNode.text)) {
100
+ return;
101
+ }
102
+ const resolved = this.mapper.resolveIncludeToRequire(pathNode.text, this.context.sourcePath);
67
103
  if (resolved) {
68
104
  this.state.moduleRequires.set(resolved.alias, resolved.path);
69
105
  }
@@ -64,7 +64,23 @@ class RojoMapper {
64
64
  this.config = (0, comment_json_1.parse)(content);
65
65
  this.bindings = this.walkTree(this.config.tree, []);
66
66
  }
67
- resolveIncludeToRequire(includePath) {
67
+ resolveQuotedFile(raw, fromFile) {
68
+ const bases = [];
69
+ if (fromFile) {
70
+ bases.push(node_path_1.default.dirname(fromFile));
71
+ }
72
+ if (this.srcDir) {
73
+ bases.push(this.srcDir);
74
+ }
75
+ for (const base of bases) {
76
+ const candidate = node_path_1.default.resolve(base, raw);
77
+ if (node_fs_1.default.existsSync(candidate) && node_fs_1.default.statSync(candidate).isFile()) {
78
+ return candidate;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ resolveIncludeToRequire(includePath, fromFile) {
68
84
  const raw = includePath.replace(/[<>'"]/g, "").trim();
69
85
  if (!raw) {
70
86
  return null;
@@ -79,11 +95,15 @@ class RojoMapper {
79
95
  if (library) {
80
96
  return library;
81
97
  }
82
- const fromTree = this.resolveFromRojoTree(cleanPath, baseName);
98
+ const resolvedFile = this.resolveQuotedFile(raw, fromFile);
99
+ const lookupPath = resolvedFile && this.srcDir
100
+ ? posix(node_path_1.default.relative(this.srcDir, resolvedFile)).replace(/\.h(pp|h)?$/i, "")
101
+ : cleanPath;
102
+ const fromTree = this.resolveFromRojoTree(lookupPath, baseName);
83
103
  if (fromTree) {
84
104
  return fromTree;
85
105
  }
86
- const segments = cleanPath.split("/").filter(Boolean);
106
+ const segments = lookupPath.split("/").filter(Boolean);
87
107
  if (segments[0] === "src") {
88
108
  segments.shift();
89
109
  }
@@ -17,6 +17,10 @@ using Enum::Axis;
17
17
 
18
18
  using string = const char*;
19
19
 
20
+ // Variadic join. Cluaupp emits Luau a .. b .. c.
21
+ template <typename... Args>
22
+ string string_concat(Args... args);
23
+
20
24
  struct Instance;
21
25
  struct Player;
22
26