cluaupp 0.1.4 → 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.
- package/CHANGELOG.md +9 -0
- package/README.md +6 -6
- package/docs/README.md +1 -1
- package/docs/architecture.md +28 -78
- package/docs/cli.md +4 -5
- package/docs/comparison.md +3 -3
- package/docs/config.md +6 -6
- package/docs/cpp-organization.md +2 -2
- package/docs/getting-started.md +2 -1
- package/docs/intro.md +1 -1
- 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/package.json +1 -1
- package/src/architecture.js +73 -25
- package/src/cli.js +5 -13
- package/src/compile.js +62 -8
- package/src/emit.js +1 -1
- package/src/headers.js +234 -0
- package/src/libs.js +110 -42
- package/src/preprocess.js +48 -1
- package/src/understand.js +5 -3
- package/templates/game/cluaupp.config.json +2 -2
- package/templates/game/src/client/init.client.cpp +1 -0
- package/templates/game/src/server/leaderstats.server.cpp +1 -0
package/src/architecture.js
CHANGED
|
@@ -2,17 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const { emit } = require("./emit");
|
|
5
|
-
const {
|
|
5
|
+
const { requireCluauppLib } = require("./libs");
|
|
6
6
|
const { organizeDecls, janitorNames, emitSection, joinBlocks, SECTION, ENTRY_FN } = require("./layout");
|
|
7
7
|
const {
|
|
8
8
|
VALUE_CLASSES,
|
|
9
9
|
analyze,
|
|
10
10
|
toPascalServiceName,
|
|
11
11
|
legacyOutName,
|
|
12
|
+
modernScriptOutName,
|
|
12
13
|
} = require("./understand");
|
|
14
|
+
const { implOutName } = require("./preprocess");
|
|
13
15
|
|
|
14
16
|
function header(plan) {
|
|
15
|
-
const lines = [
|
|
17
|
+
const lines = [];
|
|
18
|
+
if (plan && plan.strict) {
|
|
19
|
+
lines.push("--!strict");
|
|
20
|
+
}
|
|
21
|
+
lines.push("-- Compiled by Cluaupp — C++ × Luau");
|
|
16
22
|
if (plan && plan.reasoning) {
|
|
17
23
|
for (const line of plan.reasoning) {
|
|
18
24
|
lines.push(`-- ${line}`);
|
|
@@ -328,9 +334,7 @@ function emitDomainController(plan, ast, options) {
|
|
|
328
334
|
return decl.type === "decl" || decl.type === "proto" || decl.type === "function" || decl.type === "expr";
|
|
329
335
|
});
|
|
330
336
|
const groups = organizeDecls(keep);
|
|
331
|
-
const subset = { type: "program", body: keep };
|
|
332
337
|
const name = plan.roles.domain;
|
|
333
|
-
const libs = collectLibraries(options.source || "", subset);
|
|
334
338
|
const entry = keep.find((decl) => decl.type === "function" && ENTRY_FN.test(decl.name || ""));
|
|
335
339
|
const bootExprs = keep.filter((decl) => decl.type === "expr");
|
|
336
340
|
const janitors = janitorNames(keep);
|
|
@@ -356,7 +360,7 @@ function emitDomainController(plan, ast, options) {
|
|
|
356
360
|
const startFn = `function ${name}.Start()\n ${name}.Stop()\n${boot}\nend`;
|
|
357
361
|
const stopFn = `function ${name}.Stop()\n${stopLines.join("\n")}\nend`;
|
|
358
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}`)}`;
|
|
359
|
-
return
|
|
363
|
+
return body;
|
|
360
364
|
}
|
|
361
365
|
|
|
362
366
|
function emitModule(plan, ast, options) {
|
|
@@ -366,7 +370,6 @@ function emitModule(plan, ast, options) {
|
|
|
366
370
|
function emitScriptMeta(runContext) {
|
|
367
371
|
return `${JSON.stringify(
|
|
368
372
|
{
|
|
369
|
-
className: "Script",
|
|
370
373
|
properties: {
|
|
371
374
|
RunContext: `Enum.RunContext.${runContext}`,
|
|
372
375
|
},
|
|
@@ -380,7 +383,7 @@ function serviceBootName(plan) {
|
|
|
380
383
|
if (plan.isClient) {
|
|
381
384
|
return "init.client.luau";
|
|
382
385
|
}
|
|
383
|
-
return "init.luau";
|
|
386
|
+
return "init.server.luau";
|
|
384
387
|
}
|
|
385
388
|
|
|
386
389
|
function serviceFiles(plan, ast, options, dir) {
|
|
@@ -404,33 +407,75 @@ function serviceFiles(plan, ast, options, dir) {
|
|
|
404
407
|
return files;
|
|
405
408
|
}
|
|
406
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
|
+
|
|
407
428
|
function planOutput(ast, fileName, options = {}) {
|
|
408
429
|
const plan = analyze(ast, fileName);
|
|
430
|
+
plan.strict = options.strict === true;
|
|
409
431
|
const rel = (options.relativeName || fileName).replace(/\\/g, "/");
|
|
410
432
|
const dir = path.dirname(rel).replace(/^\.$/, "");
|
|
411
433
|
const outDir = dir && dir !== "." ? dir : "";
|
|
412
434
|
const prefix = outDir ? `${outDir}/` : "";
|
|
435
|
+
const stale = serviceFolderStale(plan, rel);
|
|
413
436
|
|
|
414
|
-
if (options.
|
|
415
|
-
return {
|
|
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
|
+
};
|
|
416
444
|
}
|
|
417
445
|
|
|
418
|
-
if (
|
|
419
|
-
|
|
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) };
|
|
420
475
|
}
|
|
421
476
|
|
|
422
|
-
if (plan.kind === "
|
|
423
|
-
|
|
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`];
|
|
428
|
-
return {
|
|
429
|
-
kind: "service",
|
|
430
|
-
plan,
|
|
431
|
-
files: serviceFiles(plan, ast, options, serviceDir),
|
|
432
|
-
stale,
|
|
433
|
-
};
|
|
477
|
+
if (plan.kind === "legacy") {
|
|
478
|
+
return { kind: "legacy", plan, files: null, stale, outName: legacyOutName(rel) };
|
|
434
479
|
}
|
|
435
480
|
|
|
436
481
|
if (plan.kind === "config" || plan.kind === "module") {
|
|
@@ -438,11 +483,14 @@ function planOutput(ast, fileName, options = {}) {
|
|
|
438
483
|
kind: plan.kind,
|
|
439
484
|
plan,
|
|
440
485
|
files: [{ name: `${prefix}${plan.serviceName}.luau`, contents: emitModule(plan, ast, options) }],
|
|
441
|
-
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],
|
|
442
487
|
};
|
|
443
488
|
}
|
|
444
489
|
|
|
445
|
-
|
|
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 };
|
|
446
494
|
}
|
|
447
495
|
|
|
448
496
|
module.exports = {
|
package/src/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const { compileService } = require("./compile");
|
|
6
|
-
const { isSourceFile,
|
|
6
|
+
const { isSourceFile, toLuauPath } = require("./preprocess");
|
|
7
7
|
const { syncEditorSupport, installEditorSupport } = require("./intellisense");
|
|
8
8
|
const pkg = require("../package.json");
|
|
9
9
|
|
|
@@ -12,7 +12,7 @@ function printHelp() {
|
|
|
12
12
|
|
|
13
13
|
Usage:
|
|
14
14
|
cluaupp init [folder] create a game (src/server, src/client, src/shared)
|
|
15
|
-
cluaupp build [folder] transpile src → out (
|
|
15
|
+
cluaupp build [folder] transpile src → out (one file per .cpp)
|
|
16
16
|
cluaupp watch [folder] rebuild on save
|
|
17
17
|
cluaupp lsp [folder] language server (stdio JSON-RPC)
|
|
18
18
|
cluaupp intellisense install Cursor/VS Code IntelliSense
|
|
@@ -110,10 +110,10 @@ function loadConfig(root) {
|
|
|
110
110
|
for (const name of configNames) {
|
|
111
111
|
const file = path.join(root, name);
|
|
112
112
|
if (fs.existsSync(file)) {
|
|
113
|
-
return { rootDir: "src", outDir: "out", strict:
|
|
113
|
+
return { rootDir: "src", outDir: "out", strict: false, architecture: false, ...JSON.parse(fs.readFileSync(file, "utf8")) };
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
-
return { rootDir: "src", outDir: "out", strict:
|
|
116
|
+
return { rootDir: "src", outDir: "out", strict: false, architecture: false };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
function collectCpp(dir, files = []) {
|
|
@@ -146,10 +146,6 @@ function collectFiles(dir, files = []) {
|
|
|
146
146
|
return files;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
function siblingCpp(file) {
|
|
150
|
-
return siblingImplementation(file) || file.replace(/\.(h|hpp|hh)$/i, ".cpp");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
149
|
function resolveKey(file) {
|
|
154
150
|
const resolved = path.resolve(file);
|
|
155
151
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
@@ -223,14 +219,10 @@ function pruneOut(root, config, written, failedPrefixes) {
|
|
|
223
219
|
function compileProject(root, config) {
|
|
224
220
|
const srcDir = path.join(root, config.rootDir);
|
|
225
221
|
const files = collectCpp(srcDir);
|
|
226
|
-
const fileSet = new Set(files.map((file) => path.resolve(file)));
|
|
227
222
|
const jobs = [];
|
|
228
223
|
const errors = [];
|
|
229
224
|
|
|
230
225
|
for (const file of files) {
|
|
231
|
-
if (isHeaderFile(file) && fileSet.has(path.resolve(siblingCpp(file)))) {
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
226
|
const source = fs.readFileSync(file, "utf8");
|
|
235
227
|
const rel = posixRel(srcDir, file);
|
|
236
228
|
try {
|
|
@@ -239,7 +231,7 @@ function compileProject(root, config) {
|
|
|
239
231
|
filePath: file,
|
|
240
232
|
relativeName: rel,
|
|
241
233
|
outName: toLuauPath(rel),
|
|
242
|
-
architecture: config.architecture
|
|
234
|
+
architecture: config.architecture === true,
|
|
243
235
|
includeDirs: [path.dirname(file), srcDir, path.join(root, "include")],
|
|
244
236
|
srcDir,
|
|
245
237
|
});
|
package/src/compile.js
CHANGED
|
@@ -1,35 +1,89 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const path = require("path");
|
|
3
4
|
const { parse } = require("./parse");
|
|
4
5
|
const { emit } = require("./emit");
|
|
5
|
-
const { preprocess } = require("./preprocess");
|
|
6
|
-
const { collectLibraries,
|
|
6
|
+
const { preprocess, isHeaderFile, toLuauPath, siblingImplementation, siblingHeader, implOutName } = require("./preprocess");
|
|
7
|
+
const { collectLibraries, insertPreamble } = require("./libs");
|
|
7
8
|
const { planOutput } = require("./architecture");
|
|
9
|
+
const { emitHeaderModule, primaryHeaderName } = require("./headers");
|
|
10
|
+
|
|
11
|
+
function shouldAttachRequires(name) {
|
|
12
|
+
return !/\.(json)$/i.test(String(name || ""));
|
|
13
|
+
}
|
|
8
14
|
|
|
9
15
|
function attachRequires(contents, source, ast, options, outName) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
if (!shouldAttachRequires(outName)) {
|
|
17
|
+
return contents;
|
|
18
|
+
}
|
|
19
|
+
const libraries = String(contents).includes("CluauppLibs") ? [] : collectLibraries(source, ast);
|
|
20
|
+
return insertPreamble(contents, options.moduleIncludes || [], libraries, outName);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function posixRel(from, file) {
|
|
24
|
+
return path.relative(from, file).replace(/\\/g, "/");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function wireHeaderImpl(ast, fileName, options) {
|
|
28
|
+
const headerPath = options.filePath || null;
|
|
29
|
+
const impl = headerPath ? siblingImplementation(headerPath) : null;
|
|
30
|
+
if (!impl) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const typeName = primaryHeaderName(ast, options.relativeName || fileName);
|
|
34
|
+
const implRel = options.srcDir ? posixRel(options.srcDir, impl) : path.basename(impl).replace(/\\/g, "/");
|
|
35
|
+
const implOut = implOutName(implRel);
|
|
36
|
+
options.headerImplName = `${typeName}Module`;
|
|
37
|
+
options.moduleIncludes = options.moduleIncludes || [];
|
|
38
|
+
if (!options.moduleIncludes.some((spec) => spec.name === options.headerImplName)) {
|
|
39
|
+
options.moduleIncludes.push({
|
|
40
|
+
name: options.headerImplName,
|
|
41
|
+
outRel: implOut,
|
|
42
|
+
exports: { consts: [], structs: [] },
|
|
43
|
+
});
|
|
13
44
|
}
|
|
14
|
-
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compileHeader(source, fileName, options, prepared, ast) {
|
|
48
|
+
wireHeaderImpl(ast, fileName, options);
|
|
49
|
+
const rel = (options.relativeName || fileName || "header.h").replace(/\\/g, "/");
|
|
50
|
+
const outName = toLuauPath(rel);
|
|
51
|
+
const luau = attachRequires(emitHeaderModule(ast, options), source, ast, options, outName);
|
|
52
|
+
return {
|
|
53
|
+
kind: "module",
|
|
54
|
+
plan: { kind: "module", strict: options.strict === true },
|
|
55
|
+
files: [{ name: outName, contents: luau }],
|
|
56
|
+
stale: [],
|
|
57
|
+
};
|
|
15
58
|
}
|
|
16
59
|
|
|
17
60
|
function compileSource(source, fileName, options = {}) {
|
|
18
61
|
options.moduleIncludes = options.moduleIncludes || [];
|
|
19
|
-
const prepared =
|
|
62
|
+
const prepared = preprocess(source, options.filePath || null, options);
|
|
20
63
|
const ast = parse(prepared, fileName || "input.cpp");
|
|
64
|
+
if (isHeaderFile(fileName) || isHeaderFile(options.filePath || "")) {
|
|
65
|
+
return compileHeader(source, fileName, options, prepared, ast).files[0].contents;
|
|
66
|
+
}
|
|
21
67
|
const luau = emit(ast, options);
|
|
22
68
|
return attachRequires(luau, source, ast, options, options.outName || fileName);
|
|
23
69
|
}
|
|
24
70
|
|
|
25
71
|
function compileService(source, fileName, options = {}) {
|
|
26
72
|
options.moduleIncludes = options.moduleIncludes || [];
|
|
27
|
-
const prepared =
|
|
73
|
+
const prepared = preprocess(source, options.filePath || null, options);
|
|
28
74
|
const ast = parse(prepared, fileName || "input.cpp");
|
|
75
|
+
if (isHeaderFile(fileName) || isHeaderFile(options.filePath || "") || isHeaderFile(options.relativeName || "")) {
|
|
76
|
+
return compileHeader(source, fileName, options, prepared, ast);
|
|
77
|
+
}
|
|
78
|
+
const implHeader = options.filePath ? siblingHeader(options.filePath) : null;
|
|
79
|
+
if (implHeader) {
|
|
80
|
+
options.skipInit = true;
|
|
81
|
+
}
|
|
29
82
|
const planned = planOutput(ast, fileName || "input.cpp", {
|
|
30
83
|
...options,
|
|
31
84
|
relativeName: options.relativeName || fileName,
|
|
32
85
|
source: prepared,
|
|
86
|
+
siblingHeader: implHeader,
|
|
33
87
|
});
|
|
34
88
|
if (planned.files) {
|
|
35
89
|
return {
|
package/src/emit.js
CHANGED
|
@@ -4,7 +4,7 @@ const { isInstanceType, isMethod, isDatatype, luauType } = require("./api");
|
|
|
4
4
|
const { isLibraryType, isLibraryMethod, MODULE_COLON } = require("./libs");
|
|
5
5
|
|
|
6
6
|
function emit(ast, options = {}) {
|
|
7
|
-
const strict = options.strict
|
|
7
|
+
const strict = options.strict === true;
|
|
8
8
|
const lines = [];
|
|
9
9
|
if (!options.skipHeader) {
|
|
10
10
|
if (strict) {
|
package/src/headers.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { luauType } = require("./api");
|
|
5
|
+
|
|
6
|
+
function fileStem(fileName) {
|
|
7
|
+
return path.basename(String(fileName || "module")).replace(/\.(h|hpp|hh|cpp|cc|cxx|c)$/i, "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function emitValue(node) {
|
|
11
|
+
if (!node) {
|
|
12
|
+
return "nil";
|
|
13
|
+
}
|
|
14
|
+
if (node.type === "number") {
|
|
15
|
+
return node.value;
|
|
16
|
+
}
|
|
17
|
+
if (node.type === "string") {
|
|
18
|
+
return `"${node.value}"`;
|
|
19
|
+
}
|
|
20
|
+
if (node.type === "bool") {
|
|
21
|
+
return node.value ? "true" : "false";
|
|
22
|
+
}
|
|
23
|
+
if (node.type === "null") {
|
|
24
|
+
return "nil";
|
|
25
|
+
}
|
|
26
|
+
if (node.type === "initlist") {
|
|
27
|
+
const inner = (node.fields || []).map((field) => `${field.name} = ${emitValue(field.value)}`).join(", ");
|
|
28
|
+
return `{ ${inner} }`;
|
|
29
|
+
}
|
|
30
|
+
return "nil";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectHeaderShape(ast, fileName) {
|
|
34
|
+
const owners = new Map();
|
|
35
|
+
const ensure = (name) => {
|
|
36
|
+
if (!owners.has(name)) {
|
|
37
|
+
owners.set(name, { fields: [], methods: [] });
|
|
38
|
+
}
|
|
39
|
+
return owners.get(name);
|
|
40
|
+
};
|
|
41
|
+
const freeProtos = [];
|
|
42
|
+
const consts = [];
|
|
43
|
+
for (const decl of ast.body || []) {
|
|
44
|
+
if (decl.type === "decl" && decl.owner) {
|
|
45
|
+
ensure(decl.owner).fields.push(decl);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (decl.type === "decl") {
|
|
49
|
+
consts.push(decl);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if ((decl.type === "proto" || decl.type === "function") && decl.owner) {
|
|
53
|
+
ensure(decl.owner).methods.push(decl);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (decl.type === "proto") {
|
|
57
|
+
freeProtos.push(decl);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const nested = new Set();
|
|
61
|
+
for (const [ownerName, spec] of owners) {
|
|
62
|
+
for (const field of spec.fields) {
|
|
63
|
+
if (field.valueType && owners.has(field.valueType) && field.valueType !== ownerName) {
|
|
64
|
+
nested.add(field.valueType);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const roots = [...owners.keys()].filter((name) => !nested.has(name));
|
|
69
|
+
const withMethods = roots.filter((name) => ensure(name).methods.length > 0);
|
|
70
|
+
const stem = fileStem(fileName);
|
|
71
|
+
let primary = withMethods[0] || (freeProtos.length ? stem : roots[0] || stem);
|
|
72
|
+
return { owners, nested, roots, freeProtos, consts, primary, stem };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function primaryHeaderName(ast, fileName) {
|
|
76
|
+
return collectHeaderShape(ast, fileName).primary;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function methodType(fn, owner) {
|
|
80
|
+
const params = [];
|
|
81
|
+
if (owner) {
|
|
82
|
+
params.push(`self: ${owner}`);
|
|
83
|
+
}
|
|
84
|
+
for (const param of fn.params || []) {
|
|
85
|
+
if (typeof param === "string") {
|
|
86
|
+
params.push(param);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const typeAnn = luauType(param.valueType);
|
|
90
|
+
params.push(typeAnn ? `${param.name}: ${typeAnn}` : param.name);
|
|
91
|
+
}
|
|
92
|
+
const ret = luauType(fn.returnType);
|
|
93
|
+
const retAnn = !ret || ret === "()" ? "()" : ret;
|
|
94
|
+
return `(${params.join(", ")}) -> ${retAnn}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function fieldType(field, owners) {
|
|
98
|
+
if (field.valueType && owners.has(field.valueType)) {
|
|
99
|
+
return field.valueType;
|
|
100
|
+
}
|
|
101
|
+
return luauType(field.valueType) || "any";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function emitExportType(lines, name, spec, owners, extraMethods, extraFields) {
|
|
105
|
+
lines.push(`export type ${name} = {`);
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
for (const field of [...((spec && spec.fields) || []), ...(extraFields || [])]) {
|
|
108
|
+
if (!field.name || seen.has(field.name)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
seen.add(field.name);
|
|
112
|
+
lines.push(` ${field.name}: ${fieldType(field, owners)},`);
|
|
113
|
+
}
|
|
114
|
+
for (const method of (spec && spec.methods) || []) {
|
|
115
|
+
if (!method.name || seen.has(method.name)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
seen.add(method.name);
|
|
119
|
+
lines.push(` ${method.name}: ${methodType(method, name)},`);
|
|
120
|
+
}
|
|
121
|
+
for (const method of extraMethods || []) {
|
|
122
|
+
if (!method.name || seen.has(method.name)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
seen.add(method.name);
|
|
126
|
+
lines.push(` ${method.name}: ${methodType(method, null)},`);
|
|
127
|
+
}
|
|
128
|
+
lines.push("}");
|
|
129
|
+
lines.push("");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function emitDataConstructor(lines, name, spec, owners) {
|
|
133
|
+
const emitField = (field) => {
|
|
134
|
+
if (field.valueType && owners.has(field.valueType)) {
|
|
135
|
+
const nested = owners.get(field.valueType);
|
|
136
|
+
const inner = nested.fields.map((item) => `${item.name} = ${emitValue(item.value)}`).join(", ");
|
|
137
|
+
return `{ ${inner} }`;
|
|
138
|
+
}
|
|
139
|
+
if (field.value && field.value.type === "initlist") {
|
|
140
|
+
return emitValue(field.value);
|
|
141
|
+
}
|
|
142
|
+
return emitValue(field.value);
|
|
143
|
+
};
|
|
144
|
+
lines.push(`const function ${name}(): ${name}`);
|
|
145
|
+
lines.push(" return {");
|
|
146
|
+
for (const field of spec.fields) {
|
|
147
|
+
lines.push(` ${field.name} = ${emitField(field)},`);
|
|
148
|
+
}
|
|
149
|
+
lines.push(" }");
|
|
150
|
+
lines.push("end");
|
|
151
|
+
lines.push("");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function emitHeaderModule(ast, options = {}) {
|
|
155
|
+
const strict = options.strict === true;
|
|
156
|
+
const fileName = options.relativeName || options.filePath || "header.h";
|
|
157
|
+
const shape = collectHeaderShape(ast, fileName);
|
|
158
|
+
const lines = [];
|
|
159
|
+
if (strict) {
|
|
160
|
+
lines.push("--!strict");
|
|
161
|
+
}
|
|
162
|
+
lines.push("-- Compiled by Cluaupp — C++ × Luau");
|
|
163
|
+
lines.push("");
|
|
164
|
+
|
|
165
|
+
const typeOrder = [...shape.nested, ...shape.roots];
|
|
166
|
+
for (const name of typeOrder) {
|
|
167
|
+
const extra = name === shape.primary ? shape.freeProtos : [];
|
|
168
|
+
emitExportType(lines, name, shape.owners.get(name), shape.owners, extra);
|
|
169
|
+
}
|
|
170
|
+
if (shape.freeProtos.length && !shape.owners.has(shape.primary)) {
|
|
171
|
+
emitExportType(lines, shape.primary, { fields: [], methods: [] }, shape.owners, shape.freeProtos, shape.consts);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const decl of shape.consts) {
|
|
175
|
+
const typeAnn = luauType(decl.valueType);
|
|
176
|
+
const typed = typeAnn ? `: ${typeAnn}` : "";
|
|
177
|
+
lines.push(`const ${decl.name}${typed} = ${emitValue(decl.value)}`);
|
|
178
|
+
}
|
|
179
|
+
if (shape.consts.length) {
|
|
180
|
+
lines.push("");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const hasMethods = shape.roots.some((name) => shape.owners.get(name).methods.length > 0) || shape.freeProtos.length > 0;
|
|
184
|
+
const implName = hasMethods ? options.headerImplName : null;
|
|
185
|
+
|
|
186
|
+
if (!hasMethods && shape.roots.length > 0) {
|
|
187
|
+
for (const name of shape.roots) {
|
|
188
|
+
emitDataConstructor(lines, name, shape.owners.get(name), shape.owners);
|
|
189
|
+
}
|
|
190
|
+
lines.push(`return ${shape.primary}`);
|
|
191
|
+
lines.push("");
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!hasMethods && shape.consts.length > 0) {
|
|
196
|
+
lines.push("return {");
|
|
197
|
+
for (const decl of shape.consts) {
|
|
198
|
+
lines.push(` ${decl.name} = ${decl.name},`);
|
|
199
|
+
}
|
|
200
|
+
lines.push("}");
|
|
201
|
+
lines.push("");
|
|
202
|
+
return lines.join("\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const tableName = shape.primary;
|
|
206
|
+
const spec = shape.owners.get(tableName) || { fields: [], methods: [] };
|
|
207
|
+
const methods = [...spec.methods, ...shape.freeProtos];
|
|
208
|
+
lines.push(`const ${tableName}: ${tableName} = {`);
|
|
209
|
+
if (implName) {
|
|
210
|
+
for (const field of spec.fields) {
|
|
211
|
+
if (field.valueType && shape.owners.has(field.valueType) && field.valueType !== tableName) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
lines.push(` ${field.name} = ${implName}.${field.name},`);
|
|
215
|
+
}
|
|
216
|
+
for (const method of methods) {
|
|
217
|
+
lines.push(` ${method.name} = ${implName}.${method.name},`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const decl of shape.consts) {
|
|
221
|
+
lines.push(` ${decl.name} = ${decl.name},`);
|
|
222
|
+
}
|
|
223
|
+
lines.push("}");
|
|
224
|
+
lines.push("");
|
|
225
|
+
lines.push(`return ${tableName}`);
|
|
226
|
+
lines.push("");
|
|
227
|
+
return lines.join("\n");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
emitHeaderModule,
|
|
232
|
+
primaryHeaderName,
|
|
233
|
+
collectHeaderShape,
|
|
234
|
+
};
|