cluaupp 0.1.0 → 0.1.1

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1
4
+
5
+ - DataService: `DataServiceOptions<T>.Template` is the required player-data table (`T` matches the save struct). Also documents `Exclude`.
6
+ - Compiler: C++ designated initializers (`Type { .Field = value }`) emit Luau tables, so `DataService.Server:Init({ Template = ... })` type-checks and compiles.
7
+ - Watch/build: delete orphaned `out/` files when a source is removed. Never wipe `out/` or `libs/` as a whole — Rojo 7 crashes if `libs/ArrayIndexer` disappears mid-serve. Vendor/header sync only writes missing/changed files (no `rmSync` of `libs/`). Watch rebuilds skip recopying `libs/`. `default.project.json` marks `out/*`, `libs`, and `Packages` as `$optional`.
8
+
3
9
  ## 0.1.0
4
10
 
5
11
  - Product name: **Cluaupp** (C++ × Luau)
package/docs/cli.md CHANGED
@@ -31,6 +31,8 @@ Transpiles `src/**/*.{cpp,h,hpp}` into `out/`. Script files become PascalCase **
31
31
  - `.h` / `.hpp` with a sibling `.cpp` are not emitted twice
32
32
  - `new Folder(parent)` in a stats system becomes `CacheController.Ensure`
33
33
  - A parse error exits with code 1 and `file:line:column`
34
+ - After emit, **orphans in `out/` are removed** (source deleted → matching Luau deleted). `out/` itself is never wiped, so a running Rojo serve keeps the live tree.
35
+ - `libs/` and `include/cluaupp` are synced by writing missing/changed files only. They are **never** deleted as a folder — Rojo 7 unwrap-crashes if `libs/ArrayIndexer` vanishes while serving.
34
36
 
35
37
  ```bash
36
38
  cluaupp build
@@ -39,7 +41,7 @@ cluaupp build ./my-game
39
41
 
40
42
  ## `cluaupp watch [folder]`
41
43
 
42
- Runs a `build` and rebuilds when a `.cpp`, `.h`, or `.hpp` under `src/` changes. Parse errors are printed; the watcher stays alive.
44
+ Runs a `build` and rebuilds when anything under `src/` changes, including deletes. Parse errors are printed; the watcher stays alive. Rebuilds are debounced and do **not** recopy `libs/` (Rojo is likely already serving those files). Deleted `.cpp` files prune their `out/` artifacts on the next tick.
43
45
 
44
46
  ## `cluaupp --version` / `cluaupp -v`
45
47
 
package/docs/libraries.md CHANGED
@@ -67,7 +67,10 @@ void init() {
67
67
  auto* coins = Net::Event("Coins");
68
68
  coins->On(OnCoins);
69
69
  print(FormatNumber::Abbreviate(1500));
70
- DataService::Set(GetService<Players>()->GetPlayers()[0], "Currencies.Coins", 10);
70
+ DataService::Server.Init(DataServiceOptions {
71
+ .Template = { .Coins = 0 },
72
+ .StoreName = "PlayerData",
73
+ });
71
74
  janitor->Add(coins);
72
75
  }
73
76
  ```
package/docs/syntax.md CHANGED
@@ -108,6 +108,25 @@ The only `for` accepted today is **range-for**: `for (auto* x : list)`. C-style
108
108
  | `obj->Method(a)` | `obj:Method(a)` if the method is a Roblox API |
109
109
  | `fn(a)` | `fn(a)` |
110
110
  | `signal.Connect(fn)` | `signal:Connect(fn)` |
111
+ | `Type { .Field = value }` | `{ Field = value }` |
112
+
113
+ Designated initializers become Luau tables. Nested braces work the same way:
114
+
115
+ ```cpp
116
+ DataService::Server.Init(DataServiceOptions {
117
+ .Template = playerData,
118
+ .StoreName = "PlayerData",
119
+ .UseMock = true,
120
+ });
121
+ ```
122
+
123
+ ```luau
124
+ DataService.Server:Init({
125
+ Template = playerData,
126
+ StoreName = "PlayerData",
127
+ UseMock = true,
128
+ })
129
+ ```
111
130
 
112
131
  ## `new` and services
113
132
 
@@ -34,10 +34,13 @@ struct DataServiceEnum {
34
34
  DataOrderList OrderList;
35
35
  };
36
36
 
37
+ template <typename T>
37
38
  struct DataServiceOptions {
39
+ T Template;
38
40
  string StoreName;
39
41
  bool UseMock;
40
42
  string KeyPrefix;
43
+ LuaArray<DataPath> Exclude;
41
44
  bool StrictPaths;
42
45
  bool AutoCreateMissingTables;
43
46
  };
@@ -112,7 +115,8 @@ public:
112
115
  class DataServiceServer {
113
116
  public:
114
117
  DataPath Paths;
115
- DataServiceServer* Init(DataServiceOptions options);
118
+ template <typename T>
119
+ DataServiceServer* Init(DataServiceOptions<T> options);
116
120
  Data* WaitFor(Player* player);
117
121
  Data* Get(Player* player);
118
122
  bool HasData(Player* player);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cluaupp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",
@@ -29,7 +29,7 @@
29
29
  "cluau": "node bin/cluaupp.js",
30
30
  "build": "node bin/cluaupp.js build",
31
31
  "watch": "node bin/cluaupp.js watch",
32
- "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",
32
+ "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/dataservice.test.js && node test/prune.test.js",
33
33
  "generate-api": "node scripts/generate-api.js",
34
34
  "check-highlight": "node scripts/check-highlight.js",
35
35
  "vendor-libs": "node scripts/vendor-libs.js",
package/src/cli.js CHANGED
@@ -34,27 +34,69 @@ function copyDir(from, to) {
34
34
  }
35
35
  }
36
36
 
37
+ function tryRm(target) {
38
+ try {
39
+ fs.rmSync(target, { recursive: true, force: true });
40
+ return true;
41
+ } catch (err) {
42
+ if (err.code === "EPERM" || err.code === "EBUSY" || err.code === "ENOTEMPTY") {
43
+ console.error("cluaupp: skip remove", target, `(${err.code})`);
44
+ return false;
45
+ }
46
+ throw err;
47
+ }
48
+ }
49
+
50
+ function copyFileIfChanged(src, dest) {
51
+ if (fs.existsSync(dest)) {
52
+ const from = fs.statSync(src);
53
+ const to = fs.statSync(dest);
54
+ if (from.size === to.size && from.mtimeMs <= to.mtimeMs) {
55
+ return;
56
+ }
57
+ }
58
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
59
+ try {
60
+ fs.copyFileSync(src, dest);
61
+ } catch (err) {
62
+ if (err.code === "EPERM" || err.code === "EBUSY") {
63
+ console.error("cluaupp: skip copy", dest, `(${err.code})`);
64
+ return;
65
+ }
66
+ throw err;
67
+ }
68
+ }
69
+
70
+ function syncDir(from, to) {
71
+ if (!fs.existsSync(from)) {
72
+ return;
73
+ }
74
+ fs.mkdirSync(to, { recursive: true });
75
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
76
+ const src = path.join(from, entry.name);
77
+ const dest = path.join(to, entry.name);
78
+ if (entry.isDirectory()) {
79
+ syncDir(src, dest);
80
+ } else {
81
+ copyFileIfChanged(src, dest);
82
+ }
83
+ }
84
+ }
85
+
37
86
  function copyRuntime(dest) {
38
87
  const runtime = path.join(__dirname, "..", "runtime");
39
- const libs = path.join(dest, "libs");
40
- if (fs.existsSync(libs)) {
41
- fs.rmSync(libs, { recursive: true, force: true });
42
- }
43
- if (fs.existsSync(runtime)) {
44
- copyDir(runtime, libs);
88
+ if (!fs.existsSync(runtime)) {
89
+ return;
45
90
  }
91
+ syncDir(runtime, path.join(dest, "libs"));
46
92
  }
47
93
 
48
94
  function copyHeaders(dest) {
49
95
  const from = path.join(__dirname, "..", "include", "cluaupp");
50
- const to = path.join(dest, "include", "cluaupp");
51
96
  if (!fs.existsSync(from)) {
52
97
  return;
53
98
  }
54
- if (fs.existsSync(to)) {
55
- fs.rmSync(to, { recursive: true, force: true });
56
- }
57
- copyDir(from, to);
99
+ syncDir(from, path.join(dest, "include", "cluaupp"));
58
100
  }
59
101
 
60
102
  function loadConfig(root) {
@@ -83,6 +125,21 @@ function collectCpp(dir, files = []) {
83
125
  return files;
84
126
  }
85
127
 
128
+ function collectFiles(dir, files = []) {
129
+ if (!fs.existsSync(dir)) {
130
+ return files;
131
+ }
132
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
133
+ const full = path.join(dir, entry.name);
134
+ if (entry.isDirectory()) {
135
+ collectFiles(full, files);
136
+ } else {
137
+ files.push(full);
138
+ }
139
+ }
140
+ return files;
141
+ }
142
+
86
143
  function siblingCpp(file) {
87
144
  return file.replace(/\.(h|hpp|hh)$/i, ".cpp");
88
145
  }
@@ -92,29 +149,100 @@ function resolveKey(file) {
92
149
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
93
150
  }
94
151
 
152
+ function posixRel(from, file) {
153
+ return path.relative(from, file).replace(/\\/g, "/");
154
+ }
155
+
156
+ function sourcePrefixes(rel) {
157
+ const noExt = rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, "");
158
+ const noTag = noExt.replace(/\.(server|client)$/i, "");
159
+ return [...new Set([noExt, noTag, toLuauPath(rel).replace(/\\/g, "/")])];
160
+ }
161
+
162
+ function matchesPrefix(relOut, prefixes) {
163
+ const n = relOut.replace(/\\/g, "/").toLowerCase();
164
+ return prefixes.some((prefix) => {
165
+ const k = prefix.replace(/\\/g, "/").toLowerCase();
166
+ return n === k || n === `${k}.luau` || n.startsWith(`${k}/`) || n.startsWith(`${k}.`);
167
+ });
168
+ }
169
+
170
+ function removeEmptyOutDirs(outDir, projectRoot) {
171
+ if (!fs.existsSync(outDir)) {
172
+ return;
173
+ }
174
+ const walk = (dir) => {
175
+ if (resolveKey(dir) === resolveKey(outDir)) {
176
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
177
+ if (entry.isDirectory()) {
178
+ walk(path.join(dir, entry.name));
179
+ }
180
+ }
181
+ return;
182
+ }
183
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
184
+ if (entry.isDirectory()) {
185
+ walk(path.join(dir, entry.name));
186
+ }
187
+ }
188
+ if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
189
+ if (tryRm(dir)) {
190
+ console.log("cluaupp: removed", path.relative(projectRoot, dir));
191
+ }
192
+ }
193
+ };
194
+ walk(outDir);
195
+ }
196
+
197
+ function pruneOut(root, config, written, failedPrefixes) {
198
+ const outDir = path.join(root, config.outDir);
199
+ if (!fs.existsSync(outDir)) {
200
+ return;
201
+ }
202
+ for (const file of collectFiles(outDir)) {
203
+ if (written.has(resolveKey(file))) {
204
+ continue;
205
+ }
206
+ const rel = posixRel(outDir, file);
207
+ if (matchesPrefix(rel, failedPrefixes)) {
208
+ continue;
209
+ }
210
+ if (tryRm(file)) {
211
+ console.log("cluaupp: removed", path.relative(root, file));
212
+ }
213
+ }
214
+ removeEmptyOutDirs(outDir, root);
215
+ }
216
+
95
217
  function build(root, options = {}) {
96
218
  const exitOnError = options.exitOnError !== false;
219
+ const syncVendor = options.syncVendor !== false;
97
220
  const config = loadConfig(root);
98
- copyRuntime(root);
99
- copyHeaders(root);
100
- const files = collectCpp(path.join(root, config.rootDir));
221
+ if (syncVendor) {
222
+ copyRuntime(root);
223
+ copyHeaders(root);
224
+ }
225
+ const srcDir = path.join(root, config.rootDir);
226
+ const files = collectCpp(srcDir);
101
227
  if (files.length === 0) {
102
228
  console.error("no .cpp/.h/.hpp files in", config.rootDir);
229
+ pruneOut(root, config, new Set(), []);
103
230
  if (exitOnError) {
104
231
  process.exit(1);
105
232
  }
106
- return;
233
+ return { failed: 0, written: new Set() };
107
234
  }
108
235
 
109
236
  const fileSet = new Set(files.map((file) => path.resolve(file)));
110
237
  let failed = 0;
111
238
  const written = new Set();
239
+ const failedPrefixes = [];
112
240
  for (const file of files) {
113
241
  if (isHeaderFile(file) && fileSet.has(path.resolve(siblingCpp(file)))) {
114
242
  continue;
115
243
  }
116
244
  const source = fs.readFileSync(file, "utf8");
117
- const rel = path.relative(path.join(root, config.rootDir), file).replace(/\\/g, "/");
245
+ const rel = posixRel(srcDir, file);
118
246
  try {
119
247
  const result = compileService(source, rel, {
120
248
  ...config,
@@ -122,7 +250,7 @@ function build(root, options = {}) {
122
250
  relativeName: rel,
123
251
  outName: toLuauPath(rel),
124
252
  architecture: config.architecture !== false,
125
- includeDirs: [path.dirname(file), path.join(root, config.rootDir), path.join(root, "include")],
253
+ includeDirs: [path.dirname(file), srcDir, path.join(root, "include")],
126
254
  });
127
255
  for (const artifact of result.files) {
128
256
  const dest = path.join(root, config.outDir, artifact.name);
@@ -134,18 +262,22 @@ function build(root, options = {}) {
134
262
  for (const stale of result.stale || []) {
135
263
  const dest = path.join(root, config.outDir, stale);
136
264
  if (fs.existsSync(dest) && !written.has(resolveKey(dest))) {
137
- fs.unlinkSync(dest);
138
- console.log("cluaupp: removed", path.relative(root, dest));
265
+ if (tryRm(dest)) {
266
+ console.log("cluaupp: removed", path.relative(root, dest));
267
+ }
139
268
  }
140
269
  }
141
270
  } catch (err) {
142
271
  failed += 1;
272
+ failedPrefixes.push(...sourcePrefixes(rel));
143
273
  console.error(err.message);
144
274
  }
145
275
  }
276
+ pruneOut(root, config, written, failedPrefixes);
146
277
  if (failed > 0 && exitOnError) {
147
278
  process.exit(1);
148
279
  }
280
+ return { failed, written };
149
281
  }
150
282
 
151
283
  function init(dest) {
@@ -160,18 +292,39 @@ function init(dest) {
160
292
  }
161
293
 
162
294
  function watch(root) {
163
- build(root, { exitOnError: false });
295
+ build(root, { exitOnError: false, syncVendor: true });
164
296
  const config = loadConfig(root);
165
297
  const dir = path.join(root, config.rootDir);
166
- console.log("watching", dir);
167
- fs.watch(dir, { recursive: true }, (_event, filename) => {
168
- if (filename && isSourceFile(filename)) {
169
- try {
170
- build(root, { exitOnError: false });
171
- } catch (err) {
172
- console.error(err.message);
298
+ let timer = null;
299
+ let running = false;
300
+ let queued = false;
301
+
302
+ const run = () => {
303
+ if (running) {
304
+ queued = true;
305
+ return;
306
+ }
307
+ running = true;
308
+ try {
309
+ build(root, { exitOnError: false, syncVendor: false });
310
+ } catch (err) {
311
+ console.error(err.message);
312
+ } finally {
313
+ running = false;
314
+ if (queued) {
315
+ queued = false;
316
+ run();
173
317
  }
174
318
  }
319
+ };
320
+
321
+ console.log("watching", dir);
322
+ if (!fs.existsSync(dir)) {
323
+ fs.mkdirSync(dir, { recursive: true });
324
+ }
325
+ fs.watch(dir, { recursive: true }, () => {
326
+ clearTimeout(timer);
327
+ timer = setTimeout(run, 200);
175
328
  });
176
329
  }
177
330
 
@@ -190,3 +343,5 @@ if (cmd === "init") {
190
343
  } else {
191
344
  printHelp();
192
345
  }
346
+
347
+ module.exports = { build, watch };
package/src/emit.js CHANGED
@@ -31,6 +31,13 @@ function emit(ast, options = {}) {
31
31
  return `"${node.value}"`;
32
32
  case "ident":
33
33
  return node.name;
34
+ case "initlist": {
35
+ const entries = (node.fields || []).map((field) => `${field.name} = ${emitExpr(field.value)}`);
36
+ if (entries.length === 0) {
37
+ return "{}";
38
+ }
39
+ return `{ ${entries.join(", ")} }`;
40
+ }
34
41
  case "unary": {
35
42
  const inner = emitExpr(node.argument);
36
43
  if (node.op === "!") {
package/src/parse.js CHANGED
@@ -84,6 +84,28 @@ function parse(source, fileName) {
84
84
  return args;
85
85
  }
86
86
 
87
+ function parseInitList() {
88
+ eat("op", "{");
89
+ const fields = [];
90
+ while (!at("op", "}") && !at("eof")) {
91
+ if (at("op", ".")) {
92
+ i += 1;
93
+ const name = eat("ident").value;
94
+ eat("op", "=");
95
+ fields.push({ name, value: parseExpr() });
96
+ } else {
97
+ throw error("expected designated initializer .Field = value");
98
+ }
99
+ if (at("op", ",")) {
100
+ i += 1;
101
+ } else {
102
+ break;
103
+ }
104
+ }
105
+ eat("op", "}");
106
+ return { type: "initlist", fields };
107
+ }
108
+
87
109
  function parsePrimary() {
88
110
  if (at("kw", "true") || at("kw", "false")) {
89
111
  return { type: "bool", value: eat("kw").value === "true" };
@@ -120,6 +142,9 @@ function parse(source, fileName) {
120
142
  eat("op", ")");
121
143
  return expr;
122
144
  }
145
+ if (at("op", "{")) {
146
+ return parseInitList();
147
+ }
123
148
  throw error("invalid expression");
124
149
  }
125
150
 
@@ -155,6 +180,10 @@ function parse(source, fileName) {
155
180
  node = { type: "call", object: null, name: node.name, args: parseArgs(), access: "." };
156
181
  continue;
157
182
  }
183
+ if (at("op", "{") && (node.type === "ident" || node.type === "member")) {
184
+ node = parseInitList();
185
+ continue;
186
+ }
158
187
  break;
159
188
  }
160
189
  return node;
@@ -5,19 +5,23 @@
5
5
  "ServerScriptService": {
6
6
  "$className": "ServerScriptService",
7
7
  "Cluaupp": {
8
- "$path": "out/server"
8
+ "$path": "out/server",
9
+ "$optional": true
9
10
  }
10
11
  },
11
12
  "ReplicatedStorage": {
12
13
  "$className": "ReplicatedStorage",
13
14
  "Cluaupp": {
14
- "$path": "out/shared"
15
+ "$path": "out/shared",
16
+ "$optional": true
15
17
  },
16
18
  "CluauppLibs": {
17
- "$path": "libs"
19
+ "$path": "libs",
20
+ "$optional": true
18
21
  },
19
22
  "Packages": {
20
- "$path": "Packages"
23
+ "$path": "Packages",
24
+ "$optional": true
21
25
  }
22
26
  },
23
27
  "StarterPlayer": {
@@ -25,7 +29,8 @@
25
29
  "StarterPlayerScripts": {
26
30
  "$className": "StarterPlayerScripts",
27
31
  "Cluaupp": {
28
- "$path": "out/client"
32
+ "$path": "out/client",
33
+ "$optional": true
29
34
  }
30
35
  }
31
36
  },