cluaupp 0.1.0 → 0.1.2
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 +13 -0
- package/docs/cli.md +3 -1
- package/docs/libraries.md +4 -1
- package/docs/syntax.md +19 -0
- package/include/cluaupp/libs/dataservice.hpp +5 -1
- package/package.json +2 -2
- package/src/cli.js +273 -59
- package/src/emit.js +7 -0
- package/src/parse.js +29 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
- Watch does not write `out/` while any file fails to compile (Studio keeps the last good scripts).
|
|
6
|
+
- Watch/build skip rewriting Luau whose contents did not change (a space in C++ no longer floods Rojo).
|
|
7
|
+
- `libs/` is fill-only: missing files are restored, existing files are never overwritten or deleted. Fixes Rojo 7 crashing on `libs/ArrayIndexer` when watch/build raced a live serve.
|
|
8
|
+
- Dropped `$optional` from `default.project.json` (Rojo 7.7 failed to deserialize it).
|
|
9
|
+
|
|
10
|
+
## 0.1.1
|
|
11
|
+
|
|
12
|
+
- DataService: `DataServiceOptions<T>.Template` is the required player-data table (`T` matches the save struct). Also documents `Exclude`.
|
|
13
|
+
- Compiler: C++ designated initializers (`Type { .Field = value }`) emit Luau tables, so `DataService.Server:Init({ Template = ... })` type-checks and compiles.
|
|
14
|
+
- Watch/build: delete orphaned `out/` files when a source is removed. Never wipe `out/` or `libs/` as a whole — Rojo 7 crashes if `libs/ArrayIndexer` disappears mid-serve. Vendor files are only created if missing; existing `libs/` is never overwritten on watch/build. Watch skips writing `out/` when compile fails, and skips rewriting Luau whose contents did not change.
|
|
15
|
+
|
|
3
16
|
## 0.1.0
|
|
4
17
|
|
|
5
18
|
- Product name: **Cluaupp** (C++ × Luau)
|
package/docs/cli.md
CHANGED
|
@@ -31,6 +31,8 @@ Transpiles `src/**/*.{cpp,h,hpp}` into `out/`. Script files become PascalCase **
|
|
|
31
31
|
- `.h` / `.hpp` with a sibling `.cpp` are not emitted twice
|
|
32
32
|
- `new Folder(parent)` in a stats system becomes `CacheController.Ensure`
|
|
33
33
|
- A parse error exits with code 1 and `file:line:column`
|
|
34
|
+
- After emit, **orphans in `out/` are removed** (source deleted → matching Luau deleted). `out/` itself is never wiped, so a running Rojo serve keeps the live tree.
|
|
35
|
+
- `libs/` and `include/cluaupp` are synced by writing missing/changed files only. They are **never** deleted as a folder — Rojo 7 unwrap-crashes if `libs/ArrayIndexer` vanishes while serving.
|
|
34
36
|
|
|
35
37
|
```bash
|
|
36
38
|
cluaupp build
|
|
@@ -39,7 +41,7 @@ cluaupp build ./my-game
|
|
|
39
41
|
|
|
40
42
|
## `cluaupp watch [folder]`
|
|
41
43
|
|
|
42
|
-
Runs a `build` and rebuilds when
|
|
44
|
+
Runs a `build` and rebuilds when anything under `src/` changes, including deletes. Parse errors are printed; the watcher stays alive and **does not write `out/`** until the project compiles cleanly (Studio keeps the last good scripts). Rebuilds are debounced and do **not** recopy `libs/`. Deleted `.cpp` files prune their `out/` artifacts on the next successful compile.
|
|
43
45
|
|
|
44
46
|
## `cluaupp --version` / `cluaupp -v`
|
|
45
47
|
|
package/docs/libraries.md
CHANGED
|
@@ -67,7 +67,10 @@ void init() {
|
|
|
67
67
|
auto* coins = Net::Event("Coins");
|
|
68
68
|
coins->On(OnCoins);
|
|
69
69
|
print(FormatNumber::Abbreviate(1500));
|
|
70
|
-
DataService::
|
|
70
|
+
DataService::Server.Init(DataServiceOptions {
|
|
71
|
+
.Template = { .Coins = 0 },
|
|
72
|
+
.StoreName = "PlayerData",
|
|
73
|
+
});
|
|
71
74
|
janitor->Add(coins);
|
|
72
75
|
}
|
|
73
76
|
```
|
package/docs/syntax.md
CHANGED
|
@@ -108,6 +108,25 @@ The only `for` accepted today is **range-for**: `for (auto* x : list)`. C-style
|
|
|
108
108
|
| `obj->Method(a)` | `obj:Method(a)` if the method is a Roblox API |
|
|
109
109
|
| `fn(a)` | `fn(a)` |
|
|
110
110
|
| `signal.Connect(fn)` | `signal:Connect(fn)` |
|
|
111
|
+
| `Type { .Field = value }` | `{ Field = value }` |
|
|
112
|
+
|
|
113
|
+
Designated initializers become Luau tables. Nested braces work the same way:
|
|
114
|
+
|
|
115
|
+
```cpp
|
|
116
|
+
DataService::Server.Init(DataServiceOptions {
|
|
117
|
+
.Template = playerData,
|
|
118
|
+
.StoreName = "PlayerData",
|
|
119
|
+
.UseMock = true,
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```luau
|
|
124
|
+
DataService.Server:Init({
|
|
125
|
+
Template = playerData,
|
|
126
|
+
StoreName = "PlayerData",
|
|
127
|
+
UseMock = true,
|
|
128
|
+
})
|
|
129
|
+
```
|
|
111
130
|
|
|
112
131
|
## `new` and services
|
|
113
132
|
|
|
@@ -34,10 +34,13 @@ struct DataServiceEnum {
|
|
|
34
34
|
DataOrderList OrderList;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
template <typename T>
|
|
37
38
|
struct DataServiceOptions {
|
|
39
|
+
T Template;
|
|
38
40
|
string StoreName;
|
|
39
41
|
bool UseMock;
|
|
40
42
|
string KeyPrefix;
|
|
43
|
+
LuaArray<DataPath> Exclude;
|
|
41
44
|
bool StrictPaths;
|
|
42
45
|
bool AutoCreateMissingTables;
|
|
43
46
|
};
|
|
@@ -112,7 +115,8 @@ public:
|
|
|
112
115
|
class DataServiceServer {
|
|
113
116
|
public:
|
|
114
117
|
DataPath Paths;
|
|
115
|
-
|
|
118
|
+
template <typename T>
|
|
119
|
+
DataServiceServer* Init(DataServiceOptions<T> options);
|
|
116
120
|
Data* WaitFor(Player* player);
|
|
117
121
|
Data* Get(Player* player);
|
|
118
122
|
bool HasData(Player* player);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cluaupp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Cluaupp — the definitive merge of C++ and modern Luau. Source-to-source transpiler with first-class Roblox APIs.",
|
|
5
5
|
"author": "KartzDev",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"cluau": "node bin/cluaupp.js",
|
|
30
30
|
"build": "node bin/cluaupp.js build",
|
|
31
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",
|
|
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
33
|
"generate-api": "node scripts/generate-api.js",
|
|
34
34
|
"check-highlight": "node scripts/check-highlight.js",
|
|
35
35
|
"vendor-libs": "node scripts/vendor-libs.js",
|
package/src/cli.js
CHANGED
|
@@ -34,27 +34,72 @@ function copyDir(from, to) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function tryRm(target) {
|
|
38
|
+
try {
|
|
39
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
40
|
+
return true;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err.code === "EPERM" || err.code === "EBUSY" || err.code === "ENOTEMPTY") {
|
|
43
|
+
console.error("cluaupp: skip remove", target, `(${err.code})`);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function copyFileIfChanged(src, dest, mode = "fill") {
|
|
51
|
+
if (mode === "fill" && fs.existsSync(dest)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (mode === "update" && fs.existsSync(dest)) {
|
|
55
|
+
const from = fs.statSync(src);
|
|
56
|
+
const to = fs.statSync(dest);
|
|
57
|
+
if (from.size === to.size && from.mtimeMs <= to.mtimeMs) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
62
|
+
try {
|
|
63
|
+
fs.copyFileSync(src, dest);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err.code === "EPERM" || err.code === "EBUSY") {
|
|
66
|
+
console.error("cluaupp: skip copy", dest, `(${err.code})`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function syncDir(from, to, mode = "fill") {
|
|
74
|
+
if (!fs.existsSync(from)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
fs.mkdirSync(to, { recursive: true });
|
|
78
|
+
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
79
|
+
const src = path.join(from, entry.name);
|
|
80
|
+
const dest = path.join(to, entry.name);
|
|
81
|
+
if (entry.isDirectory()) {
|
|
82
|
+
syncDir(src, dest, mode);
|
|
83
|
+
} else {
|
|
84
|
+
copyFileIfChanged(src, dest, mode);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
37
89
|
function copyRuntime(dest) {
|
|
38
90
|
const runtime = path.join(__dirname, "..", "runtime");
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
fs.rmSync(libs, { recursive: true, force: true });
|
|
42
|
-
}
|
|
43
|
-
if (fs.existsSync(runtime)) {
|
|
44
|
-
copyDir(runtime, libs);
|
|
91
|
+
if (!fs.existsSync(runtime)) {
|
|
92
|
+
return;
|
|
45
93
|
}
|
|
94
|
+
syncDir(runtime, path.join(dest, "libs"));
|
|
46
95
|
}
|
|
47
96
|
|
|
48
97
|
function copyHeaders(dest) {
|
|
49
98
|
const from = path.join(__dirname, "..", "include", "cluaupp");
|
|
50
|
-
const to = path.join(dest, "include", "cluaupp");
|
|
51
99
|
if (!fs.existsSync(from)) {
|
|
52
100
|
return;
|
|
53
101
|
}
|
|
54
|
-
|
|
55
|
-
fs.rmSync(to, { recursive: true, force: true });
|
|
56
|
-
}
|
|
57
|
-
copyDir(from, to);
|
|
102
|
+
syncDir(from, path.join(dest, "include", "cluaupp"));
|
|
58
103
|
}
|
|
59
104
|
|
|
60
105
|
function loadConfig(root) {
|
|
@@ -83,6 +128,21 @@ function collectCpp(dir, files = []) {
|
|
|
83
128
|
return files;
|
|
84
129
|
}
|
|
85
130
|
|
|
131
|
+
function collectFiles(dir, files = []) {
|
|
132
|
+
if (!fs.existsSync(dir)) {
|
|
133
|
+
return files;
|
|
134
|
+
}
|
|
135
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
136
|
+
const full = path.join(dir, entry.name);
|
|
137
|
+
if (entry.isDirectory()) {
|
|
138
|
+
collectFiles(full, files);
|
|
139
|
+
} else {
|
|
140
|
+
files.push(full);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return files;
|
|
144
|
+
}
|
|
145
|
+
|
|
86
146
|
function siblingCpp(file) {
|
|
87
147
|
return file.replace(/\.(h|hpp|hh)$/i, ".cpp");
|
|
88
148
|
}
|
|
@@ -92,29 +152,84 @@ function resolveKey(file) {
|
|
|
92
152
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
93
153
|
}
|
|
94
154
|
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
155
|
+
function posixRel(from, file) {
|
|
156
|
+
return path.relative(from, file).replace(/\\/g, "/");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function sourcePrefixes(rel) {
|
|
160
|
+
const noExt = rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, "");
|
|
161
|
+
const noTag = noExt.replace(/\.(server|client)$/i, "");
|
|
162
|
+
return [...new Set([noExt, noTag, toLuauPath(rel).replace(/\\/g, "/")])];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function matchesPrefix(relOut, prefixes) {
|
|
166
|
+
const n = relOut.replace(/\\/g, "/").toLowerCase();
|
|
167
|
+
return prefixes.some((prefix) => {
|
|
168
|
+
const k = prefix.replace(/\\/g, "/").toLowerCase();
|
|
169
|
+
return n === k || n === `${k}.luau` || n.startsWith(`${k}/`) || n.startsWith(`${k}.`);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function removeEmptyOutDirs(outDir, projectRoot) {
|
|
174
|
+
if (!fs.existsSync(outDir)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const walk = (dir) => {
|
|
178
|
+
if (resolveKey(dir) === resolveKey(outDir)) {
|
|
179
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
180
|
+
if (entry.isDirectory()) {
|
|
181
|
+
walk(path.join(dir, entry.name));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
187
|
+
if (entry.isDirectory()) {
|
|
188
|
+
walk(path.join(dir, entry.name));
|
|
189
|
+
}
|
|
105
190
|
}
|
|
191
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
|
|
192
|
+
if (tryRm(dir)) {
|
|
193
|
+
console.log("cluaupp: removed", path.relative(projectRoot, dir));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
walk(outDir);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function pruneOut(root, config, written, failedPrefixes) {
|
|
201
|
+
const outDir = path.join(root, config.outDir);
|
|
202
|
+
if (!fs.existsSync(outDir)) {
|
|
106
203
|
return;
|
|
107
204
|
}
|
|
205
|
+
for (const file of collectFiles(outDir)) {
|
|
206
|
+
if (written.has(resolveKey(file))) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const rel = posixRel(outDir, file);
|
|
210
|
+
if (matchesPrefix(rel, failedPrefixes)) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (tryRm(file)) {
|
|
214
|
+
console.log("cluaupp: removed", path.relative(root, file));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
removeEmptyOutDirs(outDir, root);
|
|
218
|
+
}
|
|
108
219
|
|
|
220
|
+
function compileProject(root, config) {
|
|
221
|
+
const srcDir = path.join(root, config.rootDir);
|
|
222
|
+
const files = collectCpp(srcDir);
|
|
109
223
|
const fileSet = new Set(files.map((file) => path.resolve(file)));
|
|
110
|
-
|
|
111
|
-
const
|
|
224
|
+
const jobs = [];
|
|
225
|
+
const errors = [];
|
|
226
|
+
|
|
112
227
|
for (const file of files) {
|
|
113
228
|
if (isHeaderFile(file) && fileSet.has(path.resolve(siblingCpp(file)))) {
|
|
114
229
|
continue;
|
|
115
230
|
}
|
|
116
231
|
const source = fs.readFileSync(file, "utf8");
|
|
117
|
-
const rel =
|
|
232
|
+
const rel = posixRel(srcDir, file);
|
|
118
233
|
try {
|
|
119
234
|
const result = compileService(source, rel, {
|
|
120
235
|
...config,
|
|
@@ -122,30 +237,100 @@ function build(root, options = {}) {
|
|
|
122
237
|
relativeName: rel,
|
|
123
238
|
outName: toLuauPath(rel),
|
|
124
239
|
architecture: config.architecture !== false,
|
|
125
|
-
includeDirs: [path.dirname(file),
|
|
240
|
+
includeDirs: [path.dirname(file), srcDir, path.join(root, "include")],
|
|
126
241
|
});
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
242
|
+
jobs.push({ rel, files: result.files, stale: result.stale || [] });
|
|
243
|
+
} catch (err) {
|
|
244
|
+
errors.push({ rel, prefixes: sourcePrefixes(rel), message: err.message });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { files, jobs, errors };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function writeTextIfChanged(dest, contents) {
|
|
252
|
+
if (fs.existsSync(dest) && fs.readFileSync(dest, "utf8") === contents) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
256
|
+
fs.writeFileSync(dest, contents, "utf8");
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function writeJobs(root, config, jobs) {
|
|
261
|
+
const written = new Set();
|
|
262
|
+
for (const job of jobs) {
|
|
263
|
+
for (const artifact of job.files) {
|
|
264
|
+
const dest = path.join(root, config.outDir, artifact.name);
|
|
265
|
+
const changed = writeTextIfChanged(dest, artifact.contents);
|
|
266
|
+
written.add(resolveKey(dest));
|
|
267
|
+
if (changed) {
|
|
268
|
+
console.log("cluaupp:", job.rel, "→", path.relative(root, dest));
|
|
133
269
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
270
|
+
}
|
|
271
|
+
for (const stale of job.stale) {
|
|
272
|
+
const dest = path.join(root, config.outDir, stale);
|
|
273
|
+
if (fs.existsSync(dest) && !written.has(resolveKey(dest))) {
|
|
274
|
+
if (tryRm(dest)) {
|
|
138
275
|
console.log("cluaupp: removed", path.relative(root, dest));
|
|
139
276
|
}
|
|
140
277
|
}
|
|
141
|
-
}
|
|
142
|
-
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return written;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function libsPresent(root) {
|
|
284
|
+
const libs = path.join(root, "libs");
|
|
285
|
+
return fs.existsSync(libs) && fs.readdirSync(libs).length > 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function ensureVendor(root) {
|
|
289
|
+
copyRuntime(root);
|
|
290
|
+
copyHeaders(root);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function build(root, options = {}) {
|
|
294
|
+
const exitOnError = options.exitOnError !== false;
|
|
295
|
+
const holdOnError = options.holdOnError === true;
|
|
296
|
+
const syncVendor = options.syncVendor === true;
|
|
297
|
+
const config = loadConfig(root);
|
|
298
|
+
if (syncVendor) {
|
|
299
|
+
copyRuntime(root);
|
|
300
|
+
copyHeaders(root);
|
|
301
|
+
} else {
|
|
302
|
+
ensureVendor(root);
|
|
303
|
+
}
|
|
304
|
+
const srcDir = path.join(root, config.rootDir);
|
|
305
|
+
if (!fs.existsSync(srcDir) || collectCpp(srcDir).length === 0) {
|
|
306
|
+
console.error("no .cpp/.h/.hpp files in", config.rootDir);
|
|
307
|
+
if (!holdOnError) {
|
|
308
|
+
pruneOut(root, config, new Set(), []);
|
|
309
|
+
}
|
|
310
|
+
if (exitOnError) {
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
return { failed: 0, written: new Set() };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const compiled = compileProject(root, config);
|
|
317
|
+
if (compiled.errors.length > 0) {
|
|
318
|
+
for (const err of compiled.errors) {
|
|
143
319
|
console.error(err.message);
|
|
144
320
|
}
|
|
321
|
+
if (holdOnError) {
|
|
322
|
+
console.error(`cluaupp: out not updated (${compiled.errors.length} compile error${compiled.errors.length === 1 ? "" : "s"})`);
|
|
323
|
+
return { failed: compiled.errors.length, written: new Set() };
|
|
324
|
+
}
|
|
145
325
|
}
|
|
146
|
-
|
|
326
|
+
|
|
327
|
+
const written = writeJobs(root, config, compiled.jobs);
|
|
328
|
+
const failedPrefixes = compiled.errors.flatMap((err) => err.prefixes);
|
|
329
|
+
pruneOut(root, config, written, failedPrefixes);
|
|
330
|
+
if (compiled.errors.length > 0 && exitOnError) {
|
|
147
331
|
process.exit(1);
|
|
148
332
|
}
|
|
333
|
+
return { failed: compiled.errors.length, written };
|
|
149
334
|
}
|
|
150
335
|
|
|
151
336
|
function init(dest) {
|
|
@@ -160,33 +345,62 @@ function init(dest) {
|
|
|
160
345
|
}
|
|
161
346
|
|
|
162
347
|
function watch(root) {
|
|
163
|
-
build(root, { exitOnError: false });
|
|
348
|
+
build(root, { exitOnError: false, syncVendor: false, holdOnError: true });
|
|
164
349
|
const config = loadConfig(root);
|
|
165
350
|
const dir = path.join(root, config.rootDir);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
351
|
+
let timer = null;
|
|
352
|
+
let running = false;
|
|
353
|
+
let queued = false;
|
|
354
|
+
|
|
355
|
+
const run = () => {
|
|
356
|
+
if (running) {
|
|
357
|
+
queued = true;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
running = true;
|
|
361
|
+
try {
|
|
362
|
+
build(root, { exitOnError: false, syncVendor: false, holdOnError: true });
|
|
363
|
+
} catch (err) {
|
|
364
|
+
console.error(err.message);
|
|
365
|
+
} finally {
|
|
366
|
+
running = false;
|
|
367
|
+
if (queued) {
|
|
368
|
+
queued = false;
|
|
369
|
+
run();
|
|
173
370
|
}
|
|
174
371
|
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
console.log("watching", dir);
|
|
375
|
+
if (!fs.existsSync(dir)) {
|
|
376
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
377
|
+
}
|
|
378
|
+
fs.watch(dir, { recursive: true }, () => {
|
|
379
|
+
clearTimeout(timer);
|
|
380
|
+
timer = setTimeout(run, 400);
|
|
175
381
|
});
|
|
176
382
|
}
|
|
177
383
|
|
|
178
|
-
|
|
179
|
-
const cmd = args[0] || "help";
|
|
180
|
-
const cwd = process.cwd();
|
|
384
|
+
function dispatch(args) {
|
|
385
|
+
const cmd = args[0] || "help";
|
|
386
|
+
const cwd = process.cwd();
|
|
387
|
+
if (cmd === "init") {
|
|
388
|
+
init(path.resolve(cwd, args[1] || "."));
|
|
389
|
+
} else if (cmd === "build") {
|
|
390
|
+
build(path.resolve(cwd, args[1] || "."));
|
|
391
|
+
} else if (cmd === "watch") {
|
|
392
|
+
watch(path.resolve(cwd, args[1] || "."));
|
|
393
|
+
} else if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
394
|
+
console.log(pkg.version);
|
|
395
|
+
} else {
|
|
396
|
+
printHelp();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
181
399
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
} else if (cmd === "watch") {
|
|
187
|
-
watch(path.resolve(cwd, args[1] || "."));
|
|
188
|
-
} else if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
189
|
-
console.log(pkg.version);
|
|
190
|
-
} else {
|
|
191
|
-
printHelp();
|
|
400
|
+
const launchedAsCli =
|
|
401
|
+
require.main && ["cli.js", "cluaupp.js", "cluau.js"].includes(path.basename(require.main.filename));
|
|
402
|
+
if (launchedAsCli) {
|
|
403
|
+
dispatch(process.argv.slice(2));
|
|
192
404
|
}
|
|
405
|
+
|
|
406
|
+
module.exports = { build, watch, dispatch };
|
package/src/emit.js
CHANGED
|
@@ -31,6 +31,13 @@ function emit(ast, options = {}) {
|
|
|
31
31
|
return `"${node.value}"`;
|
|
32
32
|
case "ident":
|
|
33
33
|
return node.name;
|
|
34
|
+
case "initlist": {
|
|
35
|
+
const entries = (node.fields || []).map((field) => `${field.name} = ${emitExpr(field.value)}`);
|
|
36
|
+
if (entries.length === 0) {
|
|
37
|
+
return "{}";
|
|
38
|
+
}
|
|
39
|
+
return `{ ${entries.join(", ")} }`;
|
|
40
|
+
}
|
|
34
41
|
case "unary": {
|
|
35
42
|
const inner = emitExpr(node.argument);
|
|
36
43
|
if (node.op === "!") {
|
package/src/parse.js
CHANGED
|
@@ -84,6 +84,28 @@ function parse(source, fileName) {
|
|
|
84
84
|
return args;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function parseInitList() {
|
|
88
|
+
eat("op", "{");
|
|
89
|
+
const fields = [];
|
|
90
|
+
while (!at("op", "}") && !at("eof")) {
|
|
91
|
+
if (at("op", ".")) {
|
|
92
|
+
i += 1;
|
|
93
|
+
const name = eat("ident").value;
|
|
94
|
+
eat("op", "=");
|
|
95
|
+
fields.push({ name, value: parseExpr() });
|
|
96
|
+
} else {
|
|
97
|
+
throw error("expected designated initializer .Field = value");
|
|
98
|
+
}
|
|
99
|
+
if (at("op", ",")) {
|
|
100
|
+
i += 1;
|
|
101
|
+
} else {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
eat("op", "}");
|
|
106
|
+
return { type: "initlist", fields };
|
|
107
|
+
}
|
|
108
|
+
|
|
87
109
|
function parsePrimary() {
|
|
88
110
|
if (at("kw", "true") || at("kw", "false")) {
|
|
89
111
|
return { type: "bool", value: eat("kw").value === "true" };
|
|
@@ -120,6 +142,9 @@ function parse(source, fileName) {
|
|
|
120
142
|
eat("op", ")");
|
|
121
143
|
return expr;
|
|
122
144
|
}
|
|
145
|
+
if (at("op", "{")) {
|
|
146
|
+
return parseInitList();
|
|
147
|
+
}
|
|
123
148
|
throw error("invalid expression");
|
|
124
149
|
}
|
|
125
150
|
|
|
@@ -155,6 +180,10 @@ function parse(source, fileName) {
|
|
|
155
180
|
node = { type: "call", object: null, name: node.name, args: parseArgs(), access: "." };
|
|
156
181
|
continue;
|
|
157
182
|
}
|
|
183
|
+
if (at("op", "{") && (node.type === "ident" || node.type === "member")) {
|
|
184
|
+
node = parseInitList();
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
158
187
|
break;
|
|
159
188
|
}
|
|
160
189
|
return node;
|