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.
- package/CHANGELOG.md +21 -0
- package/README.md +12 -7
- package/bin/cluau.js +1 -1
- package/bin/cluaupp.js +1 -1
- package/docs/README.md +1 -1
- package/docs/architecture.md +52 -77
- package/docs/cli.md +29 -9
- package/docs/comparison.md +3 -3
- package/docs/config.md +6 -6
- package/docs/cpp-organization.md +2 -2
- package/docs/examples/index.md +2 -0
- package/docs/getting-started.md +7 -5
- package/docs/intellisense.md +49 -31
- package/docs/intro.md +2 -2
- package/docs/oop/file-tags.md +16 -17
- package/docs/oop/index.md +2 -2
- package/docs/oop/modules.md +3 -1
- package/docs/oop/services.md +7 -14
- package/docs/syntax.md +6 -5
- package/editors/vscode/extension.js +97 -146
- package/editors/vscode/package.json +6 -6
- package/examples/README.md +18 -0
- package/examples/game/.clangd +25 -0
- package/examples/game/.vscode/c_cpp_properties.json +27 -0
- package/examples/game/.vscode/extensions.json +5 -0
- package/examples/game/.vscode/settings.json +27 -0
- package/examples/game/Packages/.gitkeep +0 -0
- package/examples/game/cluaupp.config.json +6 -0
- package/examples/game/compile_flags.txt +6 -0
- package/examples/game/default.project.json +39 -0
- package/examples/game/package.json +9 -0
- package/examples/game/rokit.toml +5 -0
- package/examples/game/src/client/hud.client.cpp +12 -0
- package/{src → examples/game/src}/client/init.client.cpp +1 -0
- package/examples/game/src/server/combat.server.cpp +23 -0
- package/{src → examples/game/src}/server/leaderstats.server.cpp +1 -0
- package/examples/game/wally.toml +11 -0
- package/generated/api.js +51 -0
- package/generated/architecture.js +473 -0
- package/generated/ast.js +2 -0
- package/generated/cli.js +191 -0
- package/generated/compile.js +115 -0
- package/generated/editor-install.js +170 -0
- package/generated/emit.js +503 -0
- package/generated/emitter/luau-codegen.js +167 -0
- package/generated/emitter/translators.js +164 -0
- package/generated/headers.js +220 -0
- package/generated/index.html +49 -0
- package/generated/intellisense.js +277 -0
- package/generated/layout.js +112 -0
- package/generated/lex.js +146 -0
- package/generated/libs.js +484 -0
- package/generated/lsp.js +55 -0
- package/generated/package-info.js +11 -0
- package/generated/parse.js +607 -0
- package/generated/parser/collector.js +100 -0
- package/generated/parser/index.js +30 -0
- package/generated/preprocess.js +181 -0
- package/generated/system-understander.js +443 -0
- package/generated/transpile.js +66 -0
- package/generated/types.js +2 -0
- package/generated/understand.js +298 -0
- package/generated/utils/process-orchestrator.js +63 -0
- package/generated/utils/project.js +491 -0
- package/generated/utils/rojo-mapper.js +181 -0
- package/generated/utils/safe-paths.js +104 -0
- package/package.json +25 -8
- package/src/api.ts +53 -0
- package/src/{architecture.js → architecture.ts} +80 -33
- package/src/ast.ts +37 -0
- package/src/cli.ts +214 -0
- package/src/compile.ts +118 -0
- package/src/editor-install.ts +182 -0
- package/src/{emit.js → emit.ts} +5 -6
- package/src/emitter/luau-codegen.ts +187 -0
- package/src/emitter/translators.ts +168 -0
- package/src/headers.ts +233 -0
- package/src/intellisense.ts +286 -0
- package/src/{layout.js → layout.ts} +114 -129
- package/src/{lex.js → lex.ts} +4 -6
- package/src/{libs.js → libs.ts} +119 -46
- package/src/lsp.ts +60 -0
- package/src/package-info.ts +7 -0
- package/src/{parse.js → parse.ts} +3 -4
- package/src/parser/collector.ts +115 -0
- package/src/parser/index.ts +27 -0
- package/src/{preprocess.js → preprocess.ts} +64 -32
- package/src/system-understander.ts +501 -0
- package/src/transpile.ts +64 -0
- package/src/tree-sitter-cpp.d.ts +4 -0
- package/src/types.ts +81 -0
- package/src/{understand.js → understand.ts} +24 -213
- package/src/utils/process-orchestrator.ts +72 -0
- package/src/utils/project.ts +506 -0
- package/src/utils/rojo-mapper.ts +190 -0
- package/src/utils/safe-paths.ts +98 -0
- package/templates/game/.clangd +9 -0
- package/templates/game/.vscode/c_cpp_properties.json +1 -1
- package/templates/game/.vscode/extensions.json +5 -6
- package/templates/game/.vscode/settings.json +7 -6
- package/templates/game/cluaupp.config.json +2 -2
- package/templates/game/compile_flags.txt +1 -0
- package/templates/game/rokit.toml +5 -0
- package/templates/game/src/client/init.client.cpp +1 -0
- package/templates/game/src/server/leaderstats.server.cpp +1 -0
- package/src/api.js +0 -57
- package/src/cli.js +0 -481
- package/src/compile.js +0 -56
- package/src/editor-install.js +0 -218
- package/src/intellisense.js +0 -1368
- package/src/lsp.js +0 -227
- /package/{src → examples/game/src}/shared/config.cpp +0 -0
- /package/{src → examples/game/src}/shared/config.h +0 -0
|
@@ -1,129 +1,114 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const SECTION = {
|
|
4
|
-
api: "API",
|
|
5
|
-
constants: "
|
|
6
|
-
variables: "
|
|
7
|
-
types: "
|
|
8
|
-
support: "
|
|
9
|
-
principal: "
|
|
10
|
-
cleanup: "
|
|
11
|
-
returns: "
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const CLEANUP_FN = /^(OnClose|OnPlayerRemoving|Cleanup|Shutdown|OnDestroy)$/i;
|
|
15
|
-
const PRINCIPAL_FN = /^(init|start|main|setup)/i;
|
|
16
|
-
const ENTRY_FN = /^(init|main)$/i;
|
|
17
|
-
|
|
18
|
-
function isGetService(node) {
|
|
19
|
-
return Boolean(node && node.type === "getService");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function isDataServiceAlias(node) {
|
|
23
|
-
let current = node;
|
|
24
|
-
while (current && current.type === "member") {
|
|
25
|
-
if (current.object && current.object.type === "ident" && current.object.name === "DataService") {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
current = current.object;
|
|
29
|
-
}
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function isJanitorDecl(decl) {
|
|
34
|
-
if (!decl || decl.type !== "decl") {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
const name = String(decl.name || "").toLowerCase();
|
|
38
|
-
const typeName = String(decl.valueType || "").toLowerCase();
|
|
39
|
-
const created = decl.value && decl.value.type === "new" && decl.value.className === "Janitor";
|
|
40
|
-
return name.includes("janitor") || typeName.includes("janitor") || created;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isApiDecl(decl) {
|
|
44
|
-
if (!decl || decl.type !== "decl") {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
return isGetService(decl.value) || isDataServiceAlias(decl.value);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function classifyDecl(decl) {
|
|
51
|
-
if (!decl || decl.type === "proto") {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
if (decl.type === "decl") {
|
|
55
|
-
if (isApiDecl(decl)) {
|
|
56
|
-
return "api";
|
|
57
|
-
}
|
|
58
|
-
if (isJanitorDecl(decl)) {
|
|
59
|
-
return "variables";
|
|
60
|
-
}
|
|
61
|
-
if (decl.isConst) {
|
|
62
|
-
return "constants";
|
|
63
|
-
}
|
|
64
|
-
return "variables";
|
|
65
|
-
}
|
|
66
|
-
if (decl.type === "function") {
|
|
67
|
-
if (CLEANUP_FN.test(decl.name || "")) {
|
|
68
|
-
return "cleanup";
|
|
69
|
-
}
|
|
70
|
-
if (PRINCIPAL_FN.test(decl.name || "")) {
|
|
71
|
-
return "principal";
|
|
72
|
-
}
|
|
73
|
-
return "support";
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function organizeDecls(keep) {
|
|
79
|
-
const groups = {
|
|
80
|
-
api: [],
|
|
81
|
-
constants: [],
|
|
82
|
-
variables: [],
|
|
83
|
-
support: [],
|
|
84
|
-
principal: [],
|
|
85
|
-
cleanup: [],
|
|
86
|
-
};
|
|
87
|
-
for (const decl of keep) {
|
|
88
|
-
const bucket = classifyDecl(decl);
|
|
89
|
-
if (bucket && groups[bucket]) {
|
|
90
|
-
groups[bucket].push(decl);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return groups;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function janitorNames(keep) {
|
|
97
|
-
return (keep || []).filter(isJanitorDecl).map((decl) => decl.name);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function emitSection(_title, body) {
|
|
101
|
-
const text = String(body || "").trimEnd();
|
|
102
|
-
if (!text) {
|
|
103
|
-
return "";
|
|
104
|
-
}
|
|
105
|
-
return `${text}\n\n`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function joinBlocks(parts) {
|
|
109
|
-
return parts.filter((part) => Boolean(part && String(part).trim())).join("\n\n");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function commentLine(title) {
|
|
113
|
-
return `-- ${title}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
module.exports = {
|
|
117
|
-
SECTION,
|
|
118
|
-
CLEANUP_FN,
|
|
119
|
-
PRINCIPAL_FN,
|
|
120
|
-
ENTRY_FN,
|
|
121
|
-
isJanitorDecl,
|
|
122
|
-
isApiDecl,
|
|
123
|
-
classifyDecl,
|
|
124
|
-
organizeDecls,
|
|
125
|
-
janitorNames,
|
|
126
|
-
emitSection,
|
|
127
|
-
joinBlocks,
|
|
128
|
-
commentLine,
|
|
129
|
-
};
|
|
1
|
+
import type { AstNode } from "./ast.js";
|
|
2
|
+
|
|
3
|
+
export const SECTION = {
|
|
4
|
+
api: "API",
|
|
5
|
+
constants: "CONSTANTS",
|
|
6
|
+
variables: "VARIABLES",
|
|
7
|
+
types: "TYPES",
|
|
8
|
+
support: "HELPERS FUNCTIONS",
|
|
9
|
+
principal: "MAIN FUNCTIONS",
|
|
10
|
+
cleanup: "JANITOR",
|
|
11
|
+
returns: "RETURN",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const CLEANUP_FN = /^(OnClose|OnPlayerRemoving|Cleanup|Shutdown|OnDestroy)$/i;
|
|
15
|
+
export const PRINCIPAL_FN = /^(init|start|main|setup)/i;
|
|
16
|
+
export const ENTRY_FN = /^(init|main)$/i;
|
|
17
|
+
|
|
18
|
+
function isGetService(node: AstNode | null | undefined): boolean {
|
|
19
|
+
return Boolean(node && node.type === "getService");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isDataServiceAlias(node: AstNode | null | undefined): boolean {
|
|
23
|
+
let current: AstNode | undefined = node || undefined;
|
|
24
|
+
while (current && current.type === "member") {
|
|
25
|
+
if (current.object && current.object.type === "ident" && current.object.name === "DataService") {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
current = current.object;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isJanitorDecl(decl: AstNode | null | undefined): boolean {
|
|
34
|
+
if (!decl || decl.type !== "decl") {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const name = String(decl.name || "").toLowerCase();
|
|
38
|
+
const typeName = String(decl.valueType || "").toLowerCase();
|
|
39
|
+
const created = decl.value && decl.value.type === "new" && decl.value.className === "Janitor";
|
|
40
|
+
return name.includes("janitor") || typeName.includes("janitor") || created;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isApiDecl(decl: AstNode | null | undefined): boolean {
|
|
44
|
+
if (!decl || decl.type !== "decl") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return isGetService(decl.value) || isDataServiceAlias(decl.value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function classifyDecl(decl: AstNode | null | undefined): string | null {
|
|
51
|
+
if (!decl || decl.type === "proto") {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (decl.type === "decl") {
|
|
55
|
+
if (isApiDecl(decl)) {
|
|
56
|
+
return "api";
|
|
57
|
+
}
|
|
58
|
+
if (isJanitorDecl(decl)) {
|
|
59
|
+
return "variables";
|
|
60
|
+
}
|
|
61
|
+
if (decl.isConst) {
|
|
62
|
+
return "constants";
|
|
63
|
+
}
|
|
64
|
+
return "variables";
|
|
65
|
+
}
|
|
66
|
+
if (decl.type === "function") {
|
|
67
|
+
if (CLEANUP_FN.test(decl.name || "")) {
|
|
68
|
+
return "cleanup";
|
|
69
|
+
}
|
|
70
|
+
if (PRINCIPAL_FN.test(decl.name || "")) {
|
|
71
|
+
return "principal";
|
|
72
|
+
}
|
|
73
|
+
return "support";
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function organizeDecls(keep: AstNode[]): Record<string, AstNode[]> {
|
|
79
|
+
const groups: Record<string, AstNode[]> = {
|
|
80
|
+
api: [],
|
|
81
|
+
constants: [],
|
|
82
|
+
variables: [],
|
|
83
|
+
support: [],
|
|
84
|
+
principal: [],
|
|
85
|
+
cleanup: [],
|
|
86
|
+
};
|
|
87
|
+
for (const decl of keep) {
|
|
88
|
+
const bucket = classifyDecl(decl);
|
|
89
|
+
if (bucket && groups[bucket]) {
|
|
90
|
+
groups[bucket].push(decl);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return groups;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function janitorNames(keep: AstNode[] | null | undefined): string[] {
|
|
97
|
+
return (keep || []).filter(isJanitorDecl).map((decl) => decl.name);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function emitSection(_title: string, body: string): string {
|
|
101
|
+
const text = String(body || "").trimEnd();
|
|
102
|
+
if (!text) {
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
return `${text}\n\n`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function joinBlocks(parts: Array<string | null | undefined>): string {
|
|
109
|
+
return parts.filter((part) => Boolean(part && String(part).trim())).join("\n\n");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function commentLine(title: string): string {
|
|
113
|
+
return `-- ${title}`;
|
|
114
|
+
}
|
package/src/{lex.js → lex.ts}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Token } from "./ast.js";
|
|
2
2
|
|
|
3
3
|
const KEYWORDS = new Set([
|
|
4
4
|
"if",
|
|
@@ -34,13 +34,13 @@ const KEYWORDS = new Set([
|
|
|
34
34
|
"break",
|
|
35
35
|
]);
|
|
36
36
|
|
|
37
|
-
function tokenize(source) {
|
|
38
|
-
const tokens = [];
|
|
37
|
+
export function tokenize(source: string): Token[] {
|
|
38
|
+
const tokens: Token[] = [];
|
|
39
39
|
let i = 0;
|
|
40
40
|
let line = 1;
|
|
41
41
|
let col = 1;
|
|
42
42
|
|
|
43
|
-
const push = (type, value, startLine, startCol, start, end) => {
|
|
43
|
+
const push = (type: string, value: string, startLine: number, startCol: number, start: number, end: number) => {
|
|
44
44
|
tokens.push({ type, value, line: startLine, col: startCol, start, end });
|
|
45
45
|
};
|
|
46
46
|
|
|
@@ -153,5 +153,3 @@ function tokenize(source) {
|
|
|
153
153
|
push("eof", "", line, col, source.length, source.length);
|
|
154
154
|
return tokens;
|
|
155
155
|
}
|
|
156
|
-
|
|
157
|
-
module.exports = { tokenize };
|
package/src/{libs.js → libs.ts}
RENAMED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const path = require("path");
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import path from "node:path";
|
|
4
3
|
|
|
5
4
|
const TYPE_EXPORTS = {
|
|
6
5
|
Janitor: "Janitor",
|
|
@@ -299,17 +298,64 @@ function collectLibraries(source, ast) {
|
|
|
299
298
|
return unique;
|
|
300
299
|
}
|
|
301
300
|
|
|
301
|
+
const ROJO_ROOTS = {
|
|
302
|
+
shared: { service: "ReplicatedStorage", expr: "ReplicatedStorage.Cluaupp" },
|
|
303
|
+
server: { service: "ServerScriptService", expr: "ServerScriptService.Cluaupp" },
|
|
304
|
+
client: { service: "StarterPlayer", expr: "StarterPlayer.StarterPlayerScripts.Cluaupp" },
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const SERVICE_ORDER = ["ReplicatedStorage", "ServerScriptService", "StarterPlayer"];
|
|
308
|
+
|
|
309
|
+
const SERVICE_GET = {
|
|
310
|
+
ReplicatedStorage: 'const ReplicatedStorage = game:GetService("ReplicatedStorage")',
|
|
311
|
+
ServerScriptService: 'const ServerScriptService = game:GetService("ServerScriptService")',
|
|
312
|
+
StarterPlayer: 'const StarterPlayer = game:GetService("StarterPlayer")',
|
|
313
|
+
};
|
|
314
|
+
|
|
302
315
|
function requireCluauppLib(name) {
|
|
303
316
|
return `require(ReplicatedStorage.CluauppLibs.${name})`;
|
|
304
317
|
}
|
|
305
318
|
|
|
306
|
-
function
|
|
319
|
+
function parseOutRel(toOutRel) {
|
|
320
|
+
const posix = String(toOutRel || "module.luau")
|
|
321
|
+
.replace(/\\/g, "/")
|
|
322
|
+
.replace(/\.luau$/i, "");
|
|
323
|
+
const parts = posix.split("/").filter(Boolean);
|
|
324
|
+
const tree = parts[0];
|
|
325
|
+
const root = ROJO_ROOTS[tree] || null;
|
|
326
|
+
const rest = root ? parts.slice(1) : parts;
|
|
327
|
+
return { tree, root, rest, posix };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function robloxRequireFrom(_fromOutRel, toOutRel) {
|
|
331
|
+
const { root, rest } = parseOutRel(toOutRel);
|
|
332
|
+
if (root) {
|
|
333
|
+
const tail = rest.join(".");
|
|
334
|
+
return tail ? `require(${root.expr}.${tail})` : `require(${root.expr})`;
|
|
335
|
+
}
|
|
336
|
+
const fromDir = path.posix.dirname(String(_fromOutRel || "module.luau").replace(/\\/g, "/"));
|
|
337
|
+
const toMod = String(toOutRel || "module.luau")
|
|
338
|
+
.replace(/\\/g, "/")
|
|
339
|
+
.replace(/\.luau$/i, "");
|
|
340
|
+
let rel = path.posix.relative(fromDir, toMod);
|
|
341
|
+
if (!rel || rel === ".") {
|
|
342
|
+
rel = path.posix.basename(toMod);
|
|
343
|
+
}
|
|
344
|
+
const parts = rel.split("/");
|
|
345
|
+
let expr = "script.Parent";
|
|
346
|
+
for (const part of parts) {
|
|
347
|
+
if (part === "..") {
|
|
348
|
+
expr += ".Parent";
|
|
349
|
+
} else if (part && part !== ".") {
|
|
350
|
+
expr += `.${part}`;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return `require(${expr})`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function emitLibraryLines(libraries) {
|
|
307
357
|
const api = [];
|
|
308
358
|
const types = [];
|
|
309
|
-
if (!libraries.length) {
|
|
310
|
-
return { api: "", types: "" };
|
|
311
|
-
}
|
|
312
|
-
api.push('const ReplicatedStorage = game:GetService("ReplicatedStorage")');
|
|
313
359
|
for (const spec of libraries) {
|
|
314
360
|
api.push(`const ${spec.bind} = ${requireCluauppLib(spec.file)}`);
|
|
315
361
|
const exported = TYPE_EXPORTS[spec.bind];
|
|
@@ -323,6 +369,15 @@ function emitRequireParts(libraries) {
|
|
|
323
369
|
}
|
|
324
370
|
}
|
|
325
371
|
}
|
|
372
|
+
return { api, types };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function emitRequireParts(libraries) {
|
|
376
|
+
if (!libraries.length) {
|
|
377
|
+
return { api: "", types: "" };
|
|
378
|
+
}
|
|
379
|
+
const { api, types } = emitLibraryLines(libraries);
|
|
380
|
+
api.unshift(SERVICE_GET.ReplicatedStorage);
|
|
326
381
|
return {
|
|
327
382
|
api: `${api.join("\n")}\n`,
|
|
328
383
|
types: types.length ? `${types.join("\n")}\n` : "",
|
|
@@ -339,34 +394,7 @@ function insertRequires(luau, libraries) {
|
|
|
339
394
|
if (!block) {
|
|
340
395
|
return luau;
|
|
341
396
|
}
|
|
342
|
-
|
|
343
|
-
if (!match) {
|
|
344
|
-
return `${block}\n${luau}`;
|
|
345
|
-
}
|
|
346
|
-
const insertAt = match[0].length;
|
|
347
|
-
const rest = luau.slice(insertAt).replace(/^\n*/, "\n");
|
|
348
|
-
return `${luau.slice(0, insertAt)}\n${block}\n${rest}`;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function robloxRequireFrom(fromOutRel, toOutRel) {
|
|
352
|
-
const fromDir = path.posix.dirname(String(fromOutRel || "module.luau").replace(/\\/g, "/"));
|
|
353
|
-
const toMod = String(toOutRel || "module.luau")
|
|
354
|
-
.replace(/\\/g, "/")
|
|
355
|
-
.replace(/\.luau$/i, "");
|
|
356
|
-
let rel = path.posix.relative(fromDir, toMod);
|
|
357
|
-
if (!rel || rel === ".") {
|
|
358
|
-
rel = path.posix.basename(toMod);
|
|
359
|
-
}
|
|
360
|
-
const parts = rel.split("/");
|
|
361
|
-
let expr = "script.Parent";
|
|
362
|
-
for (const part of parts) {
|
|
363
|
-
if (part === "..") {
|
|
364
|
-
expr += ".Parent";
|
|
365
|
-
} else if (part && part !== ".") {
|
|
366
|
-
expr += `.${part}`;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return `require(${expr})`;
|
|
397
|
+
return insertBlock(luau, block);
|
|
370
398
|
}
|
|
371
399
|
|
|
372
400
|
function moduleIsUsed(luau, spec) {
|
|
@@ -377,13 +405,11 @@ function moduleIsUsed(luau, spec) {
|
|
|
377
405
|
return exported.some((name) => String(luau).includes(name));
|
|
378
406
|
}
|
|
379
407
|
|
|
380
|
-
function
|
|
381
|
-
if (!modules || modules.length === 0) {
|
|
382
|
-
return luau;
|
|
383
|
-
}
|
|
408
|
+
function moduleRequireLines(modules, fromOutRel, luau) {
|
|
384
409
|
const lines = [];
|
|
410
|
+
const services = new Set();
|
|
385
411
|
const seen = new Set();
|
|
386
|
-
for (const spec of modules) {
|
|
412
|
+
for (const spec of modules || []) {
|
|
387
413
|
if (!spec || !spec.name || seen.has(spec.name)) {
|
|
388
414
|
continue;
|
|
389
415
|
}
|
|
@@ -391,20 +417,65 @@ function insertModuleRequires(luau, modules, fromOutRel) {
|
|
|
391
417
|
continue;
|
|
392
418
|
}
|
|
393
419
|
seen.add(spec.name);
|
|
420
|
+
const { root } = parseOutRel(spec.outRel);
|
|
421
|
+
if (root) {
|
|
422
|
+
services.add(root.service);
|
|
423
|
+
}
|
|
394
424
|
lines.push(`const ${spec.name} = ${robloxRequireFrom(fromOutRel, spec.outRel)}`);
|
|
395
425
|
for (const name of (spec.exports && spec.exports.consts) || []) {
|
|
396
426
|
if (name !== spec.name) {
|
|
397
427
|
lines.push(`const ${name} = ${spec.name}.${name}`);
|
|
398
428
|
}
|
|
399
429
|
}
|
|
400
|
-
if ((spec.exports && spec.exports.structs || []).includes(spec.name) &&
|
|
401
|
-
lines.push(`type ${spec.name} =
|
|
430
|
+
if (((spec.exports && spec.exports.structs) || []).includes(spec.name) || (spec.exports && spec.exports.hasProtos)) {
|
|
431
|
+
lines.push(`type ${spec.name} = ${spec.name}.${spec.name}`);
|
|
402
432
|
}
|
|
403
433
|
}
|
|
404
|
-
|
|
434
|
+
return { lines, services };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function insertModuleRequires(luau, modules, fromOutRel) {
|
|
438
|
+
return insertPreamble(luau, modules, [], fromOutRel);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function stripServiceGets(luau, services) {
|
|
442
|
+
let next = String(luau);
|
|
443
|
+
for (const name of services) {
|
|
444
|
+
const line = SERVICE_GET[name];
|
|
445
|
+
if (!line) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
next = next.replace(new RegExp(`^${line.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`, "m"), "");
|
|
449
|
+
}
|
|
450
|
+
return next;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function insertPreamble(luau, modules, libraries, fromOutRel) {
|
|
454
|
+
const text = String(luau || "");
|
|
455
|
+
const trimmed = text.trim();
|
|
456
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[") || /\.json$/i.test(String(fromOutRel || ""))) {
|
|
405
457
|
return luau;
|
|
406
458
|
}
|
|
407
|
-
|
|
459
|
+
const { lines: moduleLines, services } = moduleRequireLines(modules, fromOutRel, luau);
|
|
460
|
+
const libList = libraries || [];
|
|
461
|
+
if (libList.length) {
|
|
462
|
+
services.add("ReplicatedStorage");
|
|
463
|
+
}
|
|
464
|
+
if (moduleLines.length === 0 && libList.length === 0) {
|
|
465
|
+
return luau;
|
|
466
|
+
}
|
|
467
|
+
const { api: libApi, types: libTypes } = emitLibraryLines(libList);
|
|
468
|
+
const blockLines = [];
|
|
469
|
+
for (const name of SERVICE_ORDER) {
|
|
470
|
+
if (services.has(name)) {
|
|
471
|
+
blockLines.push(SERVICE_GET[name]);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
blockLines.push(...moduleLines);
|
|
475
|
+
blockLines.push(...libApi);
|
|
476
|
+
blockLines.push(...libTypes);
|
|
477
|
+
const body = stripServiceGets(luau, services);
|
|
478
|
+
return insertBlock(body, blockLines.join("\n"));
|
|
408
479
|
}
|
|
409
480
|
|
|
410
481
|
function insertBlock(luau, block) {
|
|
@@ -417,8 +488,9 @@ function insertBlock(luau, block) {
|
|
|
417
488
|
return `${luau.slice(0, insertAt)}\n${block}\n${rest}`;
|
|
418
489
|
}
|
|
419
490
|
|
|
420
|
-
|
|
491
|
+
export {
|
|
421
492
|
MODULES,
|
|
493
|
+
INCLUDE_TO_MODULE,
|
|
422
494
|
TYPE_EXPORTS,
|
|
423
495
|
EXTRA_TYPE_EXPORTS,
|
|
424
496
|
LIBRARY_TYPES,
|
|
@@ -431,4 +503,5 @@ module.exports = {
|
|
|
431
503
|
emitRequires,
|
|
432
504
|
insertRequires,
|
|
433
505
|
insertModuleRequires,
|
|
506
|
+
insertPreamble,
|
|
434
507
|
};
|
package/src/lsp.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
createConnection,
|
|
4
|
+
ProposedFeatures,
|
|
5
|
+
TextDocuments,
|
|
6
|
+
TextDocumentSyncKind,
|
|
7
|
+
} from "vscode-languageserver/node";
|
|
8
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
9
|
+
import { diagnosticsFor } from "./intellisense.js";
|
|
10
|
+
import { pkg } from "./package-info.js";
|
|
11
|
+
|
|
12
|
+
function uriPath(uri: string): string {
|
|
13
|
+
let file = String(uri || "").replace(/^file:\/\//, "");
|
|
14
|
+
if (/^\/[A-Za-z]:/.test(file)) {
|
|
15
|
+
file = file.slice(1);
|
|
16
|
+
}
|
|
17
|
+
return decodeURIComponent(file);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function start(options: { projectRoot?: string } = {}): void {
|
|
21
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
22
|
+
const connection = createConnection(ProposedFeatures.all);
|
|
23
|
+
const documents = new TextDocuments(TextDocument);
|
|
24
|
+
|
|
25
|
+
connection.onInitialize(() => ({
|
|
26
|
+
capabilities: {
|
|
27
|
+
textDocumentSync: TextDocumentSyncKind.Full,
|
|
28
|
+
},
|
|
29
|
+
serverInfo: { name: "cluaupp", version: pkg.version },
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const publish = (document: TextDocument) => {
|
|
33
|
+
const file = uriPath(document.uri);
|
|
34
|
+
const items = diagnosticsFor(document.getText(), file, {
|
|
35
|
+
filePath: file,
|
|
36
|
+
includeDirs: [path.dirname(file), path.join(projectRoot, "src"), path.join(projectRoot, "include")],
|
|
37
|
+
});
|
|
38
|
+
connection.sendDiagnostics({
|
|
39
|
+
uri: document.uri,
|
|
40
|
+
diagnostics: items.map((item) => ({
|
|
41
|
+
range: {
|
|
42
|
+
start: { line: Math.max(0, (item.line || 1) - 1), character: Math.max(0, (item.col || 1) - 1) },
|
|
43
|
+
end: { line: Math.max(0, (item.line || 1) - 1), character: Math.max(1, item.col || 1) },
|
|
44
|
+
},
|
|
45
|
+
severity: 1,
|
|
46
|
+
source: "cluaupp",
|
|
47
|
+
message: item.message,
|
|
48
|
+
})),
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
documents.onDidOpen((event) => publish(event.document));
|
|
53
|
+
documents.onDidChangeContent((event) => publish(event.document));
|
|
54
|
+
documents.onDidClose((event) => {
|
|
55
|
+
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
documents.listen(connection);
|
|
59
|
+
connection.listen();
|
|
60
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const projectRoot = path.resolve(__dirname, "..");
|
|
5
|
+
|
|
6
|
+
const fromPkg = createRequire(path.join(projectRoot, "package.json"));
|
|
7
|
+
export const pkg = fromPkg("./package.json") as { name: string; version: string };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const { tokenize } = require("./lex");
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { tokenize } from "./lex.js";
|
|
4
3
|
|
|
5
4
|
function parse(source, fileName) {
|
|
6
5
|
const tokens = tokenize(source);
|
|
@@ -628,4 +627,4 @@ function parse(source, fileName) {
|
|
|
628
627
|
return { type: "program", body: parseProgramInside(), fileName };
|
|
629
628
|
}
|
|
630
629
|
|
|
631
|
-
|
|
630
|
+
export { parse };
|