@tilua/compiler 8.1.0 → 9.0.1

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/dist/cli.js CHANGED
@@ -1,33 +1,43 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ WRITTEN_BY_TILUA,
3
4
  bundle,
4
5
  resolveConfig
5
- } from "./chunk-WOBHMGGC.js";
6
+ } from "./chunk-3RJVN4KC.js";
6
7
 
7
8
  // src/cli.ts
8
- import { mkdirSync, writeFileSync } from "fs";
9
- import { basename, dirname, join, relative, resolve } from "path";
10
- var usage = "usage: tilua <entry> [--out <file>] [--output single|chunks|modules] [--config <tilua.config.json>] [--noCheck] [--noLineMap]";
9
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, watch, writeFileSync } from "fs";
10
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
11
+ import { stripJsonComments } from "@tilua/parser";
12
+ var usage = "usage: tilua [<entry>] [--out <file>] [--output single|chunks|files] [--config <tilua.config.json>] [--noCheck] [--noLineMap] [--minify] [--watch]\n tilua init";
11
13
  var args = process.argv.slice(2);
12
14
  var entry;
13
15
  var out;
14
16
  var config;
15
17
  var typeCheck = true;
16
18
  var lineMap;
19
+ var minify;
17
20
  var output;
21
+ var watching = false;
18
22
  for (let i = 0; i < args.length; i++) {
19
23
  const arg = args[i];
20
24
  if (arg === "--out") out = args[++i];
21
25
  else if (arg === "--config") config = args[++i];
22
26
  else if (arg === "--noCheck") typeCheck = false;
23
27
  else if (arg === "--noLineMap") lineMap = false;
28
+ else if (arg === "--minify") minify = true;
29
+ else if (arg === "--watch" || arg === "-w") watching = true;
24
30
  else if (arg === "--target") {
25
31
  console.error(`--target is gone: the output runs on Lua 5.1 to 5.4, LuaJIT and Luau, and a library says what more its Lua has \u2014 add "luau" to the config's 'types'`);
26
32
  process.exit(2);
27
33
  } else if (arg === "--output") {
28
34
  const value = args[++i];
29
- if (value !== "single" && value !== "chunks" && value !== "modules") {
30
- console.error(`--output must be 'single', 'chunks' or 'modules', not '${value}'`);
35
+ if (value === "modules") {
36
+ console.error("--output modules is gone: --output files writes one file per module, each of which returns its exports");
37
+ process.exit(2);
38
+ }
39
+ if (value !== "single" && value !== "chunks" && value !== "files") {
40
+ console.error(`--output must be 'single', 'chunks' or 'files', not '${value}'`);
31
41
  process.exit(2);
32
42
  }
33
43
  output = value;
@@ -36,39 +46,284 @@ for (let i = 0; i < args.length; i++) {
36
46
  process.exit(0);
37
47
  } else entry = arg;
38
48
  }
39
- if (!entry) {
40
- console.error(usage);
41
- process.exit(2);
49
+ var shown = (path) => relative(process.cwd(), path) || ".";
50
+ var OUT = "out";
51
+ function init() {
52
+ const folder = process.cwd();
53
+ let existing;
54
+ for (const name of ["tilua.config.json", "tilua.config.jsonc"]) {
55
+ const path = join(folder, name);
56
+ if (!existsSync(path)) continue;
57
+ let options2 = {};
58
+ try {
59
+ options2 = JSON.parse(stripJsonComments(readFileSync(path, "utf8")));
60
+ } catch (error) {
61
+ console.error(`${name}: ${error.message}`);
62
+ process.exit(1);
63
+ }
64
+ if (options2.output !== "files" || options2.outDir !== void 0 && options2.outDir !== options2.rootDir) {
65
+ console.error(`${name} is already here`);
66
+ process.exit(1);
67
+ }
68
+ existing = { path, options: options2 };
69
+ }
70
+ const project = join(folder, "default.project.json");
71
+ if (!existsSync(project)) {
72
+ console.error("No default.project.json here: run `tilua init` in the folder of a Rojo project (`rojo init` makes one)");
73
+ process.exit(1);
74
+ }
75
+ const text = readFileSync(project, "utf8");
76
+ const paths = [];
77
+ const walk = (node) => {
78
+ if (!node || typeof node !== "object") return;
79
+ const path = node.$path;
80
+ const named = typeof path === "string" ? path : path?.optional;
81
+ if (typeof named === "string") paths.push(named);
82
+ for (const [key, child] of Object.entries(node)) if (!key.startsWith("$")) walk(child);
83
+ };
84
+ try {
85
+ walk(JSON.parse(text).tree);
86
+ } catch (error) {
87
+ console.error(`default.project.json: ${error.message}`);
88
+ process.exit(1);
89
+ }
90
+ const split = paths.map((path) => path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "").split("/"));
91
+ const shared = [];
92
+ for (let i = 0; split.length && split.every((parts) => parts.length > i && parts[i] === split[0][i]); i++) shared.push(split[0][i]);
93
+ if (split.length === 1 && /\.\w+$/.test(shared.at(-1) ?? "")) shared.pop();
94
+ const rootDir = shared.join("/");
95
+ if (!rootDir) {
96
+ console.error("The project's $paths share no folder to compile from: put the code under one (src), as `rojo init` does");
97
+ process.exit(1);
98
+ }
99
+ if (rootDir === OUT || rootDir.startsWith(`${OUT}/`)) {
100
+ console.error(`The project already reads from ${OUT}: set tilua.config.json up by hand`);
101
+ process.exit(1);
102
+ }
103
+ const out2 = join(folder, OUT);
104
+ if (existsSync(out2) && readdirSync(out2).length) {
105
+ console.error(`${OUT} is already here and not empty: the build would replace what is in it`);
106
+ process.exit(1);
107
+ }
108
+ const options = existing ? { ...existing.options, rootDir: existing.options.rootDir ?? rootDir, outDir: OUT } : { types: ["roblox"], output: "files", rootDir, outDir: OUT };
109
+ writeFileSync(existing?.path ?? join(folder, "tilua.config.json"), JSON.stringify(options, null, 4) + "\n");
110
+ console.log(`${basename(existing?.path ?? "tilua.config.json")}: ${String(options.rootDir)} compiled to ${OUT}`);
111
+ const escaped = rootDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
112
+ let changed = 0;
113
+ const pointed = text.replace(new RegExp(`("\\$path"\\s*:\\s*")(\\./)?${escaped}(?=/|")`, "g"), (_, head) => {
114
+ changed++;
115
+ return `${head}${OUT}`;
116
+ });
117
+ writeFileSync(project, pointed);
118
+ console.log(`default.project.json: ${changed} $path${changed === 1 ? "" : "s"} now read from ${OUT}`);
119
+ const ignore = join(folder, ".gitignore");
120
+ const ignored = existsSync(ignore) ? readFileSync(ignore, "utf8") : "";
121
+ if (!ignored.split(/\r?\n/).some((line) => [OUT, `/${OUT}`, `${OUT}/`, `/${OUT}/`].includes(line.trim()))) {
122
+ writeFileSync(ignore, `${ignored}${ignored && !ignored.endsWith("\n") ? "\n" : ""}
123
+ # What tilua builds from ${rootDir}
124
+ /${OUT}
125
+ `);
126
+ console.log(`.gitignore: /${OUT}`);
127
+ }
128
+ console.log("Next: npm i -D @tilua/compiler @tilua-types/roblox, then run `npx tilua --watch` beside `rojo serve`.");
129
+ process.exit(0);
130
+ }
131
+ function isInside(folder, path) {
132
+ const below = relative(folder, path);
133
+ return below === "" || !below.startsWith("..") && !isAbsolute(below);
134
+ }
135
+ function filesUnder(folder) {
136
+ return under(folder).files;
137
+ }
138
+ function under(folder) {
139
+ const files = [];
140
+ const folders = [];
141
+ const walk = (at) => {
142
+ let items;
143
+ try {
144
+ items = readdirSync(at, { withFileTypes: true });
145
+ } catch {
146
+ return;
147
+ }
148
+ for (const item of items) {
149
+ if (item.name === "node_modules" || item.name === ".git") continue;
150
+ const path = join(at, item.name);
151
+ if (item.isDirectory()) {
152
+ folders.push(path);
153
+ walk(path);
154
+ } else files.push(path);
155
+ }
156
+ };
157
+ walk(folder);
158
+ return { files, folders };
159
+ }
160
+ function removeEmpty(folder, keep) {
161
+ for (const item of readdirSync(folder, { withFileTypes: true })) {
162
+ if (!item.isDirectory()) continue;
163
+ const path = join(folder, item.name);
164
+ removeEmpty(path, keep);
165
+ if (!readdirSync(path).length && !keep.has(resolve(path))) rmdirSync(path);
166
+ }
167
+ }
168
+ function writtenByTilua(folder) {
169
+ return filesUnder(folder).filter((path) => /\.luau?$/.test(path) && isTilua(path));
170
+ }
171
+ function isTilua(path) {
172
+ try {
173
+ const text = readFileSync(path, "utf8").trimEnd();
174
+ return text.slice(text.lastIndexOf("\n") + 1).startsWith(WRITTEN_BY_TILUA);
175
+ } catch {
176
+ return false;
177
+ }
42
178
  }
43
- async function main(from) {
44
- const found = resolveConfig(config, resolve(from)).config;
179
+ async function build(from) {
180
+ const found = resolveConfig(config, resolve(from ?? "tilua.config.json")).config;
45
181
  const mode = output ?? found?.output ?? "single";
46
- const entryFile = mode === "modules" ? void 0 : out ?? from.replace(/\.tilua$/, "") + ".luau";
47
- const folder = entryFile !== void 0 ? dirname(entryFile) : out ?? found?.directory ?? dirname(from);
182
+ if (mode !== "files" && from === void 0) {
183
+ console.error(usage);
184
+ process.exit(2);
185
+ }
186
+ const perFile = mode === "files";
187
+ const entryFile = perFile || from === void 0 ? void 0 : out ?? from.replace(/\.tilua$/, "") + ".luau";
188
+ const folder = resolve(entryFile !== void 0 ? dirname(entryFile) : out ?? found?.outDir ?? process.cwd());
189
+ const rootDir = resolve(found?.rootDir ?? process.cwd());
190
+ const mirror = perFile && resolve(folder) !== rootDir;
191
+ if (mirror && (isInside(folder, rootDir) || isInside(rootDir, folder) || isInside(folder, found?.directory ?? process.cwd()))) {
192
+ console.error(`outDir (${shown(folder)}) has to be a folder of its own: not ${isInside(rootDir, folder) ? "under" : "around"} rootDir (${shown(rootDir)}), and not around the project`);
193
+ return false;
194
+ }
48
195
  const result = await bundle({
49
- entry: from,
196
+ entry: perFile ? void 0 : from,
50
197
  config,
51
198
  typeCheck,
52
199
  output: mode,
53
200
  lineMap,
201
+ minify,
202
+ outDir: perFile ? out : void 0,
54
203
  outFile: entryFile !== void 0 ? basename(entryFile) : void 0
55
204
  });
56
205
  for (const d of result.diagnostics) {
57
- console.error(`${relative(process.cwd(), d.file)}:${d.line}:${d.column} ${d.severity === "warning" ? "warning: " : ""}${d.message}`);
206
+ console.error(`${shown(d.file)}:${d.line}:${d.column} ${d.severity === "warning" ? "warning: " : ""}${d.message}`);
58
207
  }
59
208
  if (result.code === void 0) {
60
- console.error("no bundle written");
61
- process.exit(1);
209
+ console.error("no output written");
210
+ return false;
62
211
  }
63
- for (const file of result.files) {
64
- const target = join(folder, file.path);
65
- mkdirSync(dirname(target), { recursive: true });
66
- writeFileSync(target, file.code);
67
- console.log(`${target}: ${file.modules.length} module${file.modules.length === 1 ? "" : "s"}`);
212
+ const targets = result.files.map((file) => ({ ...file, target: join(folder, file.path) }));
213
+ if (mirror) {
214
+ const compiled = new Map(targets.map((file) => [resolve(file.target), file]));
215
+ const copies = [];
216
+ let clash = false;
217
+ const leftovers = [];
218
+ for (const source of filesUnder(rootDir)) {
219
+ if (source.endsWith(".tilua")) continue;
220
+ if (/\.luau?$/.test(source) && isTilua(source)) {
221
+ leftovers.push(source);
222
+ continue;
223
+ }
224
+ const target = resolve(folder, relative(rootDir, source));
225
+ if (compiled.has(target)) {
226
+ console.error(`${shown(source)}: a .tilua compiles to the same file \u2014 rename or remove one of them`);
227
+ clash = true;
228
+ continue;
229
+ }
230
+ copies.push({ source, target });
231
+ }
232
+ if (clash) return false;
233
+ for (const leftover of leftovers) {
234
+ rmSync(leftover);
235
+ console.log(`${shown(leftover)}: removed \u2014 tilua compiled it beside its .tilua, and the output is in ${shown(folder)} now`);
236
+ }
237
+ for (const file of targets) {
238
+ if (existsSync(file.target) && readFileSync(file.target, "utf8") === file.code) continue;
239
+ mkdirSync(dirname(file.target), { recursive: true });
240
+ writeFileSync(file.target, file.code);
241
+ console.log(`${shown(file.target)}: ${file.modules.length} module${file.modules.length === 1 ? "" : "s"}`);
242
+ }
243
+ for (const { source, target } of copies) {
244
+ const bytes = readFileSync(source);
245
+ if (existsSync(target) && readFileSync(target).equals(bytes)) continue;
246
+ mkdirSync(dirname(target), { recursive: true });
247
+ copyFileSync(source, target);
248
+ console.log(`${shown(target)}: copied`);
249
+ }
250
+ const kept = /* @__PURE__ */ new Set([...compiled.keys(), ...copies.map((copy) => copy.target)]);
251
+ for (const stale of filesUnder(folder)) {
252
+ if (kept.has(resolve(stale))) continue;
253
+ rmSync(stale);
254
+ console.log(`${shown(stale)}: removed`);
255
+ }
256
+ const folders = /* @__PURE__ */ new Set([folder, ...under(rootDir).folders.map((path) => resolve(folder, relative(rootDir, path)))]);
257
+ for (const path of folders) mkdirSync(path, { recursive: true });
258
+ removeEmpty(folder, folders);
259
+ return !result.diagnostics.some((d) => d.severity !== "warning");
260
+ }
261
+ if (perFile) {
262
+ const theirs = targets.filter((file) => existsSync(file.target) && !isTilua(file.target));
263
+ for (const file of theirs) {
264
+ console.error(`${shown(file.target)}: this file was not written by tilua, and a .tilua would replace it \u2014 rename or remove one of them`);
265
+ }
266
+ if (theirs.length) return false;
267
+ }
268
+ for (const file of targets) {
269
+ if (existsSync(file.target) && readFileSync(file.target, "utf8") === file.code) continue;
270
+ mkdirSync(dirname(file.target), { recursive: true });
271
+ writeFileSync(file.target, file.code);
272
+ console.log(`${shown(file.target)}: ${file.modules.length} module${file.modules.length === 1 ? "" : "s"}`);
273
+ }
274
+ if (perFile) {
275
+ const kept = new Set(targets.map((file) => resolve(file.target)));
276
+ for (const stale of writtenByTilua(folder)) {
277
+ if (kept.has(resolve(stale))) continue;
278
+ rmSync(stale);
279
+ console.log(`${shown(stale)}: removed, its .tilua is gone`);
280
+ }
68
281
  }
69
- process.exit(result.diagnostics.some((d) => d.severity !== "warning") ? 1 : 0);
282
+ return !result.diagnostics.some((d) => d.severity !== "warning");
283
+ }
284
+ async function watchAndBuild(from) {
285
+ const found = resolveConfig(config, resolve(from ?? "tilua.config.json")).config;
286
+ const folder = found?.directory ?? (from !== void 0 ? dirname(resolve(from)) : process.cwd());
287
+ const rootDir = found?.rootDir;
288
+ const outDir = resolve(out ?? found?.outDir ?? folder);
289
+ const mirrored = (output ?? found?.output) === "files" && rootDir !== void 0 && outDir !== resolve(rootDir);
290
+ let running = false;
291
+ let again = false;
292
+ let timer;
293
+ const run = async () => {
294
+ if (running) {
295
+ again = true;
296
+ return;
297
+ }
298
+ running = true;
299
+ const ok = await build(from).catch((error) => {
300
+ console.error(error instanceof Error ? error.message : error);
301
+ return false;
302
+ });
303
+ console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] ${ok ? "built" : "built with errors"}; watching ${shown(folder)}`);
304
+ running = false;
305
+ if (again) {
306
+ again = false;
307
+ await run();
308
+ }
309
+ };
310
+ await run();
311
+ watch(folder, { recursive: true }, (_event, name) => {
312
+ if (!name) return;
313
+ const path = resolve(folder, name.toString());
314
+ if (relative(folder, path).split(/[\\/]/).includes("node_modules") || isInside(outDir, path) && mirrored) return;
315
+ const read = /\.tilua$|tilua\.config\.jsonc?$|\.project\.json$/.test(path) || mirrored && isInside(rootDir, path);
316
+ if (!read) return;
317
+ clearTimeout(timer);
318
+ timer = setTimeout(() => void run(), 100);
319
+ });
320
+ }
321
+ async function main() {
322
+ if (entry === "init") init();
323
+ if (watching) return watchAndBuild(entry);
324
+ process.exit(await build(entry) ? 0 : 1);
70
325
  }
71
- main(entry).catch((error) => {
326
+ main().catch((error) => {
72
327
  console.error(error instanceof Error ? error.message : error);
73
328
  process.exit(1);
74
329
  });