cluaupp 0.1.2 → 0.1.5

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 (69) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +10 -13
  3. package/docs/README.md +13 -10
  4. package/docs/architecture.md +30 -73
  5. package/docs/cli.md +5 -6
  6. package/docs/comparison.md +3 -3
  7. package/docs/config.md +6 -6
  8. package/docs/cpp-advanced.md +10 -6
  9. package/docs/cpp-organization.md +4 -2
  10. package/docs/cpp-safety.md +11 -7
  11. package/docs/cpp-types.md +1 -1
  12. package/docs/examples/_category_.json +5 -0
  13. package/docs/examples/combat.md +239 -0
  14. package/docs/examples/data-boot.md +98 -0
  15. package/docs/examples/hud.md +96 -0
  16. package/docs/examples/index.md +43 -0
  17. package/docs/examples/leaderstats.md +140 -0
  18. package/docs/examples/shop.md +122 -0
  19. package/docs/examples/sword.md +108 -0
  20. package/docs/getting-started.md +14 -7
  21. package/docs/intellisense.md +31 -0
  22. package/docs/intro.md +3 -3
  23. package/docs/libraries/_category_.json +5 -0
  24. package/docs/libraries/dataservice.md +104 -0
  25. package/docs/libraries/index.md +22 -0
  26. package/docs/libraries/janitor.md +65 -0
  27. package/docs/libraries/more.md +79 -0
  28. package/docs/libraries/net.md +35 -0
  29. package/docs/libraries/promise.md +27 -0
  30. package/docs/oop/_category_.json +5 -0
  31. package/docs/oop/file-tags.md +48 -0
  32. package/docs/oop/index.md +22 -0
  33. package/docs/oop/modules.md +88 -0
  34. package/docs/oop/services.md +76 -0
  35. package/docs/print-cout.md +91 -0
  36. package/docs/syntax.md +63 -8
  37. package/editors/vscode/extension.js +146 -0
  38. package/editors/vscode/package.json +25 -0
  39. package/include/cluaupp/libs/janitor.hpp +7 -3
  40. package/include/cluaupp/roblox.hpp +19 -0
  41. package/package.json +66 -65
  42. package/runtime/Janitor/init.luau +4 -34
  43. package/src/architecture.js +174 -66
  44. package/src/cli.js +88 -21
  45. package/src/client/init.client.cpp +5 -0
  46. package/src/compile.js +77 -7
  47. package/src/editor-install.js +218 -0
  48. package/src/emit.js +321 -9
  49. package/src/headers.js +234 -0
  50. package/src/intellisense.js +1368 -0
  51. package/src/layout.js +129 -0
  52. package/src/lex.js +17 -9
  53. package/src/libs.js +165 -18
  54. package/src/lsp.js +227 -0
  55. package/src/parse.js +187 -6
  56. package/src/preprocess.js +118 -3
  57. package/src/server/leaderstats.server.cpp +28 -0
  58. package/src/shared/config.cpp +5 -0
  59. package/src/shared/config.h +3 -0
  60. package/src/understand.js +66 -6
  61. package/templates/game/.clangd +11 -0
  62. package/templates/game/.vscode/c_cpp_properties.json +6 -2
  63. package/templates/game/.vscode/extensions.json +6 -0
  64. package/templates/game/.vscode/settings.json +16 -2
  65. package/templates/game/cluaupp.config.json +2 -2
  66. package/templates/game/compile_flags.txt +2 -0
  67. package/templates/game/src/client/init.client.cpp +1 -0
  68. package/templates/game/src/server/leaderstats.server.cpp +1 -0
  69. package/docs/libraries.md +0 -80
package/package.json CHANGED
@@ -1,65 +1,66 @@
1
- {
2
- "name": "cluaupp",
3
- "version": "0.1.2",
4
- "description": "Cluaupp — the definitive merge of C++ and modern Luau. Source-to-source transpiler with first-class Roblox APIs.",
5
- "author": "KartzDev",
6
- "license": "MIT",
7
- "bin": {
8
- "cluaupp": "bin/cluaupp.js",
9
- "cluau": "bin/cluaupp.js"
10
- },
11
- "main": "src/compile.js",
12
- "exports": {
13
- ".": "./src/compile.js",
14
- "./package.json": "./package.json"
15
- },
16
- "files": [
17
- "bin",
18
- "src",
19
- "include",
20
- "templates",
21
- "runtime",
22
- "README.md",
23
- "LICENSE",
24
- "CHANGELOG.md",
25
- "docs"
26
- ],
27
- "scripts": {
28
- "cluaupp": "node bin/cluaupp.js",
29
- "cluau": "node bin/cluaupp.js",
30
- "build": "node bin/cluaupp.js build",
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 && node test/hold.test.js",
33
- "generate-api": "node scripts/generate-api.js",
34
- "check-highlight": "node scripts/check-highlight.js",
35
- "vendor-libs": "node scripts/vendor-libs.js",
36
- "docs": "moonwave dev",
37
- "docs:build": "moonwave build",
38
- "site": "node scripts/generate-api.js && node scripts/check-highlight.js",
39
- "dev": "node scripts/dev.js",
40
- "stop": "node scripts/stop.js",
41
- "prepublishOnly": "npm test"
42
- },
43
- "engines": {
44
- "node": ">=18"
45
- },
46
- "keywords": [
47
- "luau",
48
- "roblox",
49
- "cpp",
50
- "c++",
51
- "transpiler",
52
- "compiler",
53
- "rojo",
54
- "cluaupp",
55
- "cluau"
56
- ],
57
- "repository": {
58
- "type": "git",
59
- "url": "git+https://github.com/KartzRbx/Cluaupp.git"
60
- },
61
- "bugs": {
62
- "url": "https://github.com/KartzRbx/Cluaupp/issues"
63
- },
64
- "homepage": "https://github.com/KartzRbx/Cluaupp#readme"
65
- }
1
+ {
2
+ "name": "cluaupp",
3
+ "version": "0.1.5",
4
+ "description": "Cluaupp — the definitive merge of C++ and modern Luau. Source-to-source transpiler with first-class Roblox APIs.",
5
+ "author": "KartzDev",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "cluaupp": "bin/cluaupp.js",
9
+ "cluau": "bin/cluaupp.js"
10
+ },
11
+ "main": "src/compile.js",
12
+ "exports": {
13
+ ".": "./src/compile.js",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "bin",
18
+ "src",
19
+ "include",
20
+ "templates",
21
+ "editors",
22
+ "runtime",
23
+ "README.md",
24
+ "LICENSE",
25
+ "CHANGELOG.md",
26
+ "docs"
27
+ ],
28
+ "scripts": {
29
+ "cluaupp": "node bin/cluaupp.js",
30
+ "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",
34
+ "generate-api": "node scripts/generate-api.js",
35
+ "check-highlight": "node scripts/check-highlight.js",
36
+ "vendor-libs": "node scripts/vendor-libs.js",
37
+ "docs": "moonwave dev",
38
+ "docs:build": "moonwave build",
39
+ "site": "node scripts/generate-api.js && node scripts/check-highlight.js",
40
+ "dev": "node scripts/dev.js",
41
+ "stop": "node scripts/stop.js",
42
+ "prepublishOnly": "npm test"
43
+ },
44
+ "engines": {
45
+ "node": ">=18"
46
+ },
47
+ "keywords": [
48
+ "luau",
49
+ "roblox",
50
+ "cpp",
51
+ "c++",
52
+ "transpiler",
53
+ "compiler",
54
+ "rojo",
55
+ "cluaupp",
56
+ "cluau"
57
+ ],
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/KartzRbx/Cluaupp.git"
61
+ },
62
+ "bugs": {
63
+ "url": "https://github.com/KartzRbx/Cluaupp/issues"
64
+ },
65
+ "homepage": "https://github.com/KartzRbx/Cluaupp#readme"
66
+ }
@@ -1,45 +1,15 @@
1
1
  --!strict
2
2
  -- Cluaupp Janitor — typed border. Engine: howmanysmall/Janitor (_impl).
3
- -- Requires the sibling CluauppLibs.Promise module, never a nested stub.
3
+ -- Public type and return value are the implementation. Promise is required
4
+ -- inside _impl as CluauppLibs.Promise (`script.Parent.Parent.Promise`).
4
5
 
5
6
  --[=[
6
7
  @class Janitor
7
8
  Cleans up connections, instances, promises, and threads.
8
9
  ]=]
9
10
 
10
- local PromiseMod = require(script.Parent.Promise)
11
- type Promise<T...> = PromiseMod.Promise<T...>
12
-
13
11
  local Impl = require(script._impl)
14
12
 
15
- export type Janitor = {
16
- CurrentlyCleaning: boolean,
17
- SuppressInstanceReDestroy: boolean,
18
- UnsafeThreadCleanup: boolean,
19
- Add: <T>(self: Janitor, object: T, methodName: (boolean | string)?, index: string?) -> T,
20
- AddObject: <T, A...>(
21
- self: Janitor,
22
- constructor: { new: (A...) -> T },
23
- methodName: (boolean | string)?,
24
- index: string?,
25
- A...
26
- ) -> T,
27
- AddPromise: <T...>(self: Janitor, promiseObject: Promise<T...>, index: string?) -> Promise<T...>,
28
- Remove: (self: Janitor, index: string) -> Janitor,
29
- RemoveNoClean: (self: Janitor, index: string) -> Janitor,
30
- RemoveList: (self: Janitor, ...string) -> Janitor,
31
- Get: <T>(self: Janitor, index: string) -> T?,
32
- GetAll: (self: Janitor) -> { [string]: unknown },
33
- Has: (self: Janitor, index: string) -> boolean,
34
- Cleanup: (self: Janitor) -> (),
35
- Destroy: (self: Janitor) -> (),
36
- LinkToInstance: (self: Janitor, object: Instance, allowMultiple: boolean?) -> RBXScriptConnection,
37
- LinkToInstances: (self: Janitor, ...Instance) -> Janitor,
38
- }
39
-
40
- type JanitorStatic = {
41
- new: () -> Janitor,
42
- Is: (value: unknown) -> boolean,
43
- }
13
+ export type Janitor = Impl.Janitor
44
14
 
45
- return Impl :: JanitorStatic
15
+ return Impl
@@ -2,16 +2,23 @@
2
2
 
3
3
  const path = require("path");
4
4
  const { emit } = require("./emit");
5
- const { collectLibraries, insertRequires, requireCluauppLib } = require("./libs");
5
+ const { requireCluauppLib } = require("./libs");
6
+ const { organizeDecls, janitorNames, emitSection, joinBlocks, SECTION, ENTRY_FN } = require("./layout");
6
7
  const {
7
8
  VALUE_CLASSES,
8
9
  analyze,
9
10
  toPascalServiceName,
10
11
  legacyOutName,
12
+ modernScriptOutName,
11
13
  } = require("./understand");
14
+ const { implOutName } = require("./preprocess");
12
15
 
13
16
  function header(plan) {
14
- const lines = ["--!strict", "-- Compiled by Cluaupp — C++ × Luau"];
17
+ const lines = [];
18
+ if (plan && plan.strict) {
19
+ lines.push("--!strict");
20
+ }
21
+ lines.push("-- Compiled by Cluaupp — C++ × Luau");
15
22
  if (plan && plan.reasoning) {
16
23
  for (const line of plan.reasoning) {
17
24
  lines.push(`-- ${line}`);
@@ -27,9 +34,9 @@ function cluauppLib(name) {
27
34
 
28
35
  function emitTypes(plan) {
29
36
  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")}`);
37
+ lines.push('const ReplicatedStorage = game:GetService("ReplicatedStorage")');
38
+ lines.push(`const Occlude = ${cluauppLib("Occlude")}`);
39
+ lines.push(`const ArrayIndexer = ${cluauppLib("ArrayIndexer")}`);
33
40
  lines.push("");
34
41
 
35
42
  if (plan.stats.length > 0) {
@@ -60,6 +67,7 @@ function emitTypes(plan) {
60
67
  manifest.push(" Get: (player: Player) -> PlayerCache?,");
61
68
  manifest.push(" Ensure: (player: Player) -> PlayerCache,");
62
69
  manifest.push(" Clear: (player: Player) -> (),");
70
+ manifest.push(" ClearAll: () -> (),");
63
71
  manifest.push(" },");
64
72
  }
65
73
  if (plan.roles.players) {
@@ -100,23 +108,26 @@ function emitTypes(plan) {
100
108
  }
101
109
 
102
110
  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
111
+ return `${header(plan).trimEnd()}
112
+
113
+ const Players = game:GetService("Players")
114
+ const ReplicatedStorage = game:GetService("ReplicatedStorage")
115
+ const Janitor = ${cluauppLib("Janitor")}
108
116
 
117
+ type Janitor = Janitor.Janitor
109
118
  export type PlayerHandler = (player: Player) -> ()
110
119
 
111
120
  local PlayersManager = {}
112
121
  local lifetime: Janitor? = nil
122
+ local leaveHandler: PlayerHandler? = nil
113
123
 
114
124
  function PlayersManager.Start(onPlayer: PlayerHandler, onLeave: PlayerHandler?)
115
125
  PlayersManager.Stop()
116
126
  local janitor = Janitor.new()
117
127
  lifetime = janitor
128
+ leaveHandler = onLeave
118
129
 
119
- local function accept(player: Player)
130
+ const function accept(player: Player)
120
131
  onPlayer(player)
121
132
  end
122
133
 
@@ -134,6 +145,13 @@ function PlayersManager.Stop()
134
145
  if lifetime == nil then
135
146
  return
136
147
  end
148
+ local onLeave = leaveHandler
149
+ leaveHandler = nil
150
+ if onLeave then
151
+ for _, player in Players:GetPlayers() do
152
+ onLeave(player)
153
+ end
154
+ end
137
155
  lifetime:Cleanup()
138
156
  lifetime = nil
139
157
  end
@@ -146,11 +164,11 @@ function emitCacheController(plan) {
146
164
  const types = plan.typesName;
147
165
  const folder = plan.folderName || "leaderstats";
148
166
  const lines = [header(plan).trimEnd(), ""];
149
- lines.push(`local Types = require(script.Parent.${types})`);
167
+ lines.push(`const Types = require(script.Parent.${types})`);
150
168
  lines.push("");
151
- lines.push(`local FOLDER_NAME = "${folder}"`);
152
- lines.push("local cache: { [Player]: Types.PlayerCache } = {}");
169
+ lines.push(`const FOLDER_NAME = "${folder}"`);
153
170
  lines.push("");
171
+ lines.push("local cache: { [Player]: Types.PlayerCache } = {}");
154
172
  lines.push("local CacheController = {}");
155
173
  lines.push("");
156
174
  lines.push("function CacheController.Get(player: Player): Types.PlayerCache?");
@@ -201,6 +219,10 @@ function emitCacheController(plan) {
201
219
  lines.push(" cache[player] = nil");
202
220
  lines.push("end");
203
221
  lines.push("");
222
+ lines.push("function CacheController.ClearAll()");
223
+ lines.push(" table.clear(cache)");
224
+ lines.push("end");
225
+ lines.push("");
204
226
  lines.push("return CacheController");
205
227
  lines.push("");
206
228
  return lines.join("\n");
@@ -209,13 +231,13 @@ function emitCacheController(plan) {
209
231
  function emitMain(plan) {
210
232
  const lines = [header(plan).trimEnd(), ""];
211
233
  if (plan.roles.players) {
212
- lines.push("local PlayersManager = require(script.Parent.PlayersManager)");
234
+ lines.push("const PlayersManager = require(script.Parent.PlayersManager)");
213
235
  }
214
236
  if (plan.roles.cache) {
215
- lines.push("local CacheController = require(script.Parent.CacheController)");
237
+ lines.push("const CacheController = require(script.Parent.CacheController)");
216
238
  }
217
239
  if (plan.roles.domain) {
218
- lines.push(`local ${plan.roles.domain} = require(script.Parent.${plan.roles.domain})`);
240
+ lines.push(`const ${plan.roles.domain} = require(script.Parent.${plan.roles.domain})`);
219
241
  }
220
242
  lines.push("");
221
243
  lines.push("local Main = {}");
@@ -242,9 +264,15 @@ function emitMain(plan) {
242
264
  if (plan.roles.domain) {
243
265
  lines.push(` ${plan.roles.domain}.Stop()`);
244
266
  }
267
+ if (plan.roles.cache) {
268
+ lines.push(" CacheController.ClearAll()");
269
+ }
245
270
  if (plan.roles.players) {
246
271
  lines.push(" PlayersManager.Stop()");
247
272
  }
273
+ if (!plan.roles.domain && !plan.roles.cache && !plan.roles.players) {
274
+ lines.push(" return");
275
+ }
248
276
  lines.push("end");
249
277
  lines.push("");
250
278
  lines.push("return Main");
@@ -258,6 +286,10 @@ function emitBootstrap(plan) {
258
286
  }
259
287
 
260
288
  function emitConfig(plan, ast, options) {
289
+ const owned = (ast.body || []).some((decl) => decl.owner);
290
+ if (owned) {
291
+ return `${header(plan).trimEnd()}\n\n${emit(ast, { ...options, skipHeader: true, skipInit: true }).trimEnd()}\n`;
292
+ }
261
293
  const lines = [];
262
294
  if (plan.onlyConsts) {
263
295
  lines.push(header(plan).trimEnd());
@@ -282,59 +314,86 @@ function emitConfig(plan, ast, options) {
282
314
  }
283
315
  if ((ast.body || []).some((decl) => decl.type === "function" && decl.name === "init")) {
284
316
  lines.push(" Start = init,");
317
+ lines.push(" Init = init,");
318
+ lines.push(" init = init,");
285
319
  }
286
320
  lines.push("}");
287
321
  lines.push("");
288
322
  return lines.join("\n");
289
323
  }
290
324
 
325
+ function emitDecls(decls, options) {
326
+ if (!decls || decls.length === 0) {
327
+ return "";
328
+ }
329
+ return emit({ type: "program", body: decls }, { ...options, skipHeader: true, skipInit: true }).trimEnd();
330
+ }
331
+
291
332
  function emitDomainController(plan, ast, options) {
292
- const absorbCache = plan.roles.cache;
293
333
  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;
334
+ return decl.type === "decl" || decl.type === "proto" || decl.type === "function" || decl.type === "expr";
308
335
  });
309
- const subset = { type: "program", body: keep };
310
- const code = emit(subset, { ...options, skipHeader: true, skipInit: true }).trimEnd();
336
+ const groups = organizeDecls(keep);
311
337
  const name = plan.roles.domain;
312
- 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
- `;
327
- return insertRequires(body, libs);
338
+ const entry = keep.find((decl) => decl.type === "function" && ENTRY_FN.test(decl.name || ""));
339
+ const bootExprs = keep.filter((decl) => decl.type === "expr");
340
+ const janitors = janitorNames(keep);
341
+ const closer = keep.find((decl) => decl.type === "function" && /^(OnClose|Cleanup|Shutdown)$/i.test(decl.name));
342
+ const stopLines = [];
343
+ if (janitors.length > 0) {
344
+ for (const janitor of janitors) {
345
+ stopLines.push(` ${janitor}:Cleanup()`);
346
+ }
347
+ } else if (closer) {
348
+ stopLines.push(` ${closer.name}()`);
349
+ } else {
350
+ stopLines.push(" return");
351
+ }
352
+ const boot = entry
353
+ ? ` ${entry.name}()`
354
+ : bootExprs.length
355
+ ? emitDecls(bootExprs, options)
356
+ .split("\n")
357
+ .map((line) => (line ? `\t${line}` : line))
358
+ .join("\n")
359
+ : " return";
360
+ const startFn = `function ${name}.Start()\n ${name}.Stop()\n${boot}\nend`;
361
+ const stopFn = `function ${name}.Stop()\n${stopLines.join("\n")}\nend`;
362
+ 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}`)}`;
363
+ return body;
328
364
  }
329
365
 
330
366
  function emitModule(plan, ast, options) {
331
367
  return emitConfig(plan, ast, options);
332
368
  }
333
369
 
370
+ function emitScriptMeta(runContext) {
371
+ return `${JSON.stringify(
372
+ {
373
+ properties: {
374
+ RunContext: `Enum.RunContext.${runContext}`,
375
+ },
376
+ },
377
+ null,
378
+ "\t",
379
+ )}\n`;
380
+ }
381
+
382
+ function serviceBootName(plan) {
383
+ if (plan.isClient) {
384
+ return "init.client.luau";
385
+ }
386
+ return "init.server.luau";
387
+ }
388
+
334
389
  function serviceFiles(plan, ast, options, dir) {
335
390
  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) }];
391
+ const boot = serviceBootName(plan);
392
+ const files = [{ name: `${folder}/${boot}`, contents: emitBootstrap(plan) }];
393
+ if (!plan.isClient && plan.runContext) {
394
+ files.push({ name: `${folder}/init.meta.json`, contents: emitScriptMeta(plan.runContext) });
395
+ }
396
+ files.push({ name: `${folder}/Main.luau`, contents: emitMain(plan) });
338
397
  if (plan.roles.players) {
339
398
  files.push({ name: `${folder}/PlayersManager.luau`, contents: emitPlayersManager(plan) });
340
399
  }
@@ -348,29 +407,75 @@ function serviceFiles(plan, ast, options, dir) {
348
407
  return files;
349
408
  }
350
409
 
410
+ function serviceFolderStale(plan, rel) {
411
+ const dir = path.posix.dirname(rel).replace(/^\.$/, "");
412
+ const serviceDir = dir && dir !== "." ? dir : plan.isClient ? "client" : "server";
413
+ const folder = `${serviceDir}/${plan.serviceName}`;
414
+ return [
415
+ folder,
416
+ `${folder}/init.luau`,
417
+ `${folder}/init.server.luau`,
418
+ `${folder}/init.client.luau`,
419
+ `${folder}/init.meta.json`,
420
+ `${folder}/Main.luau`,
421
+ `${folder}/PlayersManager.luau`,
422
+ `${folder}/CacheController.luau`,
423
+ `${folder}/${plan.typesName}.luau`,
424
+ plan.roles && plan.roles.domain ? `${folder}/${plan.roles.domain}.luau` : null,
425
+ ].filter(Boolean);
426
+ }
427
+
351
428
  function planOutput(ast, fileName, options = {}) {
352
429
  const plan = analyze(ast, fileName);
430
+ plan.strict = options.strict === true;
353
431
  const rel = (options.relativeName || fileName).replace(/\\/g, "/");
354
432
  const dir = path.dirname(rel).replace(/^\.$/, "");
355
433
  const outDir = dir && dir !== "." ? dir : "";
356
434
  const prefix = outDir ? `${outDir}/` : "";
435
+ const stale = serviceFolderStale(plan, rel);
357
436
 
358
- if (options.architecture === false) {
359
- return { kind: "flat", plan, files: null };
437
+ if (options.siblingHeader) {
438
+ return {
439
+ kind: "module",
440
+ plan,
441
+ files: [{ name: implOutName(rel), contents: emitModule(plan, ast, { ...options, skipInit: true }) }],
442
+ stale: [],
443
+ };
360
444
  }
361
445
 
362
- if (plan.kind === "legacy") {
363
- return { kind: "legacy", plan, files: null, stale: [], outName: legacyOutName(rel) };
446
+ if (options.architecture === true) {
447
+ if (plan.kind === "legacy") {
448
+ return { kind: "legacy", plan, files: null, stale: [], outName: legacyOutName(rel) };
449
+ }
450
+
451
+ if (plan.kind === "service") {
452
+ const serviceDir = outDir || (plan.isClient ? "client" : "server");
453
+ const folder = `${serviceDir}/${plan.serviceName}`;
454
+ const serviceStale = plan.isClient
455
+ ? [`${folder}/init.luau`, `${folder}/init.server.luau`, `${folder}/init.meta.json`]
456
+ : [`${folder}/init.luau`, `${folder}/init.client.luau`];
457
+ return {
458
+ kind: "service",
459
+ plan,
460
+ files: serviceFiles(plan, ast, options, serviceDir),
461
+ stale: serviceStale,
462
+ };
463
+ }
464
+
465
+ if (plan.kind === "config" || plan.kind === "module") {
466
+ return {
467
+ kind: plan.kind,
468
+ plan,
469
+ files: [{ name: `${prefix}${plan.serviceName}.luau`, contents: emitModule(plan, ast, options) }],
470
+ stale: [rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau")],
471
+ };
472
+ }
473
+
474
+ return { kind: "flat", plan, files: null, stale: [], outName: modernScriptOutName(rel) };
364
475
  }
365
476
 
366
- if (plan.kind === "service") {
367
- const serviceDir = outDir || (plan.isClient ? "client" : "server");
368
- return {
369
- kind: "service",
370
- plan,
371
- files: serviceFiles(plan, ast, options, serviceDir),
372
- stale: [rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau")],
373
- };
477
+ if (plan.kind === "legacy") {
478
+ return { kind: "legacy", plan, files: null, stale, outName: legacyOutName(rel) };
374
479
  }
375
480
 
376
481
  if (plan.kind === "config" || plan.kind === "module") {
@@ -378,11 +483,14 @@ function planOutput(ast, fileName, options = {}) {
378
483
  kind: plan.kind,
379
484
  plan,
380
485
  files: [{ name: `${prefix}${plan.serviceName}.luau`, contents: emitModule(plan, ast, options) }],
381
- stale: [rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau")],
486
+ stale: [rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau"), ...stale],
382
487
  };
383
488
  }
384
489
 
385
- return { kind: "flat", plan, files: null, stale: [] };
490
+ const outName = modernScriptOutName(rel);
491
+ const stripped = rel.replace(/\.(server|client|plugin)\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau");
492
+ const extraStale = stripped !== outName ? [stripped] : [];
493
+ return { kind: "flat", plan, files: null, stale: [...stale, ...extraStale], outName };
386
494
  }
387
495
 
388
496
  module.exports = {