cluaupp 0.1.4 → 0.2.0

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 (113) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +12 -7
  3. package/bin/cluau.js +1 -1
  4. package/bin/cluaupp.js +1 -1
  5. package/docs/README.md +1 -1
  6. package/docs/architecture.md +52 -77
  7. package/docs/cli.md +29 -9
  8. package/docs/comparison.md +3 -3
  9. package/docs/config.md +6 -6
  10. package/docs/cpp-organization.md +2 -2
  11. package/docs/examples/index.md +2 -0
  12. package/docs/getting-started.md +7 -5
  13. package/docs/intellisense.md +49 -31
  14. package/docs/intro.md +2 -2
  15. package/docs/oop/file-tags.md +16 -17
  16. package/docs/oop/index.md +2 -2
  17. package/docs/oop/modules.md +3 -1
  18. package/docs/oop/services.md +7 -14
  19. package/docs/syntax.md +6 -5
  20. package/editors/vscode/extension.js +97 -146
  21. package/editors/vscode/package.json +6 -6
  22. package/examples/README.md +18 -0
  23. package/examples/game/.clangd +25 -0
  24. package/examples/game/.vscode/c_cpp_properties.json +27 -0
  25. package/examples/game/.vscode/extensions.json +5 -0
  26. package/examples/game/.vscode/settings.json +27 -0
  27. package/examples/game/Packages/.gitkeep +0 -0
  28. package/examples/game/cluaupp.config.json +6 -0
  29. package/examples/game/compile_flags.txt +6 -0
  30. package/examples/game/default.project.json +39 -0
  31. package/examples/game/package.json +9 -0
  32. package/examples/game/rokit.toml +5 -0
  33. package/examples/game/src/client/hud.client.cpp +12 -0
  34. package/{src → examples/game/src}/client/init.client.cpp +1 -0
  35. package/examples/game/src/server/combat.server.cpp +23 -0
  36. package/{src → examples/game/src}/server/leaderstats.server.cpp +1 -0
  37. package/examples/game/wally.toml +11 -0
  38. package/generated/api.js +51 -0
  39. package/generated/architecture.js +473 -0
  40. package/generated/ast.js +2 -0
  41. package/generated/cli.js +191 -0
  42. package/generated/compile.js +115 -0
  43. package/generated/editor-install.js +170 -0
  44. package/generated/emit.js +503 -0
  45. package/generated/emitter/luau-codegen.js +167 -0
  46. package/generated/emitter/translators.js +164 -0
  47. package/generated/headers.js +220 -0
  48. package/generated/index.html +49 -0
  49. package/generated/intellisense.js +277 -0
  50. package/generated/layout.js +112 -0
  51. package/generated/lex.js +146 -0
  52. package/generated/libs.js +484 -0
  53. package/generated/lsp.js +55 -0
  54. package/generated/package-info.js +11 -0
  55. package/generated/parse.js +607 -0
  56. package/generated/parser/collector.js +100 -0
  57. package/generated/parser/index.js +30 -0
  58. package/generated/preprocess.js +181 -0
  59. package/generated/system-understander.js +443 -0
  60. package/generated/transpile.js +66 -0
  61. package/generated/types.js +2 -0
  62. package/generated/understand.js +298 -0
  63. package/generated/utils/process-orchestrator.js +63 -0
  64. package/generated/utils/project.js +491 -0
  65. package/generated/utils/rojo-mapper.js +181 -0
  66. package/generated/utils/safe-paths.js +104 -0
  67. package/package.json +25 -8
  68. package/src/api.ts +53 -0
  69. package/src/{architecture.js → architecture.ts} +80 -33
  70. package/src/ast.ts +37 -0
  71. package/src/cli.ts +214 -0
  72. package/src/compile.ts +118 -0
  73. package/src/editor-install.ts +182 -0
  74. package/src/{emit.js → emit.ts} +5 -6
  75. package/src/emitter/luau-codegen.ts +187 -0
  76. package/src/emitter/translators.ts +168 -0
  77. package/src/headers.ts +233 -0
  78. package/src/intellisense.ts +286 -0
  79. package/src/{layout.js → layout.ts} +114 -129
  80. package/src/{lex.js → lex.ts} +4 -6
  81. package/src/{libs.js → libs.ts} +119 -46
  82. package/src/lsp.ts +60 -0
  83. package/src/package-info.ts +7 -0
  84. package/src/{parse.js → parse.ts} +3 -4
  85. package/src/parser/collector.ts +115 -0
  86. package/src/parser/index.ts +27 -0
  87. package/src/{preprocess.js → preprocess.ts} +64 -32
  88. package/src/system-understander.ts +501 -0
  89. package/src/transpile.ts +64 -0
  90. package/src/tree-sitter-cpp.d.ts +4 -0
  91. package/src/types.ts +81 -0
  92. package/src/{understand.js → understand.ts} +24 -213
  93. package/src/utils/process-orchestrator.ts +72 -0
  94. package/src/utils/project.ts +506 -0
  95. package/src/utils/rojo-mapper.ts +190 -0
  96. package/src/utils/safe-paths.ts +98 -0
  97. package/templates/game/.clangd +9 -0
  98. package/templates/game/.vscode/c_cpp_properties.json +1 -1
  99. package/templates/game/.vscode/extensions.json +5 -6
  100. package/templates/game/.vscode/settings.json +7 -6
  101. package/templates/game/cluaupp.config.json +2 -2
  102. package/templates/game/compile_flags.txt +1 -0
  103. package/templates/game/rokit.toml +5 -0
  104. package/templates/game/src/client/init.client.cpp +1 -0
  105. package/templates/game/src/server/leaderstats.server.cpp +1 -0
  106. package/src/api.js +0 -57
  107. package/src/cli.js +0 -481
  108. package/src/compile.js +0 -56
  109. package/src/editor-install.js +0 -218
  110. package/src/intellisense.js +0 -1368
  111. package/src/lsp.js +0 -227
  112. /package/{src → examples/game/src}/shared/config.cpp +0 -0
  113. /package/{src → examples/game/src}/shared/config.h +0 -0
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createCppParserSync = createCppParserSync;
7
+ exports.parseCpp = parseCpp;
8
+ const tree_sitter_1 = __importDefault(require("tree-sitter"));
9
+ const node_module_1 = require("node:module");
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const package_info_js_1 = require("../package-info.js");
12
+ const nodeRequire = (0, node_module_1.createRequire)(node_path_1.default.join(package_info_js_1.projectRoot, "package.json"));
13
+ function createCppParserSync() {
14
+ try {
15
+ const Cpp = nodeRequire("tree-sitter-cpp");
16
+ const parser = new tree_sitter_1.default();
17
+ parser.setLanguage(Cpp);
18
+ parser.parse("int main() { return 0; }");
19
+ return parser;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ function parseCpp(source, parser = createCppParserSync()) {
26
+ if (!parser) {
27
+ return null;
28
+ }
29
+ return parser.parse(source);
30
+ }
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SOURCE_EXTS = void 0;
7
+ exports.isSourceFile = isSourceFile;
8
+ exports.isHeaderFile = isHeaderFile;
9
+ exports.toLuauPath = toLuauPath;
10
+ exports.isEngineStub = isEngineStub;
11
+ exports.siblingImplementation = siblingImplementation;
12
+ exports.siblingHeader = siblingHeader;
13
+ exports.implOutName = implOutName;
14
+ exports.resolveInclude = resolveInclude;
15
+ exports.scanHeaderExports = scanHeaderExports;
16
+ exports.applyPragmas = applyPragmas;
17
+ exports.preprocess = preprocess;
18
+ const node_fs_1 = __importDefault(require("node:fs"));
19
+ const node_path_1 = __importDefault(require("node:path"));
20
+ exports.SOURCE_EXTS = [".cpp", ".cc", ".cxx", ".c", ".h", ".hpp", ".hh"];
21
+ function isSourceFile(fileName) {
22
+ return exports.SOURCE_EXTS.includes(node_path_1.default.extname(fileName).toLowerCase());
23
+ }
24
+ function isHeaderFile(fileName) {
25
+ return [".h", ".hpp", ".hh"].includes(node_path_1.default.extname(fileName).toLowerCase());
26
+ }
27
+ function toLuauPath(filePath) {
28
+ return filePath.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau");
29
+ }
30
+ function isEngineStub(filePath) {
31
+ const normalized = filePath.replace(/\\/g, "/").toLowerCase();
32
+ return (normalized.includes("/include/cluaupp/") ||
33
+ normalized.includes("/include/cluau/") ||
34
+ normalized.endsWith("/roblox.hpp") ||
35
+ normalized.endsWith("/roblox.h"));
36
+ }
37
+ function includeVariants(name) {
38
+ const variants = [name];
39
+ if (/\.h$/i.test(name)) {
40
+ variants.push(name.replace(/\.h$/i, ".hpp"), name.replace(/\.h$/i, ".hh"));
41
+ }
42
+ else if (/\.hpp$/i.test(name)) {
43
+ variants.push(name.replace(/\.hpp$/i, ".h"), name.replace(/\.hpp$/i, ".hh"));
44
+ }
45
+ else if (/\.hh$/i.test(name)) {
46
+ variants.push(name.replace(/\.hh$/i, ".h"), name.replace(/\.hh$/i, ".hpp"));
47
+ }
48
+ return [...new Set(variants)];
49
+ }
50
+ function siblingImplementation(headerPath) {
51
+ if (!headerPath || !isHeaderFile(headerPath)) {
52
+ return null;
53
+ }
54
+ const base = headerPath.replace(/\.(h|hpp|hh)$/i, "");
55
+ for (const ext of [".cpp", ".cc", ".cxx", ".c"]) {
56
+ if (node_fs_1.default.existsSync(base + ext)) {
57
+ return base + ext;
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+ function siblingHeader(implPath) {
63
+ if (!implPath || isHeaderFile(implPath)) {
64
+ return null;
65
+ }
66
+ const base = String(implPath).replace(/\.(cpp|cc|cxx|c)$/i, "");
67
+ if (base === String(implPath)) {
68
+ return null;
69
+ }
70
+ for (const ext of [".h", ".hpp", ".hh"]) {
71
+ if (node_fs_1.default.existsSync(base + ext)) {
72
+ return base + ext;
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ function implOutName(rel) {
78
+ return String(rel)
79
+ .replace(/\\/g, "/")
80
+ .replace(/\.(cpp|cc|cxx|c)$/i, "Impl.luau");
81
+ }
82
+ function resolveInclude(name, fromFile, includeDirs) {
83
+ const bases = [node_path_1.default.dirname(fromFile), ...(includeDirs || [])];
84
+ for (const base of bases) {
85
+ for (const variant of includeVariants(name)) {
86
+ const candidate = node_path_1.default.resolve(base, variant);
87
+ if (node_fs_1.default.existsSync(candidate) && node_fs_1.default.statSync(candidate).isFile()) {
88
+ return candidate;
89
+ }
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+ function scanHeaderExports(source) {
95
+ const consts = [];
96
+ const structs = [];
97
+ for (const match of String(source).matchAll(/\bconst\s+(?:int|bool|float|double|auto|string)\s+(\w+)/g)) {
98
+ if (!consts.includes(match[1])) {
99
+ consts.push(match[1]);
100
+ }
101
+ }
102
+ for (const match of String(source).matchAll(/\bstruct\s+(\w+)/g)) {
103
+ if (!structs.includes(match[1])) {
104
+ structs.push(match[1]);
105
+ }
106
+ }
107
+ const hasProtos = /\b(?:void|int|bool|float|double|auto|string|[\w:]+)\s+\w+\s*\([^;]*\)\s*;/.test(String(source));
108
+ return { consts, structs, hasProtos };
109
+ }
110
+ function pushModuleInclude(options, resolved, impl) {
111
+ options.moduleIncludes = options.moduleIncludes || [];
112
+ const srcDir = options.srcDir || null;
113
+ const moduleFile = impl || resolved;
114
+ const rel = srcDir ? node_path_1.default.relative(srcDir, moduleFile).replace(/\\/g, "/") : node_path_1.default.basename(moduleFile);
115
+ const headerText = node_fs_1.default.readFileSync(resolved, "utf8");
116
+ options.moduleIncludes.push({
117
+ name: node_path_1.default.basename(resolved).replace(/\.(h|hpp|hh)$/i, ""),
118
+ outRel: rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau"),
119
+ header: resolved,
120
+ impl: impl || null,
121
+ exports: scanHeaderExports(headerText),
122
+ });
123
+ }
124
+ function applyPragmas(source, options) {
125
+ let nstrict = false;
126
+ let strict = false;
127
+ for (const line of String(source).split(/\r?\n/)) {
128
+ if (/^\s*#\s*pragma\s+nstrict\b/i.test(line)) {
129
+ nstrict = true;
130
+ }
131
+ else if (/^\s*#\s*pragma\s+strict\b/i.test(line)) {
132
+ strict = true;
133
+ }
134
+ }
135
+ if (nstrict) {
136
+ options.strict = false;
137
+ }
138
+ else if (strict) {
139
+ options.strict = true;
140
+ }
141
+ }
142
+ function preprocess(source, filePath, options = {}) {
143
+ if (!options.pragmaResolved) {
144
+ options.pragmaResolved = true;
145
+ applyPragmas(source, options);
146
+ }
147
+ const seen = options.seen || new Set();
148
+ options.seen = seen;
149
+ const includeDirs = options.includeDirs || [];
150
+ const resolvedSelf = filePath ? node_path_1.default.resolve(filePath) : null;
151
+ if (resolvedSelf) {
152
+ seen.add(resolvedSelf);
153
+ }
154
+ const lines = String(source).split(/\r?\n/);
155
+ const out = [];
156
+ for (const line of lines) {
157
+ const quoted = line.match(/^\s*#\s*include\s+"([^"]+)"/);
158
+ if (quoted) {
159
+ const resolved = resolveInclude(quoted[1], filePath || process.cwd(), includeDirs);
160
+ if (!resolved || isEngineStub(resolved) || seen.has(resolved)) {
161
+ continue;
162
+ }
163
+ const impl = siblingImplementation(resolved);
164
+ const including = filePath ? node_path_1.default.resolve(filePath) : null;
165
+ const ownHeader = Boolean(impl && including && node_path_1.default.resolve(impl) === including);
166
+ if (!ownHeader) {
167
+ pushModuleInclude(options, resolved, impl);
168
+ continue;
169
+ }
170
+ seen.add(resolved);
171
+ const inner = node_fs_1.default.readFileSync(resolved, "utf8");
172
+ out.push(preprocess(inner, resolved, { ...options, seen }));
173
+ continue;
174
+ }
175
+ if (/^\s*#\s*include\s+</.test(line)) {
176
+ continue;
177
+ }
178
+ out.push(line);
179
+ }
180
+ return out.join("\n");
181
+ }
@@ -0,0 +1,443 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.INTENTS = void 0;
7
+ exports.walk = walk;
8
+ exports.namesIn = namesIn;
9
+ exports.parseFileTag = parseFileTag;
10
+ exports.scoreIntents = scoreIntents;
11
+ exports.determineArchitecture = determineArchitecture;
12
+ exports.understandSystem = understandSystem;
13
+ const node_path_1 = __importDefault(require("node:path"));
14
+ const EXT = "(cpp|cc|cxx|c|h|hpp|hh)";
15
+ const EXT_REGEX = new RegExp(`\\.${EXT}$`, "i");
16
+ const SCORE_THRESHOLD = 2;
17
+ const TREE_NAME_TYPES = new Set([
18
+ "identifier",
19
+ "field_identifier",
20
+ "type_identifier",
21
+ "namespace_identifier",
22
+ ]);
23
+ const ROBLOX_SERVICES = new Set([
24
+ "Players",
25
+ "Workspace",
26
+ "Lighting",
27
+ "ReplicatedStorage",
28
+ "ReplicatedFirst",
29
+ "ServerStorage",
30
+ "ServerScriptService",
31
+ "StarterGui",
32
+ "StarterPack",
33
+ "StarterPlayer",
34
+ "SoundService",
35
+ "TweenService",
36
+ "RunService",
37
+ "UserInputService",
38
+ "ContextActionService",
39
+ "GuiService",
40
+ "TextChatService",
41
+ "TextService",
42
+ "CollectionService",
43
+ "PhysicsService",
44
+ "PathfindingService",
45
+ "DataStoreService",
46
+ "MemoryStoreService",
47
+ "MessagingService",
48
+ "TeleportService",
49
+ "HttpService",
50
+ "MarketplaceService",
51
+ "BadgeService",
52
+ "PolicyService",
53
+ "ProximityPromptService",
54
+ "HapticService",
55
+ "VRService",
56
+ "GamepadService",
57
+ "TouchInputService",
58
+ "VoiceChatService",
59
+ "AnimationClipProvider",
60
+ "AssetService",
61
+ "AvatarEditorService",
62
+ "Chat",
63
+ "Debris",
64
+ "InsertService",
65
+ "JointsService",
66
+ "KeyframeSequenceProvider",
67
+ "LocalizationService",
68
+ "LogService",
69
+ "MaterialService",
70
+ "NotificationService",
71
+ "PointsService",
72
+ "SocialService",
73
+ "Stats",
74
+ "Teams",
75
+ "TestService",
76
+ "TimerService",
77
+ ]);
78
+ /**
79
+ * Filename key first (roblox-ts ScriptType), then AST token density
80
+ * (Knit/Flamework Service vs Controller naming).
81
+ * @see https://github.com/roblox-ts/roblox-ts
82
+ * @see https://github.com/roblox-csharp/roblox-cs
83
+ * @see https://create.roblox.com/docs/reference/engine
84
+ */
85
+ exports.INTENTS = {
86
+ players: {
87
+ role: "manager",
88
+ module: "PlayersManager",
89
+ side: "server",
90
+ tokens: ["GetPlayers", "PlayerAdded", "PlayerRemoving", "Players"],
91
+ },
92
+ cache: {
93
+ role: "controller",
94
+ module: "CacheController",
95
+ side: "server",
96
+ tokens: ["Folder", "IntValue", "StringValue", "BoolValue", "NumberValue", "leaderstats"],
97
+ },
98
+ combat: {
99
+ role: "controller",
100
+ module: "CombatController",
101
+ side: "server",
102
+ tokens: [
103
+ "TakeDamage",
104
+ "Humanoid",
105
+ "Raycast",
106
+ "RaycastParams",
107
+ "Hitbox",
108
+ "Health",
109
+ "Damage",
110
+ "Weapon",
111
+ "Tool",
112
+ "Activated",
113
+ "Combat",
114
+ "Hurt",
115
+ "Attack",
116
+ ],
117
+ },
118
+ character: {
119
+ role: "controller",
120
+ module: "CharacterController",
121
+ side: "any",
122
+ tokens: ["CharacterAdded", "CharacterRemoving", "HumanoidRootPart", "GetPivot", "PivotTo", "Character"],
123
+ },
124
+ input: {
125
+ role: "controller",
126
+ module: "InputController",
127
+ side: "client",
128
+ tokens: [
129
+ "UserInputService",
130
+ "ContextActionService",
131
+ "InputBegan",
132
+ "InputEnded",
133
+ "InputChanged",
134
+ "Mouse",
135
+ "GetMouse",
136
+ "PreferredInput",
137
+ "GamepadService",
138
+ "TouchInputService",
139
+ ],
140
+ },
141
+ ui: {
142
+ role: "controller",
143
+ module: "ViewController",
144
+ side: "client",
145
+ tokens: [
146
+ "PlayerGui",
147
+ "ScreenGui",
148
+ "BillboardGui",
149
+ "SurfaceGui",
150
+ "TextLabel",
151
+ "TextButton",
152
+ "Frame",
153
+ "ImageLabel",
154
+ "TweenService",
155
+ "Twinkle",
156
+ "GuiService",
157
+ "StarterGui",
158
+ ],
159
+ },
160
+ net: {
161
+ role: "controller",
162
+ module: "NetController",
163
+ side: "any",
164
+ tokens: [
165
+ "RemoteEvent",
166
+ "RemoteFunction",
167
+ "UnreliableRemoteEvent",
168
+ "FireServer",
169
+ "FireAllClients",
170
+ "FireClient",
171
+ "OnServerEvent",
172
+ "OnClientEvent",
173
+ "Net",
174
+ "NetEvent",
175
+ ],
176
+ },
177
+ data: {
178
+ role: "service",
179
+ module: "DataController",
180
+ side: "server",
181
+ tokens: [
182
+ "DataStore",
183
+ "DataStoreService",
184
+ "DataService",
185
+ "SetAsync",
186
+ "GetAsync",
187
+ "UpdateAsync",
188
+ "ProfileStore",
189
+ "GetChangedSignal",
190
+ "WaitFor",
191
+ "MemoryStoreService",
192
+ "MessagingService",
193
+ ],
194
+ },
195
+ animation: {
196
+ role: "controller",
197
+ module: "AnimationController",
198
+ side: "any",
199
+ tokens: ["Animator", "Animation", "AnimationTrack", "LoadAnimation"],
200
+ },
201
+ inventory: {
202
+ role: "handler",
203
+ module: "InventoryHandler",
204
+ side: "any",
205
+ tokens: ["Backpack", "Inventory"],
206
+ },
207
+ marketplace: {
208
+ role: "service",
209
+ module: "MarketplaceService",
210
+ side: "server",
211
+ tokens: ["MarketplaceService", "PromptGamePassPurchase", "PromptProductPurchase", "ProcessReceipt"],
212
+ },
213
+ collection: {
214
+ role: "manager",
215
+ module: "CollectionManager",
216
+ side: "shared",
217
+ tokens: ["CollectionService", "GetTagged", "AddTag", "RemoveTag", "GetInstanceAddedSignal"],
218
+ },
219
+ proximity: {
220
+ role: "handler",
221
+ module: "ProximityHandler",
222
+ side: "any",
223
+ tokens: ["ProximityPrompt", "ProximityPromptService", "Triggered"],
224
+ },
225
+ camera: {
226
+ role: "controller",
227
+ module: "CameraController",
228
+ side: "client",
229
+ tokens: ["CurrentCamera", "Camera", "CameraType", "CameraSubject"],
230
+ },
231
+ run: {
232
+ role: "utility",
233
+ module: "HeartbeatUtil",
234
+ side: "any",
235
+ tokens: ["RunService", "Heartbeat", "RenderStepped", "Stepped", "PreSimulation"],
236
+ },
237
+ };
238
+ function isTreeNode(node) {
239
+ return Boolean(node &&
240
+ typeof node === "object" &&
241
+ "childCount" in node &&
242
+ typeof node.child === "function");
243
+ }
244
+ function walk(node, visit) {
245
+ if (!node || typeof node !== "object") {
246
+ return;
247
+ }
248
+ visit(node);
249
+ if (isTreeNode(node) && typeof node.childCount === "number" && node.child) {
250
+ for (let index = 0; index < node.childCount; index += 1) {
251
+ const child = node.child(index);
252
+ if (child) {
253
+ walk(child, visit);
254
+ }
255
+ }
256
+ return;
257
+ }
258
+ for (const value of Object.values(node)) {
259
+ if (Array.isArray(value)) {
260
+ for (const item of value) {
261
+ walk(item, visit);
262
+ }
263
+ }
264
+ else if (value && typeof value === "object") {
265
+ walk(value, visit);
266
+ }
267
+ }
268
+ }
269
+ function addName(found, value) {
270
+ if (typeof value !== "string") {
271
+ return;
272
+ }
273
+ const trimmed = value.replace(/^["'<]+|["'>]+$/g, "").trim();
274
+ if (trimmed) {
275
+ found.add(trimmed);
276
+ }
277
+ }
278
+ function namesIn(ast, options = {}) {
279
+ const found = new Set();
280
+ walk(ast, (node) => {
281
+ const type = String(node.type || "");
282
+ if ((type === "call" || type === "member" || type === "ident") && node.name) {
283
+ addName(found, node.name);
284
+ }
285
+ if (type === "new" && node.className) {
286
+ addName(found, node.className);
287
+ }
288
+ if (type === "getService" && node.service) {
289
+ addName(found, node.service);
290
+ }
291
+ if (options.strings !== false && type === "string" && node.value) {
292
+ addName(found, node.value);
293
+ }
294
+ if (TREE_NAME_TYPES.has(type) && typeof node.text === "string") {
295
+ addName(found, node.text);
296
+ }
297
+ if (options.strings !== false && type === "string_literal" && typeof node.text === "string") {
298
+ addName(found, node.text);
299
+ }
300
+ });
301
+ return found;
302
+ }
303
+ function parseFileTag(fileName) {
304
+ const base = node_path_1.default.basename(fileName);
305
+ if (new RegExp(`\\.legacy\\.plugin\\.${EXT}$`, "i").test(base)) {
306
+ return { key: "legacy.plugin", emit: "legacy", runtime: "plugin", rojo: "Script", runContext: "Plugin" };
307
+ }
308
+ if (new RegExp(`\\.legacy\\.server\\.${EXT}$`, "i").test(base)) {
309
+ return { key: "legacy.server", emit: "legacy", runtime: "server", rojo: "Script", runContext: null };
310
+ }
311
+ if (new RegExp(`\\.legacy\\.client\\.${EXT}$`, "i").test(base)) {
312
+ return { key: "legacy.client", emit: "legacy", runtime: "client", rojo: "LocalScript", runContext: null };
313
+ }
314
+ if (new RegExp(`\\.legacy\\.${EXT}$`, "i").test(base)) {
315
+ return { key: "legacy", emit: "legacy", runtime: "server", rojo: "Script", runContext: null };
316
+ }
317
+ if (new RegExp(`\\.plugin\\.${EXT}$`, "i").test(base)) {
318
+ return { key: "plugin", emit: "service", runtime: "plugin", rojo: "Script", runContext: "Plugin" };
319
+ }
320
+ if (new RegExp(`\\.server\\.${EXT}$`, "i").test(base)) {
321
+ return { key: "server", emit: "service", runtime: "server", rojo: "Script", runContext: "Server" };
322
+ }
323
+ if (new RegExp(`\\.client\\.${EXT}$`, "i").test(base)) {
324
+ return { key: "client", emit: "service", runtime: "client", rojo: "LocalScript", runContext: "Client" };
325
+ }
326
+ return { key: "module", emit: "module", runtime: "shared", rojo: "ModuleScript", runContext: null };
327
+ }
328
+ function sideWeight(runtime, side) {
329
+ if (side === "any" || side === "shared") {
330
+ return 1;
331
+ }
332
+ if (side === runtime) {
333
+ return 1.25;
334
+ }
335
+ if (runtime === "shared") {
336
+ return 0.6;
337
+ }
338
+ return 0.35;
339
+ }
340
+ function scoreIntents(namedSet, runtime = "shared") {
341
+ const scored = [];
342
+ for (const [name, spec] of Object.entries(exports.INTENTS)) {
343
+ const evidence = spec.tokens.filter((token) => namedSet.has(token));
344
+ if (evidence.length === 0) {
345
+ continue;
346
+ }
347
+ scored.push({
348
+ name,
349
+ score: evidence.length * sideWeight(runtime, spec.side),
350
+ evidence,
351
+ role: spec.role,
352
+ module: spec.module,
353
+ side: spec.side,
354
+ });
355
+ }
356
+ return scored.sort((a, b) => b.score - a.score || b.evidence.length - a.evidence.length);
357
+ }
358
+ function pascalUtilName(fileName) {
359
+ let base = node_path_1.default.basename(fileName).replace(EXT_REGEX, "");
360
+ base = base.replace(/\.legacy\.(server|client|plugin)$/i, "");
361
+ base = base.replace(/\.(server|client|plugin)$/i, "");
362
+ if (!base) {
363
+ return "ModuleUtil";
364
+ }
365
+ const pascal = base
366
+ .split(/[-_]+/)
367
+ .filter(Boolean)
368
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
369
+ .join("");
370
+ return /Util$/.test(pascal) ? pascal : `${pascal}Util`;
371
+ }
372
+ function collectInjectedServices(named, evidence) {
373
+ const found = new Set();
374
+ for (const token of [...evidence, ...named]) {
375
+ if (ROBLOX_SERVICES.has(token)) {
376
+ found.add(token);
377
+ }
378
+ }
379
+ return [...found].sort();
380
+ }
381
+ const SUPPORTING_INTENTS = new Set(["players", "cache", "run"]);
382
+ function pickDominant(intents) {
383
+ const top = intents[0];
384
+ if (!top) {
385
+ return undefined;
386
+ }
387
+ const domain = intents.find((item) => !SUPPORTING_INTENTS.has(item.name) && item.evidence.length >= SCORE_THRESHOLD);
388
+ if (domain && (SUPPORTING_INTENTS.has(top.name) || domain.score >= top.score * 0.8)) {
389
+ return domain;
390
+ }
391
+ return top;
392
+ }
393
+ function formatIntents(intents) {
394
+ return intents.map((item) => `${item.name}:${item.evidence.length}`).join(", ");
395
+ }
396
+ function determineArchitecture(fileName, ast, tree) {
397
+ const fileTag = parseFileTag(fileName);
398
+ const named = new Set();
399
+ if (ast) {
400
+ for (const name of namesIn(ast)) {
401
+ named.add(name);
402
+ }
403
+ }
404
+ if (tree) {
405
+ for (const name of namesIn(tree)) {
406
+ named.add(name);
407
+ }
408
+ }
409
+ const intents = scoreIntents(named, fileTag.runtime);
410
+ const top = pickDominant(intents);
411
+ if (!top || top.score < SCORE_THRESHOLD) {
412
+ const suggestedModule = pascalUtilName(fileName);
413
+ return {
414
+ role: "utility",
415
+ suggestedModule,
416
+ fileTag,
417
+ injectedServices: collectInjectedServices(named, top ? top.evidence : []),
418
+ isUtility: true,
419
+ intents,
420
+ reasoning: [
421
+ `tag ${fileTag.key} → ${fileTag.rojo} (${fileTag.emit})`,
422
+ top ? `intents ${formatIntents(intents)} below threshold` : "intents none — utility module",
423
+ `role utility ${suggestedModule}`,
424
+ ],
425
+ };
426
+ }
427
+ return {
428
+ role: top.role,
429
+ suggestedModule: top.module,
430
+ fileTag,
431
+ injectedServices: collectInjectedServices(named, top.evidence),
432
+ isUtility: false,
433
+ intents,
434
+ reasoning: [
435
+ `tag ${fileTag.key} → ${fileTag.rojo} (${fileTag.emit})`,
436
+ `intents ${formatIntents(intents)}`,
437
+ `role ${top.role} ${top.module}`,
438
+ ],
439
+ };
440
+ }
441
+ function understandSystem(fileName, ast, tree) {
442
+ return determineArchitecture(fileName, ast, tree);
443
+ }