cluaupp 0.1.1 → 0.1.2

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,10 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2
4
+
5
+ - Watch does not write `out/` while any file fails to compile (Studio keeps the last good scripts).
6
+ - Watch/build skip rewriting Luau whose contents did not change (a space in C++ no longer floods Rojo).
7
+ - `libs/` is fill-only: missing files are restored, existing files are never overwritten or deleted. Fixes Rojo 7 crashing on `libs/ArrayIndexer` when watch/build raced a live serve.
8
+ - Dropped `$optional` from `default.project.json` (Rojo 7.7 failed to deserialize it).
9
+
3
10
  ## 0.1.1
4
11
 
5
12
  - DataService: `DataServiceOptions<T>.Template` is the required player-data table (`T` matches the save struct). Also documents `Exclude`.
6
13
  - 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`.
14
+ - 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 files are only created if missing; existing `libs/` is never overwritten on watch/build. Watch skips writing `out/` when compile fails, and skips rewriting Luau whose contents did not change.
8
15
 
9
16
  ## 0.1.0
10
17
 
package/docs/cli.md CHANGED
@@ -41,7 +41,7 @@ cluaupp build ./my-game
41
41
 
42
42
  ## `cluaupp watch [folder]`
43
43
 
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.
44
+ Runs a `build` and rebuilds when anything under `src/` changes, including deletes. Parse errors are printed; the watcher stays alive and **does not write `out/`** until the project compiles cleanly (Studio keeps the last good scripts). Rebuilds are debounced and do **not** recopy `libs/`. Deleted `.cpp` files prune their `out/` artifacts on the next successful compile.
45
45
 
46
46
  ## `cluaupp --version` / `cluaupp -v`
47
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cluaupp",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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 && node test/dataservice.test.js && node test/prune.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 && node test/hold.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
@@ -47,8 +47,11 @@ function tryRm(target) {
47
47
  }
48
48
  }
49
49
 
50
- function copyFileIfChanged(src, dest) {
51
- if (fs.existsSync(dest)) {
50
+ function copyFileIfChanged(src, dest, mode = "fill") {
51
+ if (mode === "fill" && fs.existsSync(dest)) {
52
+ return;
53
+ }
54
+ if (mode === "update" && fs.existsSync(dest)) {
52
55
  const from = fs.statSync(src);
53
56
  const to = fs.statSync(dest);
54
57
  if (from.size === to.size && from.mtimeMs <= to.mtimeMs) {
@@ -67,7 +70,7 @@ function copyFileIfChanged(src, dest) {
67
70
  }
68
71
  }
69
72
 
70
- function syncDir(from, to) {
73
+ function syncDir(from, to, mode = "fill") {
71
74
  if (!fs.existsSync(from)) {
72
75
  return;
73
76
  }
@@ -76,9 +79,9 @@ function syncDir(from, to) {
76
79
  const src = path.join(from, entry.name);
77
80
  const dest = path.join(to, entry.name);
78
81
  if (entry.isDirectory()) {
79
- syncDir(src, dest);
82
+ syncDir(src, dest, mode);
80
83
  } else {
81
- copyFileIfChanged(src, dest);
84
+ copyFileIfChanged(src, dest, mode);
82
85
  }
83
86
  }
84
87
  }
@@ -214,29 +217,13 @@ function pruneOut(root, config, written, failedPrefixes) {
214
217
  removeEmptyOutDirs(outDir, root);
215
218
  }
216
219
 
217
- function build(root, options = {}) {
218
- const exitOnError = options.exitOnError !== false;
219
- const syncVendor = options.syncVendor !== false;
220
- const config = loadConfig(root);
221
- if (syncVendor) {
222
- copyRuntime(root);
223
- copyHeaders(root);
224
- }
220
+ function compileProject(root, config) {
225
221
  const srcDir = path.join(root, config.rootDir);
226
222
  const files = collectCpp(srcDir);
227
- if (files.length === 0) {
228
- console.error("no .cpp/.h/.hpp files in", config.rootDir);
229
- pruneOut(root, config, new Set(), []);
230
- if (exitOnError) {
231
- process.exit(1);
232
- }
233
- return { failed: 0, written: new Set() };
234
- }
235
-
236
223
  const fileSet = new Set(files.map((file) => path.resolve(file)));
237
- let failed = 0;
238
- const written = new Set();
239
- const failedPrefixes = [];
224
+ const jobs = [];
225
+ const errors = [];
226
+
240
227
  for (const file of files) {
241
228
  if (isHeaderFile(file) && fileSet.has(path.resolve(siblingCpp(file)))) {
242
229
  continue;
@@ -252,32 +239,98 @@ function build(root, options = {}) {
252
239
  architecture: config.architecture !== false,
253
240
  includeDirs: [path.dirname(file), srcDir, path.join(root, "include")],
254
241
  });
255
- for (const artifact of result.files) {
256
- const dest = path.join(root, config.outDir, artifact.name);
257
- fs.mkdirSync(path.dirname(dest), { recursive: true });
258
- fs.writeFileSync(dest, artifact.contents, "utf8");
259
- written.add(resolveKey(dest));
260
- console.log("cluaupp:", rel, "→", path.relative(root, dest));
242
+ jobs.push({ rel, files: result.files, stale: result.stale || [] });
243
+ } catch (err) {
244
+ errors.push({ rel, prefixes: sourcePrefixes(rel), message: err.message });
245
+ }
246
+ }
247
+
248
+ return { files, jobs, errors };
249
+ }
250
+
251
+ function writeTextIfChanged(dest, contents) {
252
+ if (fs.existsSync(dest) && fs.readFileSync(dest, "utf8") === contents) {
253
+ return false;
254
+ }
255
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
256
+ fs.writeFileSync(dest, contents, "utf8");
257
+ return true;
258
+ }
259
+
260
+ function writeJobs(root, config, jobs) {
261
+ const written = new Set();
262
+ for (const job of jobs) {
263
+ for (const artifact of job.files) {
264
+ const dest = path.join(root, config.outDir, artifact.name);
265
+ const changed = writeTextIfChanged(dest, artifact.contents);
266
+ written.add(resolveKey(dest));
267
+ if (changed) {
268
+ console.log("cluaupp:", job.rel, "→", path.relative(root, dest));
261
269
  }
262
- for (const stale of result.stale || []) {
263
- const dest = path.join(root, config.outDir, stale);
264
- if (fs.existsSync(dest) && !written.has(resolveKey(dest))) {
265
- if (tryRm(dest)) {
266
- console.log("cluaupp: removed", path.relative(root, dest));
267
- }
270
+ }
271
+ for (const stale of job.stale) {
272
+ const dest = path.join(root, config.outDir, stale);
273
+ if (fs.existsSync(dest) && !written.has(resolveKey(dest))) {
274
+ if (tryRm(dest)) {
275
+ console.log("cluaupp: removed", path.relative(root, dest));
268
276
  }
269
277
  }
270
- } catch (err) {
271
- failed += 1;
272
- failedPrefixes.push(...sourcePrefixes(rel));
278
+ }
279
+ }
280
+ return written;
281
+ }
282
+
283
+ function libsPresent(root) {
284
+ const libs = path.join(root, "libs");
285
+ return fs.existsSync(libs) && fs.readdirSync(libs).length > 0;
286
+ }
287
+
288
+ function ensureVendor(root) {
289
+ copyRuntime(root);
290
+ copyHeaders(root);
291
+ }
292
+
293
+ function build(root, options = {}) {
294
+ const exitOnError = options.exitOnError !== false;
295
+ const holdOnError = options.holdOnError === true;
296
+ const syncVendor = options.syncVendor === true;
297
+ const config = loadConfig(root);
298
+ if (syncVendor) {
299
+ copyRuntime(root);
300
+ copyHeaders(root);
301
+ } else {
302
+ ensureVendor(root);
303
+ }
304
+ const srcDir = path.join(root, config.rootDir);
305
+ if (!fs.existsSync(srcDir) || collectCpp(srcDir).length === 0) {
306
+ console.error("no .cpp/.h/.hpp files in", config.rootDir);
307
+ if (!holdOnError) {
308
+ pruneOut(root, config, new Set(), []);
309
+ }
310
+ if (exitOnError) {
311
+ process.exit(1);
312
+ }
313
+ return { failed: 0, written: new Set() };
314
+ }
315
+
316
+ const compiled = compileProject(root, config);
317
+ if (compiled.errors.length > 0) {
318
+ for (const err of compiled.errors) {
273
319
  console.error(err.message);
274
320
  }
321
+ if (holdOnError) {
322
+ console.error(`cluaupp: out not updated (${compiled.errors.length} compile error${compiled.errors.length === 1 ? "" : "s"})`);
323
+ return { failed: compiled.errors.length, written: new Set() };
324
+ }
275
325
  }
326
+
327
+ const written = writeJobs(root, config, compiled.jobs);
328
+ const failedPrefixes = compiled.errors.flatMap((err) => err.prefixes);
276
329
  pruneOut(root, config, written, failedPrefixes);
277
- if (failed > 0 && exitOnError) {
330
+ if (compiled.errors.length > 0 && exitOnError) {
278
331
  process.exit(1);
279
332
  }
280
- return { failed, written };
333
+ return { failed: compiled.errors.length, written };
281
334
  }
282
335
 
283
336
  function init(dest) {
@@ -292,7 +345,7 @@ function init(dest) {
292
345
  }
293
346
 
294
347
  function watch(root) {
295
- build(root, { exitOnError: false, syncVendor: true });
348
+ build(root, { exitOnError: false, syncVendor: false, holdOnError: true });
296
349
  const config = loadConfig(root);
297
350
  const dir = path.join(root, config.rootDir);
298
351
  let timer = null;
@@ -306,7 +359,7 @@ function watch(root) {
306
359
  }
307
360
  running = true;
308
361
  try {
309
- build(root, { exitOnError: false, syncVendor: false });
362
+ build(root, { exitOnError: false, syncVendor: false, holdOnError: true });
310
363
  } catch (err) {
311
364
  console.error(err.message);
312
365
  } finally {
@@ -324,24 +377,30 @@ function watch(root) {
324
377
  }
325
378
  fs.watch(dir, { recursive: true }, () => {
326
379
  clearTimeout(timer);
327
- timer = setTimeout(run, 200);
380
+ timer = setTimeout(run, 400);
328
381
  });
329
382
  }
330
383
 
331
- const args = process.argv.slice(2);
332
- const cmd = args[0] || "help";
333
- const cwd = process.cwd();
334
-
335
- if (cmd === "init") {
336
- init(path.resolve(cwd, args[1] || "."));
337
- } else if (cmd === "build") {
338
- build(path.resolve(cwd, args[1] || "."));
339
- } else if (cmd === "watch") {
340
- watch(path.resolve(cwd, args[1] || "."));
341
- } else if (cmd === "--version" || cmd === "-v" || cmd === "version") {
342
- console.log(pkg.version);
343
- } else {
344
- printHelp();
384
+ function dispatch(args) {
385
+ const cmd = args[0] || "help";
386
+ const cwd = process.cwd();
387
+ if (cmd === "init") {
388
+ init(path.resolve(cwd, args[1] || "."));
389
+ } else if (cmd === "build") {
390
+ build(path.resolve(cwd, args[1] || "."));
391
+ } else if (cmd === "watch") {
392
+ watch(path.resolve(cwd, args[1] || "."));
393
+ } else if (cmd === "--version" || cmd === "-v" || cmd === "version") {
394
+ console.log(pkg.version);
395
+ } else {
396
+ printHelp();
397
+ }
398
+ }
399
+
400
+ const launchedAsCli =
401
+ require.main && ["cli.js", "cluaupp.js", "cluau.js"].includes(path.basename(require.main.filename));
402
+ if (launchedAsCli) {
403
+ dispatch(process.argv.slice(2));
345
404
  }
346
405
 
347
- module.exports = { build, watch };
406
+ module.exports = { build, watch, dispatch };
@@ -5,23 +5,19 @@
5
5
  "ServerScriptService": {
6
6
  "$className": "ServerScriptService",
7
7
  "Cluaupp": {
8
- "$path": "out/server",
9
- "$optional": true
8
+ "$path": "out/server"
10
9
  }
11
10
  },
12
11
  "ReplicatedStorage": {
13
12
  "$className": "ReplicatedStorage",
14
13
  "Cluaupp": {
15
- "$path": "out/shared",
16
- "$optional": true
14
+ "$path": "out/shared"
17
15
  },
18
16
  "CluauppLibs": {
19
- "$path": "libs",
20
- "$optional": true
17
+ "$path": "libs"
21
18
  },
22
19
  "Packages": {
23
- "$path": "Packages",
24
- "$optional": true
20
+ "$path": "Packages"
25
21
  }
26
22
  },
27
23
  "StarterPlayer": {
@@ -29,8 +25,7 @@
29
25
  "StarterPlayerScripts": {
30
26
  "$className": "StarterPlayerScripts",
31
27
  "Cluaupp": {
32
- "$path": "out/client",
33
- "$optional": true
28
+ "$path": "out/client"
34
29
  }
35
30
  }
36
31
  },