cluaupp 0.1.1 → 0.1.4

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 (65) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +5 -8
  3. package/docs/README.md +13 -10
  4. package/docs/architecture.md +18 -11
  5. package/docs/cli.md +2 -2
  6. package/docs/config.md +1 -1
  7. package/docs/cpp-advanced.md +10 -6
  8. package/docs/cpp-organization.md +3 -1
  9. package/docs/cpp-safety.md +11 -7
  10. package/docs/cpp-types.md +1 -1
  11. package/docs/examples/_category_.json +5 -0
  12. package/docs/examples/combat.md +239 -0
  13. package/docs/examples/data-boot.md +98 -0
  14. package/docs/examples/hud.md +96 -0
  15. package/docs/examples/index.md +43 -0
  16. package/docs/examples/leaderstats.md +140 -0
  17. package/docs/examples/shop.md +122 -0
  18. package/docs/examples/sword.md +108 -0
  19. package/docs/getting-started.md +12 -6
  20. package/docs/intellisense.md +31 -0
  21. package/docs/intro.md +2 -2
  22. package/docs/libraries/_category_.json +5 -0
  23. package/docs/libraries/dataservice.md +104 -0
  24. package/docs/libraries/index.md +22 -0
  25. package/docs/libraries/janitor.md +65 -0
  26. package/docs/libraries/more.md +79 -0
  27. package/docs/libraries/net.md +35 -0
  28. package/docs/libraries/promise.md +27 -0
  29. package/docs/oop/_category_.json +5 -0
  30. package/docs/oop/file-tags.md +49 -0
  31. package/docs/oop/index.md +22 -0
  32. package/docs/oop/modules.md +86 -0
  33. package/docs/oop/services.md +83 -0
  34. package/docs/print-cout.md +91 -0
  35. package/docs/syntax.md +59 -5
  36. package/editors/vscode/extension.js +146 -0
  37. package/editors/vscode/package.json +25 -0
  38. package/include/cluaupp/libs/janitor.hpp +7 -3
  39. package/include/cluaupp/roblox.hpp +19 -0
  40. package/package.json +66 -65
  41. package/runtime/Janitor/init.luau +4 -34
  42. package/src/architecture.js +108 -48
  43. package/src/cli.js +198 -64
  44. package/src/client/init.client.cpp +5 -0
  45. package/src/compile.js +20 -4
  46. package/src/editor-install.js +218 -0
  47. package/src/emit.js +320 -8
  48. package/src/intellisense.js +1368 -0
  49. package/src/layout.js +129 -0
  50. package/src/lex.js +17 -9
  51. package/src/libs.js +95 -16
  52. package/src/lsp.js +227 -0
  53. package/src/parse.js +187 -6
  54. package/src/preprocess.js +71 -3
  55. package/src/server/leaderstats.server.cpp +28 -0
  56. package/src/shared/config.cpp +5 -0
  57. package/src/shared/config.h +3 -0
  58. package/src/understand.js +64 -6
  59. package/templates/game/.clangd +11 -0
  60. package/templates/game/.vscode/c_cpp_properties.json +6 -2
  61. package/templates/game/.vscode/extensions.json +6 -0
  62. package/templates/game/.vscode/settings.json +16 -2
  63. package/templates/game/compile_flags.txt +2 -0
  64. package/templates/game/default.project.json +5 -10
  65. package/docs/libraries.md +0 -80
@@ -3,6 +3,7 @@
3
3
  const path = require("path");
4
4
  const { emit } = require("./emit");
5
5
  const { collectLibraries, insertRequires, requireCluauppLib } = require("./libs");
6
+ const { organizeDecls, janitorNames, emitSection, joinBlocks, SECTION, ENTRY_FN } = require("./layout");
6
7
  const {
7
8
  VALUE_CLASSES,
8
9
  analyze,
@@ -27,9 +28,9 @@ function cluauppLib(name) {
27
28
 
28
29
  function emitTypes(plan) {
29
30
  const lines = [header(plan).trimEnd(), ""];
30
- lines.push('local ReplicatedStorage = game:GetService("ReplicatedStorage")');
31
- lines.push(`local Occlude = ${cluauppLib("Occlude")}`);
32
- lines.push(`local ArrayIndexer = ${cluauppLib("ArrayIndexer")}`);
31
+ lines.push('const ReplicatedStorage = game:GetService("ReplicatedStorage")');
32
+ lines.push(`const Occlude = ${cluauppLib("Occlude")}`);
33
+ lines.push(`const ArrayIndexer = ${cluauppLib("ArrayIndexer")}`);
33
34
  lines.push("");
34
35
 
35
36
  if (plan.stats.length > 0) {
@@ -60,6 +61,7 @@ function emitTypes(plan) {
60
61
  manifest.push(" Get: (player: Player) -> PlayerCache?,");
61
62
  manifest.push(" Ensure: (player: Player) -> PlayerCache,");
62
63
  manifest.push(" Clear: (player: Player) -> (),");
64
+ manifest.push(" ClearAll: () -> (),");
63
65
  manifest.push(" },");
64
66
  }
65
67
  if (plan.roles.players) {
@@ -100,23 +102,26 @@ function emitTypes(plan) {
100
102
  }
101
103
 
102
104
  function emitPlayersManager(plan) {
103
- return `${header(plan)}
104
- local Players = game:GetService("Players")
105
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
106
- local Janitor = ${cluauppLib("Janitor")}
107
- type Janitor = Janitor.Janitor
105
+ return `${header(plan).trimEnd()}
106
+
107
+ const Players = game:GetService("Players")
108
+ const ReplicatedStorage = game:GetService("ReplicatedStorage")
109
+ const Janitor = ${cluauppLib("Janitor")}
108
110
 
111
+ type Janitor = Janitor.Janitor
109
112
  export type PlayerHandler = (player: Player) -> ()
110
113
 
111
114
  local PlayersManager = {}
112
115
  local lifetime: Janitor? = nil
116
+ local leaveHandler: PlayerHandler? = nil
113
117
 
114
118
  function PlayersManager.Start(onPlayer: PlayerHandler, onLeave: PlayerHandler?)
115
119
  PlayersManager.Stop()
116
120
  local janitor = Janitor.new()
117
121
  lifetime = janitor
122
+ leaveHandler = onLeave
118
123
 
119
- local function accept(player: Player)
124
+ const function accept(player: Player)
120
125
  onPlayer(player)
121
126
  end
122
127
 
@@ -134,6 +139,13 @@ function PlayersManager.Stop()
134
139
  if lifetime == nil then
135
140
  return
136
141
  end
142
+ local onLeave = leaveHandler
143
+ leaveHandler = nil
144
+ if onLeave then
145
+ for _, player in Players:GetPlayers() do
146
+ onLeave(player)
147
+ end
148
+ end
137
149
  lifetime:Cleanup()
138
150
  lifetime = nil
139
151
  end
@@ -146,11 +158,11 @@ function emitCacheController(plan) {
146
158
  const types = plan.typesName;
147
159
  const folder = plan.folderName || "leaderstats";
148
160
  const lines = [header(plan).trimEnd(), ""];
149
- lines.push(`local Types = require(script.Parent.${types})`);
161
+ lines.push(`const Types = require(script.Parent.${types})`);
150
162
  lines.push("");
151
- lines.push(`local FOLDER_NAME = "${folder}"`);
152
- lines.push("local cache: { [Player]: Types.PlayerCache } = {}");
163
+ lines.push(`const FOLDER_NAME = "${folder}"`);
153
164
  lines.push("");
165
+ lines.push("local cache: { [Player]: Types.PlayerCache } = {}");
154
166
  lines.push("local CacheController = {}");
155
167
  lines.push("");
156
168
  lines.push("function CacheController.Get(player: Player): Types.PlayerCache?");
@@ -201,6 +213,10 @@ function emitCacheController(plan) {
201
213
  lines.push(" cache[player] = nil");
202
214
  lines.push("end");
203
215
  lines.push("");
216
+ lines.push("function CacheController.ClearAll()");
217
+ lines.push(" table.clear(cache)");
218
+ lines.push("end");
219
+ lines.push("");
204
220
  lines.push("return CacheController");
205
221
  lines.push("");
206
222
  return lines.join("\n");
@@ -209,13 +225,13 @@ function emitCacheController(plan) {
209
225
  function emitMain(plan) {
210
226
  const lines = [header(plan).trimEnd(), ""];
211
227
  if (plan.roles.players) {
212
- lines.push("local PlayersManager = require(script.Parent.PlayersManager)");
228
+ lines.push("const PlayersManager = require(script.Parent.PlayersManager)");
213
229
  }
214
230
  if (plan.roles.cache) {
215
- lines.push("local CacheController = require(script.Parent.CacheController)");
231
+ lines.push("const CacheController = require(script.Parent.CacheController)");
216
232
  }
217
233
  if (plan.roles.domain) {
218
- lines.push(`local ${plan.roles.domain} = require(script.Parent.${plan.roles.domain})`);
234
+ lines.push(`const ${plan.roles.domain} = require(script.Parent.${plan.roles.domain})`);
219
235
  }
220
236
  lines.push("");
221
237
  lines.push("local Main = {}");
@@ -242,9 +258,15 @@ function emitMain(plan) {
242
258
  if (plan.roles.domain) {
243
259
  lines.push(` ${plan.roles.domain}.Stop()`);
244
260
  }
261
+ if (plan.roles.cache) {
262
+ lines.push(" CacheController.ClearAll()");
263
+ }
245
264
  if (plan.roles.players) {
246
265
  lines.push(" PlayersManager.Stop()");
247
266
  }
267
+ if (!plan.roles.domain && !plan.roles.cache && !plan.roles.players) {
268
+ lines.push(" return");
269
+ }
248
270
  lines.push("end");
249
271
  lines.push("");
250
272
  lines.push("return Main");
@@ -258,6 +280,10 @@ function emitBootstrap(plan) {
258
280
  }
259
281
 
260
282
  function emitConfig(plan, ast, options) {
283
+ const owned = (ast.body || []).some((decl) => decl.owner);
284
+ if (owned) {
285
+ return `${header(plan).trimEnd()}\n\n${emit(ast, { ...options, skipHeader: true, skipInit: true }).trimEnd()}\n`;
286
+ }
261
287
  const lines = [];
262
288
  if (plan.onlyConsts) {
263
289
  lines.push(header(plan).trimEnd());
@@ -282,48 +308,54 @@ function emitConfig(plan, ast, options) {
282
308
  }
283
309
  if ((ast.body || []).some((decl) => decl.type === "function" && decl.name === "init")) {
284
310
  lines.push(" Start = init,");
311
+ lines.push(" Init = init,");
312
+ lines.push(" init = init,");
285
313
  }
286
314
  lines.push("}");
287
315
  lines.push("");
288
316
  return lines.join("\n");
289
317
  }
290
318
 
319
+ function emitDecls(decls, options) {
320
+ if (!decls || decls.length === 0) {
321
+ return "";
322
+ }
323
+ return emit({ type: "program", body: decls }, { ...options, skipHeader: true, skipInit: true }).trimEnd();
324
+ }
325
+
291
326
  function emitDomainController(plan, ast, options) {
292
- const absorbCache = plan.roles.cache;
293
327
  const keep = (ast.body || []).filter((decl) => {
294
- if (decl.type === "decl" || decl.type === "proto") {
295
- return true;
296
- }
297
- if (decl.type !== "function") {
298
- return false;
299
- }
300
- if (decl.name === "init") {
301
- return true;
302
- }
303
- const classified = plan.functions.find((item) => item.name === decl.name);
304
- if (absorbCache && classified && classified.absorb === "cache") {
305
- return false;
306
- }
307
- return true;
328
+ return decl.type === "decl" || decl.type === "proto" || decl.type === "function" || decl.type === "expr";
308
329
  });
330
+ const groups = organizeDecls(keep);
309
331
  const subset = { type: "program", body: keep };
310
- const code = emit(subset, { ...options, skipHeader: true, skipInit: true }).trimEnd();
311
332
  const name = plan.roles.domain;
312
333
  const libs = collectLibraries(options.source || "", subset);
313
- const body = `${header(plan)}${code}
314
-
315
- local ${name} = {}
316
-
317
- function ${name}.Start()
318
- ${keep.some((decl) => decl.type === "function" && decl.name === "init") ? "init()" : "return"}
319
- end
320
-
321
- function ${name}.Stop()
322
- return
323
- end
324
-
325
- return ${name}
326
- `;
334
+ const entry = keep.find((decl) => decl.type === "function" && ENTRY_FN.test(decl.name || ""));
335
+ const bootExprs = keep.filter((decl) => decl.type === "expr");
336
+ const janitors = janitorNames(keep);
337
+ const closer = keep.find((decl) => decl.type === "function" && /^(OnClose|Cleanup|Shutdown)$/i.test(decl.name));
338
+ const stopLines = [];
339
+ if (janitors.length > 0) {
340
+ for (const janitor of janitors) {
341
+ stopLines.push(` ${janitor}:Cleanup()`);
342
+ }
343
+ } else if (closer) {
344
+ stopLines.push(` ${closer.name}()`);
345
+ } else {
346
+ stopLines.push(" return");
347
+ }
348
+ const boot = entry
349
+ ? ` ${entry.name}()`
350
+ : bootExprs.length
351
+ ? emitDecls(bootExprs, options)
352
+ .split("\n")
353
+ .map((line) => (line ? `\t${line}` : line))
354
+ .join("\n")
355
+ : " return";
356
+ const startFn = `function ${name}.Start()\n ${name}.Stop()\n${boot}\nend`;
357
+ const stopFn = `function ${name}.Stop()\n${stopLines.join("\n")}\nend`;
358
+ 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}`)}`;
327
359
  return insertRequires(body, libs);
328
360
  }
329
361
 
@@ -331,10 +363,34 @@ function emitModule(plan, ast, options) {
331
363
  return emitConfig(plan, ast, options);
332
364
  }
333
365
 
366
+ function emitScriptMeta(runContext) {
367
+ return `${JSON.stringify(
368
+ {
369
+ className: "Script",
370
+ properties: {
371
+ RunContext: `Enum.RunContext.${runContext}`,
372
+ },
373
+ },
374
+ null,
375
+ "\t",
376
+ )}\n`;
377
+ }
378
+
379
+ function serviceBootName(plan) {
380
+ if (plan.isClient) {
381
+ return "init.client.luau";
382
+ }
383
+ return "init.luau";
384
+ }
385
+
334
386
  function serviceFiles(plan, ast, options, dir) {
335
387
  const folder = `${dir}/${plan.serviceName}`;
336
- const boot = plan.isClient ? "init.client.luau" : "init.server.luau";
337
- const files = [{ name: `${folder}/${boot}`, contents: emitBootstrap(plan) }, { name: `${folder}/Main.luau`, contents: emitMain(plan) }];
388
+ const boot = serviceBootName(plan);
389
+ const files = [{ name: `${folder}/${boot}`, contents: emitBootstrap(plan) }];
390
+ if (!plan.isClient && plan.runContext) {
391
+ files.push({ name: `${folder}/init.meta.json`, contents: emitScriptMeta(plan.runContext) });
392
+ }
393
+ files.push({ name: `${folder}/Main.luau`, contents: emitMain(plan) });
338
394
  if (plan.roles.players) {
339
395
  files.push({ name: `${folder}/PlayersManager.luau`, contents: emitPlayersManager(plan) });
340
396
  }
@@ -365,11 +421,15 @@ function planOutput(ast, fileName, options = {}) {
365
421
 
366
422
  if (plan.kind === "service") {
367
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`];
368
428
  return {
369
429
  kind: "service",
370
430
  plan,
371
431
  files: serviceFiles(plan, ast, options, serviceDir),
372
- stale: [rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau")],
432
+ stale,
373
433
  };
374
434
  }
375
435
 
package/src/cli.js CHANGED
@@ -3,7 +3,8 @@
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { compileService } = require("./compile");
6
- const { isSourceFile, isHeaderFile, toLuauPath } = require("./preprocess");
6
+ const { isSourceFile, isHeaderFile, toLuauPath, siblingImplementation } = require("./preprocess");
7
+ const { syncEditorSupport, installEditorSupport } = require("./intellisense");
7
8
  const pkg = require("../package.json");
8
9
 
9
10
  function printHelp() {
@@ -13,6 +14,8 @@ Usage:
13
14
  cluaupp init [folder] create a game (src/server, src/client, src/shared)
14
15
  cluaupp build [folder] transpile src → out (PascalCase services)
15
16
  cluaupp watch [folder] rebuild on save
17
+ cluaupp lsp [folder] language server (stdio JSON-RPC)
18
+ cluaupp intellisense install Cursor/VS Code IntelliSense
16
19
  cluaupp --version print version
17
20
  cluaupp --help this help
18
21
 
@@ -47,8 +50,11 @@ function tryRm(target) {
47
50
  }
48
51
  }
49
52
 
50
- function copyFileIfChanged(src, dest) {
51
- if (fs.existsSync(dest)) {
53
+ function copyFileIfChanged(src, dest, mode = "fill") {
54
+ if (mode === "fill" && fs.existsSync(dest)) {
55
+ return;
56
+ }
57
+ if (mode === "update" && fs.existsSync(dest)) {
52
58
  const from = fs.statSync(src);
53
59
  const to = fs.statSync(dest);
54
60
  if (from.size === to.size && from.mtimeMs <= to.mtimeMs) {
@@ -67,7 +73,7 @@ function copyFileIfChanged(src, dest) {
67
73
  }
68
74
  }
69
75
 
70
- function syncDir(from, to) {
76
+ function syncDir(from, to, mode = "fill") {
71
77
  if (!fs.existsSync(from)) {
72
78
  return;
73
79
  }
@@ -76,9 +82,9 @@ function syncDir(from, to) {
76
82
  const src = path.join(from, entry.name);
77
83
  const dest = path.join(to, entry.name);
78
84
  if (entry.isDirectory()) {
79
- syncDir(src, dest);
85
+ syncDir(src, dest, mode);
80
86
  } else {
81
- copyFileIfChanged(src, dest);
87
+ copyFileIfChanged(src, dest, mode);
82
88
  }
83
89
  }
84
90
  }
@@ -141,7 +147,7 @@ function collectFiles(dir, files = []) {
141
147
  }
142
148
 
143
149
  function siblingCpp(file) {
144
- return file.replace(/\.(h|hpp|hh)$/i, ".cpp");
150
+ return siblingImplementation(file) || file.replace(/\.(h|hpp|hh)$/i, ".cpp");
145
151
  }
146
152
 
147
153
  function resolveKey(file) {
@@ -214,29 +220,13 @@ function pruneOut(root, config, written, failedPrefixes) {
214
220
  removeEmptyOutDirs(outDir, root);
215
221
  }
216
222
 
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
- }
223
+ function compileProject(root, config) {
225
224
  const srcDir = path.join(root, config.rootDir);
226
225
  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
226
  const fileSet = new Set(files.map((file) => path.resolve(file)));
237
- let failed = 0;
238
- const written = new Set();
239
- const failedPrefixes = [];
227
+ const jobs = [];
228
+ const errors = [];
229
+
240
230
  for (const file of files) {
241
231
  if (isHeaderFile(file) && fileSet.has(path.resolve(siblingCpp(file)))) {
242
232
  continue;
@@ -251,48 +241,166 @@ function build(root, options = {}) {
251
241
  outName: toLuauPath(rel),
252
242
  architecture: config.architecture !== false,
253
243
  includeDirs: [path.dirname(file), srcDir, path.join(root, "include")],
244
+ srcDir,
254
245
  });
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));
246
+ jobs.push({ rel, files: result.files, stale: result.stale || [] });
247
+ } catch (err) {
248
+ errors.push({ rel, prefixes: sourcePrefixes(rel), message: err.message });
249
+ }
250
+ }
251
+
252
+ return { files, jobs, errors };
253
+ }
254
+
255
+ function writeTextIfChanged(dest, contents) {
256
+ if (fs.existsSync(dest) && fs.readFileSync(dest, "utf8") === contents) {
257
+ return false;
258
+ }
259
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
260
+ fs.writeFileSync(dest, contents, "utf8");
261
+ return true;
262
+ }
263
+
264
+ function writeJobs(root, config, jobs) {
265
+ const written = new Set();
266
+ for (const job of jobs) {
267
+ for (const artifact of job.files) {
268
+ const dest = path.join(root, config.outDir, artifact.name);
269
+ const changed = writeTextIfChanged(dest, artifact.contents);
270
+ written.add(resolveKey(dest));
271
+ if (changed) {
272
+ console.log("cluaupp:", job.rel, "→", path.relative(root, dest));
261
273
  }
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
- }
274
+ }
275
+ for (const stale of job.stale) {
276
+ const dest = path.join(root, config.outDir, stale);
277
+ if (fs.existsSync(dest) && !written.has(resolveKey(dest))) {
278
+ if (tryRm(dest)) {
279
+ console.log("cluaupp: removed", path.relative(root, dest));
268
280
  }
269
281
  }
270
- } catch (err) {
271
- failed += 1;
272
- failedPrefixes.push(...sourcePrefixes(rel));
282
+ }
283
+ }
284
+ return written;
285
+ }
286
+
287
+ function libsPresent(root) {
288
+ const libs = path.join(root, "libs");
289
+ return fs.existsSync(libs) && fs.readdirSync(libs).length > 0;
290
+ }
291
+
292
+ function ensureVendor(root) {
293
+ copyRuntime(root);
294
+ copyHeaders(root);
295
+ }
296
+
297
+ function build(root, options = {}) {
298
+ const exitOnError = options.exitOnError !== false;
299
+ const holdOnError = options.holdOnError === true;
300
+ const config = loadConfig(root);
301
+ if (options.syncVendor !== false) {
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
+ try {
324
+ syncEditorSupport(root, config);
325
+ } catch (err) {
326
+ console.error("cluaupp: intellisense sync failed", err.message);
327
+ }
328
+ return { failed: compiled.errors.length, written: new Set() };
329
+ }
275
330
  }
331
+
332
+ const written = writeJobs(root, config, compiled.jobs);
333
+ const failedPrefixes = compiled.errors.flatMap((err) => err.prefixes);
276
334
  pruneOut(root, config, written, failedPrefixes);
277
- if (failed > 0 && exitOnError) {
335
+ try {
336
+ syncEditorSupport(root, config);
337
+ } catch (err) {
338
+ console.error("cluaupp: intellisense sync failed", err.message);
339
+ }
340
+ if (compiled.errors.length > 0 && exitOnError) {
278
341
  process.exit(1);
279
342
  }
280
- return { failed, written };
343
+ return { failed: compiled.errors.length, written };
281
344
  }
282
345
 
283
- function init(dest) {
346
+ async function init(dest) {
284
347
  const template = path.join(__dirname, "..", "templates", "game");
285
348
  const include = path.join(__dirname, "..", "include");
286
349
  copyDir(template, dest);
287
350
  copyDir(include, path.join(dest, "include"));
288
351
  copyRuntime(dest);
352
+ syncEditorSupport(dest);
353
+ const installed = await installEditorSupport();
289
354
  console.log("Cluaupp ready in", dest);
290
355
  console.log(" cluaupp build");
291
356
  console.log(" rojo serve");
357
+ if (installed.local.length || (installed.cpp && installed.cpp.status === "installed")) {
358
+ console.log(" reload Cursor (Ctrl+Shift+P → Developer: Reload Window)");
359
+ }
360
+ }
361
+
362
+ function pidAlive(pid) {
363
+ try {
364
+ process.kill(pid, 0);
365
+ return true;
366
+ } catch {
367
+ return false;
368
+ }
369
+ }
370
+
371
+ function acquireWatchLock(root) {
372
+ const file = path.join(root, ".cluaupp-watch.lock");
373
+ if (fs.existsSync(file)) {
374
+ const pid = Number(fs.readFileSync(file, "utf8").trim());
375
+ if (Number.isFinite(pid) && pid !== process.pid && pidAlive(pid)) {
376
+ console.error(`cluaupp: watch already running (pid ${pid}). Stop it first — two watchers make Rojo crash on libs/.`);
377
+ process.exit(1);
378
+ }
379
+ }
380
+ fs.writeFileSync(file, String(process.pid), "utf8");
381
+ const release = () => {
382
+ try {
383
+ if (fs.existsSync(file) && fs.readFileSync(file, "utf8").trim() === String(process.pid)) {
384
+ fs.unlinkSync(file);
385
+ }
386
+ } catch {
387
+ // ignore
388
+ }
389
+ };
390
+ process.on("exit", release);
391
+ process.on("SIGINT", () => {
392
+ release();
393
+ process.exit(0);
394
+ });
395
+ process.on("SIGTERM", () => {
396
+ release();
397
+ process.exit(0);
398
+ });
292
399
  }
293
400
 
294
401
  function watch(root) {
295
- build(root, { exitOnError: false, syncVendor: true });
402
+ acquireWatchLock(root);
403
+ build(root, { exitOnError: false, syncVendor: false, holdOnError: true });
296
404
  const config = loadConfig(root);
297
405
  const dir = path.join(root, config.rootDir);
298
406
  let timer = null;
@@ -306,7 +414,7 @@ function watch(root) {
306
414
  }
307
415
  running = true;
308
416
  try {
309
- build(root, { exitOnError: false, syncVendor: false });
417
+ build(root, { exitOnError: false, syncVendor: false, holdOnError: true });
310
418
  } catch (err) {
311
419
  console.error(err.message);
312
420
  } finally {
@@ -318,30 +426,56 @@ function watch(root) {
318
426
  }
319
427
  };
320
428
 
321
- console.log("watching", dir);
429
+ console.log("cluaupp", pkg.version, "watching", dir, "(libs untouched)");
322
430
  if (!fs.existsSync(dir)) {
323
431
  fs.mkdirSync(dir, { recursive: true });
324
432
  }
325
433
  fs.watch(dir, { recursive: true }, () => {
326
434
  clearTimeout(timer);
327
- timer = setTimeout(run, 200);
435
+ timer = setTimeout(run, 400);
328
436
  });
329
437
  }
330
438
 
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();
439
+ function dispatch(args) {
440
+ const cmd = args[0] || "help";
441
+ const cwd = process.cwd();
442
+ if (cmd === "init") {
443
+ return init(path.resolve(cwd, args[1] || ".")).catch((err) => {
444
+ console.error(err.message || err);
445
+ process.exit(1);
446
+ });
447
+ } else if (cmd === "build") {
448
+ build(path.resolve(cwd, args[1] || "."));
449
+ } else if (cmd === "watch") {
450
+ watch(path.resolve(cwd, args[1] || "."));
451
+ } else if (cmd === "lsp") {
452
+ const { start } = require("./lsp");
453
+ start({ projectRoot: path.resolve(cwd, args[1] || ".") });
454
+ } else if (cmd === "intellisense" || cmd === "intelisense") {
455
+ const root = path.resolve(cwd, args[1] || ".");
456
+ syncEditorSupport(root, loadConfig(root));
457
+ return installEditorSupport().then((installed) => {
458
+ console.log("cluaupp: compile_commands.json, .clangd, and .vscode updated in", root);
459
+ if (installed.local.length) {
460
+ for (const dest of installed.local) {
461
+ console.log("cluaupp: Cluaupp IntelliSense", dest);
462
+ }
463
+ }
464
+ }).catch((err) => {
465
+ console.error(err.message || err);
466
+ process.exit(1);
467
+ });
468
+ } else if (cmd === "--version" || cmd === "-v" || cmd === "version") {
469
+ console.log(pkg.version);
470
+ } else {
471
+ printHelp();
472
+ }
473
+ }
474
+
475
+ const launchedAsCli =
476
+ require.main && ["cli.js", "cluaupp.js", "cluau.js"].includes(path.basename(require.main.filename));
477
+ if (launchedAsCli) {
478
+ dispatch(process.argv.slice(2));
345
479
  }
346
480
 
347
- module.exports = { build, watch };
481
+ module.exports = { build, watch, dispatch };
@@ -0,0 +1,5 @@
1
+ #include <cluaupp/roblox.hpp>
2
+
3
+ void init() {
4
+ print("Cluaupp client ok");
5
+ }