@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.cjs CHANGED
@@ -13,7 +13,7 @@ var import_node_path3 = require("path");
13
13
  var import_node_fs3 = require("fs");
14
14
  var import_node_path2 = require("path");
15
15
  var import_parser7 = require("@tilua/parser");
16
- var import_luau_parser2 = require("luau-parser");
16
+ var import_luau_parser3 = require("luau-parser");
17
17
 
18
18
  // src/config.ts
19
19
  var import_node_path = require("path");
@@ -50,10 +50,10 @@ var parenthesized = (expression) => ({ type: "ParenthesizedExpression", expressi
50
50
  var table = (fields) => ({ type: "TableExpression", fields, ...span() });
51
51
  var field = (key, value) => (0, import_parser2.isLuauName)(key) ? { type: "TableFieldNamed", name: identifier(key), value } : { type: "TableFieldComputed", key: string(key), value };
52
52
  var block = (statements) => ({ type: "Block", statements, ...span() });
53
- var local = (names, init) => ({
53
+ var local = (names, init2) => ({
54
54
  type: "LocalStatement",
55
55
  names: names.map((name) => ({ type: "TypedIdentifier", name, ...span() })),
56
- init,
56
+ init: init2,
57
57
  ...span()
58
58
  });
59
59
  var assign = (targets, values) => ({ type: "AssignmentStatement", targets, values, ...span() });
@@ -401,6 +401,10 @@ var SharedRuntime = class {
401
401
  return { declare, define };
402
402
  }
403
403
  };
404
+ function moduleArguments(context, name) {
405
+ const loader2 = context.loader?.(name);
406
+ return loader2 ? [string(name), loader2] : [string(name)];
407
+ }
404
408
  function lower(program2, scopes, options = {}) {
405
409
  const result2 = new Lowerer(program2, scopes, options).run();
406
410
  dropUnreachable(result2.statements);
@@ -442,9 +446,9 @@ var Lowerer = class {
442
446
  if (Array.isArray(value)) return void value.forEach(visit);
443
447
  const node = value;
444
448
  if (node.type === "VariableDeclaration" && node.name?.type === "IdentifierPattern" && node.name.name) {
445
- let init = node.init;
446
- while (init?.type === "ParenthesizedExpression") init = init.expression;
447
- if (init?.type === "ClassExpression") this.classExpressionNames.set(init, node.name.name);
449
+ let init2 = node.init;
450
+ while (init2?.type === "ParenthesizedExpression") init2 = init2.expression;
451
+ if (init2?.type === "ClassExpression") this.classExpressionNames.set(init2, node.name.name);
448
452
  }
449
453
  for (const [key, child] of Object.entries(value)) if (key !== "line" && key !== "column") visit(child);
450
454
  };
@@ -717,6 +721,7 @@ var Lowerer = class {
717
721
  const plugin3 = lowerings[i].plugin;
718
722
  if (!plugin3.methodCall) continue;
719
723
  const answer = plugin3.methodCall({
724
+ options: lowerings[i].options ?? {},
720
725
  method,
721
726
  receiver,
722
727
  receiverGlobal,
@@ -748,6 +753,7 @@ var Lowerer = class {
748
753
  const plugin3 = lowerings[i].plugin;
749
754
  if (!plugin3.globalCall) continue;
750
755
  const answer = plugin3.globalCall({
756
+ options: lowerings[i].options ?? {},
751
757
  name,
752
758
  at: this.callSite(node.callee),
753
759
  get arguments() {
@@ -767,6 +773,7 @@ var Lowerer = class {
767
773
  if (name === void 0) return void 0;
768
774
  for (let i = lowerings.length - 1; i >= 0; i--) {
769
775
  const answer = lowerings[i].plugin.globalValue?.({
776
+ options: lowerings[i].options ?? {},
770
777
  name,
771
778
  at: this.callSite(node),
772
779
  use: (runtime2) => this.loweringRuntime(i, runtime2)
@@ -1020,12 +1027,12 @@ end`).body.statements, out2);
1020
1027
  local2 = this.names.fresh(moduleName(key));
1021
1028
  moduleLocals.set(key, local2);
1022
1029
  locals.push(local2);
1023
- requires.push(assign([identifier(local2)], [call(context.require, [string(key)])]));
1030
+ requires.push(assign([identifier(local2)], [call(context.require, moduleArguments(context, key))]));
1024
1031
  }
1025
1032
  return local2;
1026
1033
  };
1027
1034
  const load = (key) => {
1028
- if (!moduleLocals.has(key)) requires.push(callStatement(call(context.require, [string(key)])));
1035
+ if (!moduleLocals.has(key)) requires.push(callStatement(call(context.require, moduleArguments(context, key))));
1029
1036
  };
1030
1037
  const exportBinding = (node, exported) => {
1031
1038
  const binding = node && this.bindingByDeclaration.get(node);
@@ -1134,20 +1141,22 @@ end`).body.statements, out2);
1134
1141
  continue;
1135
1142
  }
1136
1143
  if (declaration.type === "ClassDeclaration") {
1137
- body.push(...this.classDeclaration(declaration, "assign"));
1144
+ body.push(...this.classDeclaration(declaration, "assign").map((s) => this.from(s, declaration)));
1138
1145
  continue;
1139
1146
  }
1140
1147
  switch (declaration.type) {
1148
+ // Stamped with where they were written, as any statement is:
1149
+ // an error in a top-level `const` names its line.
1141
1150
  case "VariableDeclaration":
1142
- body.push(...this.variableDeclaration(declaration, "assign"));
1151
+ body.push(...this.variableDeclaration(declaration, "assign").map((s) => this.from(s, declaration)));
1143
1152
  break;
1144
1153
  case "ExportDefaultStatement": {
1145
1154
  const exported = declaration.declaration;
1146
1155
  if (exported.type === "ClassDeclaration") {
1147
- body.push(...this.classDeclaration(exported, "assign"));
1148
- body.push(assign([member(exportsTable(), "default")], [this.reference(exported.name)]));
1156
+ body.push(...this.classDeclaration(exported, "assign").map((s) => this.from(s, exported)));
1157
+ body.push(this.from(assign([member(exportsTable(), "default")], [this.reference(exported.name)]), declaration));
1149
1158
  } else {
1150
- body.push(assign([member(exportsTable(), "default")], [this.expression(exported)]));
1159
+ body.push(this.from(assign([member(exportsTable(), "default")], [this.expression(exported)]), declaration));
1151
1160
  }
1152
1161
  break;
1153
1162
  }
@@ -1185,7 +1194,7 @@ end`).body.statements, out2);
1185
1194
  this.report(source, `Cannot find module '${source.value}'`);
1186
1195
  return nil();
1187
1196
  }
1188
- return call(context.import ?? context.require, [string(key)]);
1197
+ return call(context.import ?? context.require, moduleArguments(context, key));
1189
1198
  }
1190
1199
  isRewritten(node) {
1191
1200
  const binding = this.bindingByDeclaration.get(node);
@@ -1384,9 +1393,9 @@ end`).body.statements, out2);
1384
1393
  return this.assignment(node);
1385
1394
  case "CompoundAssignmentStatement": {
1386
1395
  if (node.operator === "??=") return this.nullishAssignment(node);
1387
- const join2 = node.operator === "+=" ? this.options.types?.joins.get(node) : void 0;
1388
- if (join2 === "dynamic") return this.dynamicAddition(node);
1389
- const operator = join2 === "concat" ? "..=" : node.operator;
1396
+ const join3 = node.operator === "+=" ? this.options.types?.joins.get(node) : void 0;
1397
+ if (join3 === "dynamic") return this.dynamicAddition(node);
1398
+ const operator = join3 === "concat" ? "..=" : node.operator;
1390
1399
  if (!this.syntax.has("compoundAssignment")) return this.compoundAssignment51(node, operator);
1391
1400
  return [{
1392
1401
  type: "CompoundAssignmentStatement",
@@ -1650,11 +1659,11 @@ end`).body.statements, out2);
1650
1659
  * carries neither. */
1651
1660
  classRuntimeStatements() {
1652
1661
  if (!this.classHelpers) return [];
1653
- const { build, accessors } = this.classHelpers;
1662
+ const { build: build2, accessors } = this.classHelpers;
1654
1663
  const metamethods = `{ ${CLASS_METAMETHODS.map((name) => `"${name}"`).join(", ")} }`;
1655
1664
  const out2 = [];
1656
- this.define([build, accessors], (0, import_luau_parser.parse)(`
1657
- local function ${build}(base, name)
1665
+ this.define([build2, accessors], (0, import_luau_parser.parse)(`
1666
+ local function ${build2}(base, name)
1658
1667
  local class = { __getters = {}, __setters = {} }
1659
1668
  class.__index = class
1660
1669
  class.ClassObject = class
@@ -1713,9 +1722,9 @@ end
1713
1722
  /** `declare` names the local the class table goes in; without it the
1714
1723
  * table is assigned to `self`, which a module has already declared. */
1715
1724
  classStatements(node, self, declare) {
1716
- const { build, accessors } = this.classRuntime();
1725
+ const { build: build2, accessors } = this.classRuntime();
1717
1726
  const called = node.name?.name ?? (node.type === "ClassExpression" ? this.classExpressionNames.get(node) : void 0) ?? "";
1718
- const create = call(identifier(build), [
1727
+ const create = call(identifier(build2), [
1719
1728
  node.superclass ? this.reference(node.superclass) : nil(),
1720
1729
  string(called)
1721
1730
  ]);
@@ -2072,9 +2081,9 @@ end
2072
2081
  return call(this.helper("instanceof"), [this.keptToOne(left), this.keptToOne(right)]);
2073
2082
  }
2074
2083
  if (node.operator === "+") {
2075
- const join2 = this.options.types?.joins.get(node);
2076
- if (join2 === "concat") return binary("..", left, right);
2077
- if (join2 === "dynamic") return call(this.helper("add"), [this.keptToOne(left), this.keptToOne(right)]);
2084
+ const join3 = this.options.types?.joins.get(node);
2085
+ if (join3 === "concat") return binary("..", left, right);
2086
+ if (join3 === "dynamic") return call(this.helper("add"), [this.keptToOne(left), this.keptToOne(right)]);
2078
2087
  }
2079
2088
  if (!this.syntax.has("floorDivision") && node.operator === "//") return this.floorDivide(left, right);
2080
2089
  return binary(node.operator, left, right);
@@ -3041,23 +3050,29 @@ function ownExport(f, name) {
3041
3050
  // src/linemap.ts
3042
3051
  var import_parser6 = require("@tilua/parser");
3043
3052
  function statementsOf(node) {
3044
- const out2 = [];
3045
- const visit = (value) => {
3053
+ return statementTree(node).statements;
3054
+ }
3055
+ function statementTree(node) {
3056
+ const statements = [];
3057
+ const parents = [];
3058
+ const visit = (value, parent) => {
3046
3059
  if (!value || typeof value !== "object") return;
3047
- if (Array.isArray(value)) return void value.forEach(visit);
3060
+ if (Array.isArray(value)) return void value.forEach((item) => visit(item, parent));
3048
3061
  const record = value;
3062
+ let inside = parent;
3049
3063
  if (typeof record.type === "string" && record.type.endsWith("Statement")) {
3050
- out2.push(value);
3064
+ inside = statements.push(value) - 1;
3065
+ parents.push(parent);
3051
3066
  }
3052
3067
  for (const [key, child] of Object.entries(record)) {
3053
- if (key !== "line" && key !== "column") visit(child);
3068
+ if (key !== "line" && key !== "column") visit(child, inside);
3054
3069
  }
3055
3070
  };
3056
- visit(node);
3057
- return out2;
3071
+ visit(node, -1);
3072
+ return { statements, parents };
3058
3073
  }
3059
3074
  function buildLineMap(emitted, printed, origins) {
3060
- const before = statementsOf(emitted);
3075
+ const { statements: before, parents } = statementTree(emitted);
3061
3076
  const after = statementsOf(printed);
3062
3077
  if (before.length !== after.length) return /* @__PURE__ */ new Map();
3063
3078
  const map = /* @__PURE__ */ new Map();
@@ -3068,6 +3083,18 @@ function buildLineMap(emitted, printed, origins) {
3068
3083
  const line = after[i].line.start;
3069
3084
  if (!map.has(line)) map.set(line, origin);
3070
3085
  }
3086
+ const holds = /* @__PURE__ */ new Set();
3087
+ for (let i = 0; i < before.length; i++) {
3088
+ if (!origins.has(before[i])) continue;
3089
+ for (let up = parents[i]; up >= 0 && !holds.has(up); up = parents[up]) holds.add(up);
3090
+ }
3091
+ for (let i = 0; i < before.length; i++) {
3092
+ const origin = origins.get(before[i]);
3093
+ if (!origin || holds.has(i)) continue;
3094
+ for (let line = after[i].line.start + 1; line <= after[i].line.end; line++) {
3095
+ if (!map.has(line)) map.set(line, origin);
3096
+ }
3097
+ }
3071
3098
  return map;
3072
3099
  }
3073
3100
  var VLQ_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@@ -3099,6 +3126,34 @@ function lineMapSource(map, unpack) {
3099
3126
  return `${unpack}({${files.map(import_parser6.luauString).join(",")}},${(0, import_parser6.luauString)(data)})`;
3100
3127
  }
3101
3128
 
3129
+ // src/output.ts
3130
+ var import_luau_parser2 = require("luau-parser");
3131
+ var WRITTEN = /* @__PURE__ */ new Set([
3132
+ "compoundAssignment",
3133
+ "continue",
3134
+ "ifExpression",
3135
+ "floorDivision",
3136
+ "attributes",
3137
+ "numberLiterals"
3138
+ ]);
3139
+ function printOptions(syntax, format = {}) {
3140
+ return {
3141
+ syntax: [...syntax].some((s) => WRITTEN.has(s)) ? "luau" : "lua51",
3142
+ compact: format.minify ?? false,
3143
+ ...format.indent !== void 0 && { indent: format.indent }
3144
+ };
3145
+ }
3146
+ function printOutput(node, options) {
3147
+ try {
3148
+ return { code: (0, import_luau_parser2.print)(node, options) };
3149
+ } catch (error) {
3150
+ if (!(error instanceof import_luau_parser2.PrintError)) throw error;
3151
+ return {
3152
+ problem: `The output cannot be written as Lua 5.1: ${error.message.replace(/ \(\d+:\d+\)$/, "")}. Nothing in 'types' says the project's Lua has it \u2014 "luau" does, and "roblox" brings "luau"`
3153
+ };
3154
+ }
3155
+ }
3156
+
3102
3157
  // src/lowering.ts
3103
3158
  var import_node_url = require("url");
3104
3159
  async function loadLowerings(modules) {
@@ -3115,7 +3170,7 @@ async function loadLowerings(modules) {
3115
3170
  });
3116
3171
  continue;
3117
3172
  }
3118
- lowerings.push({ plugin: plugin3, from: module2.from, library: module2.library });
3173
+ lowerings.push({ plugin: plugin3, from: module2.from, library: module2.library, options: module2.options });
3119
3174
  } catch (error) {
3120
3175
  problems.push({
3121
3176
  file: module2.file,
@@ -3127,17 +3182,30 @@ async function loadLowerings(modules) {
3127
3182
  }
3128
3183
 
3129
3184
  // src/bundle.ts
3185
+ var WRITTEN_BY_TILUA = "-- Written by tilua";
3130
3186
  async function bundle(options) {
3131
- const entry2 = (0, import_node_path2.resolve)(options.entry);
3132
3187
  const diagnostics = [];
3188
+ const from = options.entry !== void 0 ? (0, import_node_path2.resolve)(options.entry) : (0, import_node_path2.resolve)(options.root ?? process.cwd(), "tilua.config.json");
3133
3189
  const inline = options.config !== void 0 && typeof options.config !== "string";
3134
- const { config: config2, problems } = resolveConfig(options.config, entry2, options.root ?? (inline ? (0, import_node_path2.dirname)(entry2) : process.cwd()));
3190
+ const { config: config2, problems } = resolveConfig(options.config, from, options.root ?? (inline ? (0, import_node_path2.dirname)(from) : process.cwd()));
3135
3191
  for (const p of problems) diagnostics.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
3136
- const root = (0, import_node_path2.resolve)(options.root ?? config2?.directory ?? (0, import_node_path2.dirname)(entry2));
3192
+ const root = (0, import_node_path2.resolve)(options.root ?? config2?.directory ?? (0, import_node_path2.dirname)(from));
3137
3193
  const nameOf = (file) => (0, import_node_path2.relative)(root, file).replace(/\\/g, "/").replace(/\.tilua$/, "");
3194
+ const mode = options.output ?? config2?.output ?? "single";
3195
+ const perFile = mode === "files";
3196
+ const rootDir = (0, import_node_path2.resolve)(config2?.rootDir ?? root);
3197
+ const entry2 = options.entry !== void 0 ? (0, import_node_path2.resolve)(options.entry) : void 0;
3198
+ const anchor = entry2 ?? config2?.path ?? from;
3199
+ const stop = (message, file = anchor) => ({
3200
+ files: [],
3201
+ modules: [],
3202
+ diagnostics: [...diagnostics, { file, message, line: 1, column: 1, category: "config" }]
3203
+ });
3204
+ if (!perFile && entry2 === void 0) return stop('No entry to build: only "output": "files" builds without one');
3138
3205
  const sources = /* @__PURE__ */ new Map();
3139
3206
  const directives = /* @__PURE__ */ new Map();
3140
- const queue = [entry2];
3207
+ const queue = perFile ? tiluaFiles(rootDir) : [entry2];
3208
+ if (perFile && !queue.length) return stop(`No .tilua file under ${(0, import_node_path2.relative)(process.cwd(), rootDir) || "."}`);
3141
3209
  while (queue.length) {
3142
3210
  const file = queue.shift();
3143
3211
  if (sources.has(file)) continue;
@@ -3155,6 +3223,22 @@ async function bundle(options) {
3155
3223
  if (target && !target.endsWith(".d.tilua")) queue.push(target);
3156
3224
  }
3157
3225
  }
3226
+ if (perFile) {
3227
+ const outside = [...sources.keys()].filter((file) => isOutside(rootDir, file));
3228
+ if (outside.length) {
3229
+ return {
3230
+ files: [],
3231
+ modules: [],
3232
+ diagnostics: [...diagnostics, ...outside.map((file) => ({
3233
+ file,
3234
+ message: `This file is imported, but it is outside rootDir (${(0, import_node_path2.relative)(root, rootDir) || "."}), where every file of "files" output has to be`,
3235
+ line: 1,
3236
+ column: 1,
3237
+ category: "module"
3238
+ }))]
3239
+ };
3240
+ }
3241
+ }
3158
3242
  const analysis = analyzeModules([...sources.values()], config2);
3159
3243
  const { lowerings, problems: loweringProblems } = await loadLowerings(analysis.lowerings);
3160
3244
  for (const p of loweringProblems) {
@@ -3165,24 +3249,61 @@ async function bundle(options) {
3165
3249
  const G = names.fresh("G");
3166
3250
  const requireExpression = member(identifier(G), "require");
3167
3251
  const importExpression = member(identifier(G), "import");
3168
- const mode = options.output ?? config2?.output ?? "single";
3169
3252
  const byPath = syntaxOf(lowerings).has("requireByPath");
3253
+ if (perFile && !byPath) {
3254
+ return stop(`"output": "files" needs a Lua that requires a file by its path, which Lua 5.1 does not \u2014 add "luau" to 'types' ("roblox" loads it)`, config2?.path ?? anchor);
3255
+ }
3256
+ const outputPath = (file) => (0, import_node_path2.relative)(rootDir, file).replace(/\\/g, "/").replace(/\.tilua$/, ".luau");
3257
+ const fileOfModule = new Map([...sources.values()].map((s) => [s.name, s.file]));
3258
+ const outDir = (0, import_node_path2.resolve)(options.outDir ?? config2?.outDir ?? rootDir);
3259
+ const written = (file) => (0, import_node_path2.join)(outDir, outputPath(file));
3260
+ let host;
3261
+ let runtimeFile = (0, import_node_path2.join)(outDir, "_tilua.luau");
3262
+ if (perFile) {
3263
+ const outputs = [...sources.keys()].map(written);
3264
+ const taken = new Set(outputs.map(pathKey));
3265
+ for (let n = 2; taken.has(pathKey(runtimeFile)); n++) runtimeFile = (0, import_node_path2.join)(outDir, `_tilua${n}.luau`);
3266
+ for (let i = lowerings.length - 1; i >= 0 && !host; i--) {
3267
+ const { plugin: plugin3, options: own } = lowerings[i];
3268
+ host = plugin3.files?.({ options: own ?? {}, directory: config2?.directory ?? root, outDir, files: outputs, runtime: runtimeFile });
3269
+ }
3270
+ for (const problem of host?.problems ?? []) {
3271
+ diagnostics.push({
3272
+ file: problem.file ?? config2?.path ?? anchor,
3273
+ message: problem.message,
3274
+ line: 1,
3275
+ column: 1,
3276
+ category: "config",
3277
+ ...problem.severity === "warning" && { severity: "warning" }
3278
+ });
3279
+ }
3280
+ if (host?.runtime !== void 0) {
3281
+ if (isOutside(outDir, (0, import_node_path2.resolve)(host.runtime))) return stop(`The runtime has to go inside outDir, not at ${host.runtime}`);
3282
+ if (taken.has(pathKey(host.runtime))) return stop(`The runtime cannot go at ${host.runtime}: a module's file is there`);
3283
+ runtimeFile = (0, import_node_path2.resolve)(host.runtime);
3284
+ }
3285
+ }
3286
+ const placeOf = (file) => host?.place?.(file) ?? placeInFolder((0, import_node_path2.relative)(outDir, file));
3287
+ const requireFrom = (from2, to) => pathBetween(placeOf(from2), placeOf(to), host?.root);
3288
+ const isScript = (file) => host?.script?.(file) ?? false;
3170
3289
  const modules = /* @__PURE__ */ new Map();
3171
3290
  const origins = /* @__PURE__ */ new WeakMap();
3172
3291
  const shared = new SharedRuntime(names);
3173
3292
  const shaking = options.treeShaking ?? config2?.treeShaking ?? false;
3174
- const mapping = options.lineMap ?? config2?.lineMap ?? true;
3293
+ const format = { indent: options.indent ?? config2?.indent, minify: options.minify ?? config2?.minify ?? false };
3294
+ const printing = printOptions(syntaxOf(lowerings), format);
3295
+ const mapping = !format.minify && (options.lineMap ?? config2?.lineMap ?? true);
3175
3296
  const resolveIn = (file) => (specifier) => {
3176
3297
  const found = (0, import_parser7.resolveModulePath)(file, specifier, config2);
3177
3298
  return found !== void 0 && sources.has(found) ? found : void 0;
3178
3299
  };
3179
- const shaken = shaking ? treeShake([...sources.values()].map((s) => ({ file: s.file, program: s.program, scopes: s.scopes, resolve: resolveIn(s.file) })), entry2) : void 0;
3180
- const pending = [entry2];
3300
+ const shaken = shaking && entry2 !== void 0 && !perFile ? treeShake([...sources.values()].map((s) => ({ file: s.file, program: s.program, scopes: s.scopes, resolve: resolveIn(s.file) })), entry2) : void 0;
3301
+ const pending = perFile ? [...sources.keys()] : [entry2];
3181
3302
  while (pending.length) {
3182
3303
  const file = pending.shift();
3183
3304
  if (modules.has(file)) continue;
3184
3305
  const source = sources.get(file);
3185
- const shown = (0, import_node_path2.relative)(root, file).replace(/\\/g, "/");
3306
+ const shown2 = (0, import_node_path2.relative)(root, file).replace(/\\/g, "/");
3186
3307
  const imports = /* @__PURE__ */ new Set();
3187
3308
  const dynamicImports = /* @__PURE__ */ new Set();
3188
3309
  const lowered2 = lower(source.program, source.scopes, {
@@ -3191,7 +3312,7 @@ async function bundle(options) {
3191
3312
  lowerings,
3192
3313
  noLib: config2?.noLib,
3193
3314
  source: source.text,
3194
- file: shown,
3315
+ file: shown2,
3195
3316
  shared,
3196
3317
  shaken: shaken && {
3197
3318
  ...shaken.modules.get(file),
@@ -3213,6 +3334,13 @@ async function bundle(options) {
3213
3334
  lines: !mapping ? void 0 : mode === "single" ? `${G}.lines` : `${G}.maps[${(0, import_parser7.luauString)(source.name)}]`,
3214
3335
  fail: mapping ? `${G}.fail` : void 0,
3215
3336
  arrays: `${G}.arrays`,
3337
+ // The file a module is in, required from this one by its
3338
+ // path, written out.
3339
+ loader: !perFile ? void 0 : (name) => {
3340
+ const target = fileOfModule.get(name);
3341
+ if (target === void 0) return void 0;
3342
+ return (0, import_luau_parser3.parseExpressionFromSource)(`function() return require(${(0, import_parser7.luauString)(requireFrom(written(file), written(target)))}) end`);
3343
+ },
3216
3344
  resolve: (specifier, dynamic) => {
3217
3345
  const found = (0, import_parser7.resolveModulePath)(file, specifier, config2);
3218
3346
  if (!found || found.endsWith(".d.tilua") || !sources.has(found)) return void 0;
@@ -3226,7 +3354,7 @@ async function bundle(options) {
3226
3354
  for (const d of lowered2.diagnostics) diagnostics.push({ file, ...d, category: "module" });
3227
3355
  for (const statement of allStatements(lowered2.statements)) {
3228
3356
  const line = lowered2.origins.get(statement);
3229
- if (line !== void 0) origins.set(statement, { file: shown, line });
3357
+ if (line !== void 0) origins.set(statement, { file: shown2, line });
3230
3358
  }
3231
3359
  modules.set(file, {
3232
3360
  name: source.name,
@@ -3264,6 +3392,102 @@ async function bundle(options) {
3264
3392
  return { files: [], modules: moduleNames, diagnostics };
3265
3393
  }
3266
3394
  if (shaking) shared.shake(lowered.flatMap((m) => m.statements));
3395
+ const unwritable = (problem) => ({
3396
+ files: [],
3397
+ modules: moduleNames,
3398
+ diagnostics: [...diagnostics, { file: anchor, message: problem, line: 1, column: 1, category: "config" }]
3399
+ });
3400
+ const write = (node) => {
3401
+ const printed2 = printOutput(node, printing);
3402
+ return "problem" in printed2 ? printed2 : printed2.code;
3403
+ };
3404
+ if (perFile) {
3405
+ const imported = [];
3406
+ for (const [file, source] of sources) {
3407
+ for (const { specifier, line, column } of importSites(source.program)) {
3408
+ const target = resolveIn(file)(specifier);
3409
+ if (target === void 0 || !isScript(written(target))) continue;
3410
+ imported.push({
3411
+ file,
3412
+ line,
3413
+ column,
3414
+ category: "module",
3415
+ message: `'${specifier}' is a script the game runs, which nothing can import: move what it shares into a module of its own`
3416
+ });
3417
+ }
3418
+ }
3419
+ if (imported.length) return { files: [], modules: moduleNames, diagnostics: [...diagnostics, ...imported] };
3420
+ const runtimePath = (0, import_node_path2.relative)(outDir, runtimeFile).replace(/\\/g, "/");
3421
+ const files2 = [];
3422
+ for (const [file, module2] of modules) {
3423
+ const path = outputPath(file);
3424
+ const at = written(file);
3425
+ const script = isScript(at);
3426
+ const prelude = host?.prelude?.(at);
3427
+ const body = [
3428
+ ...prelude !== void 0 ? (0, import_luau_parser3.parse)(prelude).body.statements : [],
3429
+ ...(0, import_luau_parser3.parse)(`local ${G} = require(${(0, import_parser7.luauString)(requireFrom(at, runtimeFile))})`).body.statements
3430
+ ];
3431
+ for (let i = 0; i < module2.shared.length; i += 100) {
3432
+ const some = module2.shared.slice(i, i + 100);
3433
+ body.push(local(some, some.map((name) => member(member(identifier(G), "shared"), name))));
3434
+ }
3435
+ if (module2.usesScriptArgs) body.push(...(0, import_luau_parser3.parse)(`local scriptArgs = ${G}.scriptArgs`).body.statements);
3436
+ body.push(assign(
3437
+ [index(member(identifier(G), "modules"), string(module2.name))],
3438
+ [moduleTable(module2)]
3439
+ ));
3440
+ const value = `${G}.${mapping ? "run" : "require"}(${(0, import_parser7.luauString)(module2.name)})`;
3441
+ const returned2 = module2.info.exports && !script ? entryReturn(lowerings, module2.name, value, () => analysis.exportsOf(file)) : { statements: [], value };
3442
+ if ("problem" in returned2) return stop(returned2.problem.message, file);
3443
+ const ending = (0, import_luau_parser3.parse)(`${returned2.statements.join("\n")}
3444
+ return ${returned2.value}`).body.statements;
3445
+ const chunk = program(body);
3446
+ if (mapping) {
3447
+ const text = write(chunk);
3448
+ if (typeof text !== "string") return unwritable(text.problem);
3449
+ const lines = buildLineMap(chunk, (0, import_luau_parser3.parse)(text), origins);
3450
+ const named = host?.place?.(at) ? `${placeOf(at).join("/")}.luau` : path;
3451
+ body.push(...mapRegistration(G, { path: named, modules: [module2] }, lineMapSource(lines, `${G}.lineMap`)));
3452
+ }
3453
+ body.push(...ending);
3454
+ const code = write(chunk);
3455
+ if (typeof code !== "string") return unwritable(code.problem);
3456
+ const from2 = (0, import_node_path2.relative)((0, import_node_path2.dirname)(at), file).replace(/\\/g, "/");
3457
+ files2.push({ path, code: `${code}
3458
+ ${WRITTEN_BY_TILUA} from ${from2}: edit that file, not this one.
3459
+ `, modules: [module2.name] });
3460
+ }
3461
+ const runtimeProgram = (0, import_luau_parser3.parse)(runtime(G, {
3462
+ several: true,
3463
+ perFile: true,
3464
+ byPath,
3465
+ files: /* @__PURE__ */ new Map(),
3466
+ imports: lowered.some((m) => m.dynamicImports.size > 0),
3467
+ mapping
3468
+ }));
3469
+ const helpers2 = shared.chunk(new Set(lowered.flatMap((m) => m.shared)));
3470
+ const helperNames = helpers2.declare.flatMap((statement) => statement.type === "LocalStatement" ? statement.names.map((n) => n.name) : []);
3471
+ runtimeProgram.body.statements.unshift(...helpers2.declare);
3472
+ runtimeProgram.body.statements.push(...helpers2.define);
3473
+ if (helperNames.length) {
3474
+ runtimeProgram.body.statements.push(assign(
3475
+ [member(identifier(G), "shared")],
3476
+ [table(helperNames.map((name) => field(name, identifier(name))))]
3477
+ ));
3478
+ }
3479
+ if (lowered.some((m) => m.usesScriptArgs)) {
3480
+ runtimeProgram.body.statements.push(...(0, import_luau_parser3.parse)(`${G}.scriptArgs = {}
3481
+ ${G}.arrays[${G}.scriptArgs] = true`).body.statements);
3482
+ }
3483
+ runtimeProgram.body.statements.push(returns([identifier(G)]));
3484
+ const runtimeCode = write(runtimeProgram);
3485
+ if (typeof runtimeCode !== "string") return unwritable(runtimeCode.problem);
3486
+ files2.push({ path: runtimePath, code: `${runtimeCode}
3487
+ ${WRITTEN_BY_TILUA}: the runtime every file of the project requires.
3488
+ `, modules: [] });
3489
+ return { code: files2[0].code, files: files2, modules: moduleNames, diagnostics };
3490
+ }
3267
3491
  const several = mode !== "single";
3268
3492
  const layout = layoutOf(mode, modules.get(entry2), lowered, options.outFile);
3269
3493
  const usesScriptArgs = lowered.some((m) => m.usesScriptArgs);
@@ -3271,7 +3495,7 @@ async function bundle(options) {
3271
3495
  for (const file of layout.slice(1)) {
3272
3496
  for (const module2 of file.modules) elsewhere.set(module2.name, loadPath(layout[0].path, file.path, byPath));
3273
3497
  }
3274
- const program2 = (0, import_luau_parser2.parse)(runtime(G, {
3498
+ const program2 = (0, import_luau_parser3.parse)(runtime(G, {
3275
3499
  several,
3276
3500
  byPath,
3277
3501
  files: elsewhere,
@@ -3281,8 +3505,8 @@ async function bundle(options) {
3281
3505
  const assignment = program2.body.statements[1];
3282
3506
  if (usesScriptArgs) {
3283
3507
  program2.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
3284
- program2.body.statements.push(...(0, import_luau_parser2.parse)(`${G}.arrays[scriptArgs] = true`).body.statements);
3285
- if (several) program2.body.statements.push(...(0, import_luau_parser2.parse)(`${G}.scriptArgs = scriptArgs`).body.statements);
3508
+ program2.body.statements.push(...(0, import_luau_parser3.parse)(`${G}.arrays[scriptArgs] = true`).body.statements);
3509
+ if (several) program2.body.statements.push(...(0, import_luau_parser3.parse)(`${G}.scriptArgs = scriptArgs`).body.statements);
3286
3510
  }
3287
3511
  const modulesTable = assignment.type === "AssignmentStatement" && assignment.values[0].type === "TableExpression" ? assignment.values[0].fields.find((f) => f.type === "TableFieldNamed" && f.name.name === "modules")?.value : void 0;
3288
3512
  if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
@@ -3295,31 +3519,40 @@ async function bundle(options) {
3295
3519
  const result2 = names.fresh("result");
3296
3520
  const entryName = (0, import_parser7.luauString)(sources.get(entry2).name);
3297
3521
  const returnsExports = modules.get(entry2).info.exports;
3298
- const entryStatements = (0, import_luau_parser2.parse)(!mapping ? `${returnsExports ? "return " : ""}${G}.require(${entryName})` : `
3522
+ const returned = returnsExports ? entryReturn(lowerings, sources.get(entry2).name, mapping ? result2 : `${G}.require(${entryName})`, () => analysis.exportsOf(entry2)) : void 0;
3523
+ if (returned && "problem" in returned) return stop(returned.problem.message);
3524
+ const declarations = returned?.statements.join("\n") ?? "";
3525
+ const entryStatements = (0, import_luau_parser3.parse)(!mapping ? `${declarations}
3526
+ ${returned ? `return ${returned.value}` : `${G}.require(${entryName})`}` : `
3299
3527
  local ${ok}, ${result2} = xpcall(function()
3300
3528
  return ${G}.require(${entryName})
3301
3529
  end, ${G}.fail)
3302
3530
  if not ${ok} then
3303
3531
  error(${result2}, 0)
3304
3532
  end
3305
- ${returnsExports ? `return ${result2}` : ""}
3533
+ ${declarations}
3534
+ ${returned ? `return ${returned.value}` : ""}
3306
3535
  `).body.statements;
3307
3536
  program2.body.statements.push(...entryStatements);
3308
- const map = mapping ? buildLineMap(program2, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(program2)), origins) : /* @__PURE__ */ new Map();
3537
+ let printed = write(program2);
3538
+ if (typeof printed !== "string") return unwritable(printed.problem);
3539
+ const map = mapping ? buildLineMap(program2, (0, import_luau_parser3.parse)(printed), origins) : /* @__PURE__ */ new Map();
3309
3540
  if (mapping && (map.size || several)) {
3310
3541
  const mapStatements = [assign(
3311
3542
  [member(identifier(G), "lines")],
3312
- [(0, import_luau_parser2.parseExpressionFromSource)(lineMapSource(map, `${G}.lineMap`))]
3543
+ [(0, import_luau_parser3.parseExpressionFromSource)(lineMapSource(map, `${G}.lineMap`))]
3313
3544
  )];
3314
3545
  if (several) mapStatements.push(...mapRegistration(G, layout[0], `${G}.lines`));
3315
3546
  program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...mapStatements);
3316
3547
  }
3317
3548
  program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...helpers.define);
3318
- const files = [{ path: layout[0].path, code: (0, import_luau_parser2.print)(program2) + "\n", modules: layout[0].modules.map((m) => m.name) }];
3549
+ printed = write(program2);
3550
+ if (typeof printed !== "string") return unwritable(printed.problem);
3551
+ const files = [{ path: layout[0].path, code: printed + "\n", modules: layout[0].modules.map((m) => m.name) }];
3319
3552
  for (const file of layout.slice(1)) {
3320
3553
  const fileHelpers = shared.chunk(new Set(file.modules.flatMap((m) => m.shared)));
3321
3554
  const body = [...fileHelpers.declare];
3322
- if (file.modules.some((m) => m.usesScriptArgs)) body.push(...(0, import_luau_parser2.parse)(`local scriptArgs = ${G}.scriptArgs`).body.statements);
3555
+ if (file.modules.some((m) => m.usesScriptArgs)) body.push(...(0, import_luau_parser3.parse)(`local scriptArgs = ${G}.scriptArgs`).body.statements);
3323
3556
  for (const module2 of file.modules) {
3324
3557
  body.push(assign(
3325
3558
  [index(member(identifier(G), "modules"), string(module2.name))],
@@ -3328,14 +3561,34 @@ ${returnsExports ? `return ${result2}` : ""}
3328
3561
  }
3329
3562
  const chunk = program([returns([functionExpression(functionBody([G], body))])]);
3330
3563
  if (mapping) {
3331
- const lines = buildLineMap(chunk, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(chunk)), origins);
3564
+ const lines = buildLineMap(chunk, (0, import_luau_parser3.parse)((0, import_luau_parser3.print)(chunk, printing)), origins);
3332
3565
  body.push(...mapRegistration(G, file, lineMapSource(lines, `${G}.lineMap`)));
3333
3566
  }
3334
3567
  body.push(...fileHelpers.define);
3335
- files.push({ path: file.path, code: (0, import_luau_parser2.print)(chunk) + "\n", modules: file.modules.map((m) => m.name) });
3568
+ const code = write(chunk);
3569
+ if (typeof code !== "string") return unwritable(code.problem);
3570
+ files.push({ path: file.path, code: code + "\n", modules: file.modules.map((m) => m.name) });
3336
3571
  }
3337
3572
  return { code: files[0].code, files, modules: moduleNames, diagnostics };
3338
3573
  }
3574
+ function entryReturn(lowerings, module2, value, exports2) {
3575
+ const syntax = [...syntaxOf(lowerings)];
3576
+ for (let i = lowerings.length - 1; i >= 0; i--) {
3577
+ const { plugin: plugin3, from, options } = lowerings[i];
3578
+ if (!plugin3.entryExports) continue;
3579
+ const answer = plugin3.entryExports({ options: options ?? {}, module: module2, value, exports: exports2(), syntax });
3580
+ if (!answer) continue;
3581
+ const statements = [...answer.statements ?? []];
3582
+ try {
3583
+ (0, import_luau_parser3.parse)(`${statements.join("\n")}
3584
+ return ${answer.value}`);
3585
+ } catch (error) {
3586
+ return { problem: { message: `'${from}' answered entryExports with Luau that does not parse: ${error.message}` } };
3587
+ }
3588
+ return { statements, value: answer.value };
3589
+ }
3590
+ return { statements: [], value };
3591
+ }
3339
3592
  function moduleTable(module2) {
3340
3593
  const { names: exported, links, stars } = module2.info;
3341
3594
  const fields = [];
@@ -3355,10 +3608,6 @@ function layoutOf(mode, entry2, modules, outFile) {
3355
3608
  const entryStem = outFile !== void 0 ? (0, import_node_path2.basename)(outFile, (0, import_node_path2.extname)(outFile)) : import_node_path2.posix.basename(entry2.name);
3356
3609
  const others = modules.filter((m) => m !== entry2);
3357
3610
  if (mode === "single") return [{ path: `${entryStem}${extension}`, modules: [entry2, ...others] }];
3358
- if (mode === "modules") {
3359
- const pathOf = (m) => `${m.name.split("/").map((part) => part === ".." ? "__" : part).join("/")}${extension}`;
3360
- return [entry2, ...others].map((m) => ({ path: pathOf(m), modules: [m] }));
3361
- }
3362
3611
  const byName = new Map(modules.map((m) => [m.name, m]));
3363
3612
  const reach = (root) => {
3364
3613
  const seen = /* @__PURE__ */ new Set();
@@ -3394,16 +3643,75 @@ function layoutOf(mode, entry2, modules, outFile) {
3394
3643
  return files;
3395
3644
  }
3396
3645
  function loadPath(entryPath, file, byPath) {
3397
- const path = import_node_path2.posix.relative(import_node_path2.posix.dirname(entryPath), file);
3398
- if (!byPath) return path;
3646
+ return byPath ? requirePath(entryPath, file) : import_node_path2.posix.relative(import_node_path2.posix.dirname(entryPath), file);
3647
+ }
3648
+ function placeInFolder(path) {
3649
+ const parts = path.replace(/\\/g, "/").replace(/\.luau?$/, "").split("/").filter((part) => part !== "");
3650
+ if (parts.at(-1) === "init") parts.pop();
3651
+ return parts;
3652
+ }
3653
+ function pathBetween(from, to, root) {
3654
+ if (root !== void 0 && from[0] !== to[0]) return `${root}/${to.join("/")}`;
3655
+ if (to.length > from.length && from.every((part, i) => to[i] === part)) return `@self/${to.slice(from.length).join("/")}`;
3656
+ const base = from.slice(0, -1);
3657
+ let shared = 0;
3658
+ while (shared < base.length && shared < to.length - 1 && base[shared] === to[shared]) shared++;
3659
+ const up = base.length - shared;
3660
+ return `${up === 0 ? "./" : "../".repeat(up)}${to.slice(shared).join("/")}`;
3661
+ }
3662
+ function importSites(program2) {
3663
+ const sites = [];
3664
+ for (const statement of program2.body.statements) {
3665
+ if (statement.type === "ImportStatement" || statement.type === "ExportAllStatement" || statement.type === "ExportNamedStatement" && statement.source) {
3666
+ const source = statement.source;
3667
+ sites.push({ specifier: source.value, line: source.line.start, column: source.column.start });
3668
+ }
3669
+ }
3670
+ for (const node of (0, import_parser7.importExpressions)(program2)) {
3671
+ if (node.source.type === "StringLiteral") sites.push({ specifier: node.source.value, line: node.source.line.start, column: node.source.column.start });
3672
+ }
3673
+ return sites;
3674
+ }
3675
+ function pathKey(path) {
3676
+ const full = (0, import_node_path2.resolve)(path);
3677
+ return process.platform === "win32" ? full.toLowerCase() : full;
3678
+ }
3679
+ function requirePath(from, to) {
3680
+ const path = import_node_path2.posix.relative(import_node_path2.posix.dirname(from), to);
3399
3681
  const bare = path.slice(0, path.length - import_node_path2.posix.extname(path).length);
3400
3682
  return bare.startsWith("../") ? bare : `./${bare}`;
3401
3683
  }
3684
+ function tiluaFiles(folder) {
3685
+ const found = [];
3686
+ const walk2 = (at) => {
3687
+ let entries;
3688
+ try {
3689
+ entries = (0, import_node_fs3.readdirSync)(at, { withFileTypes: true });
3690
+ } catch {
3691
+ return;
3692
+ }
3693
+ for (const item of entries) {
3694
+ if (item.name.startsWith(".")) continue;
3695
+ const path = (0, import_node_path2.join)(at, item.name);
3696
+ if (item.isDirectory()) {
3697
+ if (item.name !== "node_modules") walk2(path);
3698
+ } else if (item.name.endsWith(".tilua") && !item.name.endsWith(".d.tilua")) {
3699
+ found.push(path);
3700
+ }
3701
+ }
3702
+ };
3703
+ walk2(folder);
3704
+ return found.sort();
3705
+ }
3706
+ function isOutside(folder, file) {
3707
+ const path = (0, import_node_path2.relative)(folder, file);
3708
+ return path === "" || path.startsWith("..") || (0, import_node_path2.isAbsolute)(path);
3709
+ }
3402
3710
  function mapRegistration(G, file, map) {
3403
3711
  const extension = import_node_path2.posix.extname(file.path);
3404
3712
  const parts = file.path.slice(0, file.path.length - extension.length).split(/[/.]/).filter((part) => part !== "");
3405
3713
  const entry2 = `${G}.sources[#${G}.sources + 1]`;
3406
- return (0, import_luau_parser2.parse)([
3714
+ return (0, import_luau_parser3.parse)([
3407
3715
  `${entry2} = { { ${parts.map(import_parser7.luauString).join(", ")} }, ${map}, ${(0, import_parser7.luauString)(extension.slice(1))} }`,
3408
3716
  ...file.modules.map((module2) => `${G}.maps[${(0, import_parser7.luauString)(module2.name)}] = ${G}.sources[#${G}.sources][2]`)
3409
3717
  ].join("\n")).body.statements;
@@ -3430,6 +3738,7 @@ function loader(G, byPath) {
3430
3738
  }
3431
3739
  function runtime(G, options) {
3432
3740
  const { several, mapping } = options;
3741
+ const perFile = options.perFile ?? false;
3433
3742
  const files = [...options.files].map(([name, file]) => `[${(0, import_parser7.luauString)(name)}] = ${(0, import_parser7.luauString)(file)}`).join(", ");
3434
3743
  return `
3435
3744
  local ${G}
@@ -3641,32 +3950,60 @@ ${several ? `
3641
3950
  return map[key]
3642
3951
  end })
3643
3952
  end,` : ""}
3644
- records = {},${several ? `
3953
+ records = {},${several && !perFile ? `
3645
3954
  -- The file each module outside this one is in, as \`load\` is handed it.
3646
3955
  files = { ${files} },
3647
- loaded = {},${mapping ? `
3956
+ loaded = {},` : ""}${several && mapping ? `
3648
3957
  -- Every file's line map: by the module, and by the file's name.
3649
3958
  maps = {},
3650
- sources = {},` : ""}
3959
+ sources = {},` : ""}${several && !perFile ? `
3651
3960
  -- Another of the bundle's files: a function that adds its modules here.
3652
3961
  load = function(file)
3653
3962
  if ${G}.loaded[file] then
3654
3963
  return
3655
3964
  end
3656
3965
  ${G}.loaded[file] = true${loader(G, options.byPath)}
3966
+ end,` : ""}${perFile && mapping ? `
3967
+ -- What a file returns: its module's exports. The first file asked for
3968
+ -- runs under \`xpcall\`, so an error nothing caught is reported in the
3969
+ -- project's files; the files it requires in turn are inside that already.
3970
+ running = false,
3971
+ run = function(name)
3972
+ if ${G}.running then
3973
+ return ${G}.require(name)
3974
+ end
3975
+ ${G}.running = true
3976
+ local ok, result = xpcall(${G}.require, ${G}.fail, name)
3977
+ ${G}.running = false
3978
+ if not ok then
3979
+ error(result, 0)
3980
+ end
3981
+ return result
3657
3982
  end,` : ""}${options.imports ? `
3658
3983
  -- \`import("./x")\`: the module's exports, asked for where the code runs
3659
3984
  -- rather than before its module does. \`require\` does the rest \u2014 and
3660
3985
  -- loads the file the module is in, if it is not this one.
3661
- import = function(name)
3662
- return ${G}.require(name)
3986
+ import = function(name${perFile ? ", load" : ""})
3987
+ return ${G}.require(name${perFile ? ", load" : ""})
3663
3988
  end,` : ""}
3664
- require = function(name)
3989
+ require = function(name${perFile ? ", load" : ""})
3665
3990
  local record = ${G}.records[name]
3666
3991
  if record ~= nil then
3667
3992
  return record.exports
3668
3993
  end
3669
- local module = ${G}.modules[name]${several ? `
3994
+ local module = ${G}.modules[name]${perFile ? `
3995
+ -- Its file has not run: requiring it adds the module, and runs it.
3996
+ if module == nil and load ~= nil then
3997
+ load()
3998
+ record = ${G}.records[name]
3999
+ if record ~= nil then
4000
+ return record.exports
4001
+ end
4002
+ module = ${G}.modules[name]
4003
+ end
4004
+ if module == nil then
4005
+ error(("Cannot find module '%s': nothing has loaded the file it is in"):format(name), 2)
4006
+ end` : several ? `
3670
4007
  if module == nil then
3671
4008
  ${G}.load(${G}.files[name])
3672
4009
  module = ${G}.modules[name]
@@ -3754,6 +4091,35 @@ function importedSpecifiers(program2) {
3754
4091
  return program2.body.statements.flatMap((s) => s.type === "ImportStatement" || s.type === "ExportAllStatement" || s.type === "ExportNamedStatement" && s.source ? [s.source.value] : []);
3755
4092
  }
3756
4093
  var libraryCache = /* @__PURE__ */ new Map();
4094
+ var treeCache;
4095
+ function instanceTree(config2, libs, noLib) {
4096
+ const tree = (0, import_parser7.sourceMapOf)(config2);
4097
+ if (!tree) return void 0;
4098
+ if ("problem" in tree) return tree.named ? { problem: { file: config2.path, message: tree.problem } } : void 0;
4099
+ const declares = (name) => libs.some((lib) => lib.body.statements.some((s) => s.type === "DeclareStatement" && s.name === name));
4100
+ if (!tree.named && !declares("game")) return void 0;
4101
+ if (treeCache && treeCache.text === tree.text && treeCache.libs.length === libs.length && treeCache.libs.every((lib, i) => lib === libs[i])) {
4102
+ return { types: treeCache.types };
4103
+ }
4104
+ const empty = (0, import_parser7.parse)("");
4105
+ const aliases = (0, import_parser7.analyzeTypes)(empty, (0, import_parser7.analyzeScopes)(empty, { noLib }), { libs, noLib, diagnostics: false }).aliases;
4106
+ const membersOf = (className) => {
4107
+ const type = aliases.get(className);
4108
+ return new Set(type?.kind === "object" ? type.properties.keys() : ["Name", "ClassName", "Parent", "Archivable"]);
4109
+ };
4110
+ const { types, problem } = (0, import_parser7.sourceMapTypes)(tree.text, tree.path, { classes: new Set(aliases.keys()), membersOf });
4111
+ if (!types) return tree.named && problem ? { problem: { file: tree.path, message: problem } } : void 0;
4112
+ const scripts = /* @__PURE__ */ new Map();
4113
+ const cached = {
4114
+ program: types.program,
4115
+ scriptFor: (file) => {
4116
+ if (!scripts.has(file)) scripts.set(file, types.scriptFor(file));
4117
+ return scripts.get(file);
4118
+ }
4119
+ };
4120
+ treeCache = { text: tree.text, libs: [...libs], types: cached, scripts };
4121
+ return { types: cached };
4122
+ }
3757
4123
  function parsedLibrary(file) {
3758
4124
  let stamp;
3759
4125
  try {
@@ -3772,11 +4138,15 @@ var moduleAnalyses = /* @__PURE__ */ new Map();
3772
4138
  function analyzeModules(modules, config2) {
3773
4139
  const out2 = [];
3774
4140
  const analyses = /* @__PURE__ */ new Map();
3775
- const libraries = config2 ? (0, import_parser7.resolveTypeLibraries)(config2) : { files: [], lowerings: [], problems: [] };
4141
+ const libraries = config2 ? (0, import_parser7.resolveTypeLibraries)(config2) : { files: [], lowerings: [], options: [], problems: [] };
3776
4142
  for (const p of libraries.problems) out2.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
3777
4143
  const libs = libraries.files.map(parsedLibrary);
3778
4144
  const noLib = config2?.noLib ?? false;
3779
4145
  const checked = libs.length > 0 || !noLib;
4146
+ const tree = config2 ? instanceTree(config2, libs, noLib) : void 0;
4147
+ if (tree?.problem) out2.push({ file: tree.problem.file, message: tree.problem.message, line: 1, column: 1, category: "config" });
4148
+ if (tree?.types) libs.push(tree.types.program);
4149
+ const scriptOf = (file) => tree?.types && config2 ? tree.types.scriptFor((0, import_parser7.outputPathOf)(config2, file)) : void 0;
3780
4150
  const globals = libs.flatMap((lib) => lib.body.statements.flatMap((s) => s.type === "DeclareStatement" ? [s.name] : []));
3781
4151
  const byFile = new Map(modules.map((m) => [m.file, m]));
3782
4152
  const exportsCache = /* @__PURE__ */ new Map();
@@ -3789,7 +4159,9 @@ function analyzeModules(modules, config2) {
3789
4159
  const done = analyzed.get(file);
3790
4160
  if (done) return done;
3791
4161
  const remembered = moduleAnalyses.get(file);
3792
- if (remembered && remembered.setting === settingFor && libs.length === remembered.libs.length && remembered.libs.every((lib, i) => lib === libs[i]) && remembered.program === program2 && [...remembered.read].every(([read3, stamp]) => stampOf(read3) === stamp)) {
4162
+ const script = scriptOf(file);
4163
+ const ownLibs = script ? [...libs, script] : libs;
4164
+ if (remembered && remembered.setting === settingFor && ownLibs.length === remembered.libs.length && remembered.libs.every((lib, i) => lib === ownLibs[i]) && remembered.program === program2 && [...remembered.read].every(([read3, stamp]) => stampOf(read3) === stamp)) {
3793
4165
  analyzed.set(file, remembered.result);
3794
4166
  readWhile.set(file, remembered.read);
3795
4167
  return remembered.result;
@@ -3802,7 +4174,7 @@ function analyzeModules(modules, config2) {
3802
4174
  }
3803
4175
  const before = partials;
3804
4176
  const types = (0, import_parser7.analyzeTypes)(program2, scopes, {
3805
- libs,
4177
+ libs: ownLibs,
3806
4178
  resolveModule: resolverFor(file),
3807
4179
  noLib,
3808
4180
  reportUnknownTypes: checked
@@ -3811,7 +4183,7 @@ function analyzeModules(modules, config2) {
3811
4183
  const at = d.node;
3812
4184
  said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type", severity: d.severity });
3813
4185
  }
3814
- const result2 = { types, diagnostics: said };
4186
+ const result2 = { types, diagnostics: said, scopes };
3815
4187
  if (partials === before) {
3816
4188
  analyzed.set(file, result2);
3817
4189
  const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
@@ -3819,7 +4191,7 @@ function analyzeModules(modules, config2) {
3819
4191
  if (own !== void 0) read3.set(file, own);
3820
4192
  readWhile.set(file, read3);
3821
4193
  if ([...read3.values()].every((stamp) => stamp !== void 0)) {
3822
- moduleAnalyses.set(file, { setting: settingFor, libs: [...libs], program: program2, read: read3, result: result2 });
4194
+ moduleAnalyses.set(file, { setting: settingFor, libs: [...ownLibs], program: program2, read: read3, result: result2 });
3823
4195
  }
3824
4196
  }
3825
4197
  return result2;
@@ -3868,7 +4240,16 @@ function analyzeModules(modules, config2) {
3868
4240
  analyses.set(module2.file, types);
3869
4241
  out2.push(...diagnostics);
3870
4242
  }
3871
- return { types: analyses, diagnostics: out2, lowerings: [...libraries.lowerings] };
4243
+ const exportsOf = (file) => {
4244
+ const cached = exportsCache.get(file);
4245
+ if (cached) return cached;
4246
+ const module2 = byFile.get(file);
4247
+ const { types, scopes } = analyzeModule(file, module2.program);
4248
+ const exports2 = (0, import_parser7.moduleExports)(module2.program, scopes, types, resolverFor(file));
4249
+ exportsCache.set(file, exports2);
4250
+ return exports2;
4251
+ };
4252
+ return { types: analyses, diagnostics: out2, lowerings: [...libraries.lowerings], exportsOf };
3872
4253
  }
3873
4254
  function allStatements(statements) {
3874
4255
  const out2 = [];
@@ -3888,27 +4269,36 @@ function allStatements(statements) {
3888
4269
  }
3889
4270
 
3890
4271
  // src/cli.ts
3891
- var usage = "usage: tilua <entry> [--out <file>] [--output single|chunks|modules] [--config <tilua.config.json>] [--noCheck] [--noLineMap]";
4272
+ var import_parser8 = require("@tilua/parser");
4273
+ var usage = "usage: tilua [<entry>] [--out <file>] [--output single|chunks|files] [--config <tilua.config.json>] [--noCheck] [--noLineMap] [--minify] [--watch]\n tilua init";
3892
4274
  var args = process.argv.slice(2);
3893
4275
  var entry;
3894
4276
  var out;
3895
4277
  var config;
3896
4278
  var typeCheck = true;
3897
4279
  var lineMap;
4280
+ var minify;
3898
4281
  var output;
4282
+ var watching = false;
3899
4283
  for (let i = 0; i < args.length; i++) {
3900
4284
  const arg = args[i];
3901
4285
  if (arg === "--out") out = args[++i];
3902
4286
  else if (arg === "--config") config = args[++i];
3903
4287
  else if (arg === "--noCheck") typeCheck = false;
3904
4288
  else if (arg === "--noLineMap") lineMap = false;
4289
+ else if (arg === "--minify") minify = true;
4290
+ else if (arg === "--watch" || arg === "-w") watching = true;
3905
4291
  else if (arg === "--target") {
3906
4292
  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'`);
3907
4293
  process.exit(2);
3908
4294
  } else if (arg === "--output") {
3909
4295
  const value = args[++i];
3910
- if (value !== "single" && value !== "chunks" && value !== "modules") {
3911
- console.error(`--output must be 'single', 'chunks' or 'modules', not '${value}'`);
4296
+ if (value === "modules") {
4297
+ console.error("--output modules is gone: --output files writes one file per module, each of which returns its exports");
4298
+ process.exit(2);
4299
+ }
4300
+ if (value !== "single" && value !== "chunks" && value !== "files") {
4301
+ console.error(`--output must be 'single', 'chunks' or 'files', not '${value}'`);
3912
4302
  process.exit(2);
3913
4303
  }
3914
4304
  output = value;
@@ -3917,39 +4307,284 @@ for (let i = 0; i < args.length; i++) {
3917
4307
  process.exit(0);
3918
4308
  } else entry = arg;
3919
4309
  }
3920
- if (!entry) {
3921
- console.error(usage);
3922
- process.exit(2);
4310
+ var shown = (path) => (0, import_node_path3.relative)(process.cwd(), path) || ".";
4311
+ var OUT = "out";
4312
+ function init() {
4313
+ const folder = process.cwd();
4314
+ let existing;
4315
+ for (const name of ["tilua.config.json", "tilua.config.jsonc"]) {
4316
+ const path = (0, import_node_path3.join)(folder, name);
4317
+ if (!(0, import_node_fs4.existsSync)(path)) continue;
4318
+ let options2 = {};
4319
+ try {
4320
+ options2 = JSON.parse((0, import_parser8.stripJsonComments)((0, import_node_fs4.readFileSync)(path, "utf8")));
4321
+ } catch (error) {
4322
+ console.error(`${name}: ${error.message}`);
4323
+ process.exit(1);
4324
+ }
4325
+ if (options2.output !== "files" || options2.outDir !== void 0 && options2.outDir !== options2.rootDir) {
4326
+ console.error(`${name} is already here`);
4327
+ process.exit(1);
4328
+ }
4329
+ existing = { path, options: options2 };
4330
+ }
4331
+ const project = (0, import_node_path3.join)(folder, "default.project.json");
4332
+ if (!(0, import_node_fs4.existsSync)(project)) {
4333
+ console.error("No default.project.json here: run `tilua init` in the folder of a Rojo project (`rojo init` makes one)");
4334
+ process.exit(1);
4335
+ }
4336
+ const text = (0, import_node_fs4.readFileSync)(project, "utf8");
4337
+ const paths = [];
4338
+ const walk2 = (node) => {
4339
+ if (!node || typeof node !== "object") return;
4340
+ const path = node.$path;
4341
+ const named = typeof path === "string" ? path : path?.optional;
4342
+ if (typeof named === "string") paths.push(named);
4343
+ for (const [key, child] of Object.entries(node)) if (!key.startsWith("$")) walk2(child);
4344
+ };
4345
+ try {
4346
+ walk2(JSON.parse(text).tree);
4347
+ } catch (error) {
4348
+ console.error(`default.project.json: ${error.message}`);
4349
+ process.exit(1);
4350
+ }
4351
+ const split = paths.map((path) => path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "").split("/"));
4352
+ const shared = [];
4353
+ for (let i = 0; split.length && split.every((parts) => parts.length > i && parts[i] === split[0][i]); i++) shared.push(split[0][i]);
4354
+ if (split.length === 1 && /\.\w+$/.test(shared.at(-1) ?? "")) shared.pop();
4355
+ const rootDir = shared.join("/");
4356
+ if (!rootDir) {
4357
+ console.error("The project's $paths share no folder to compile from: put the code under one (src), as `rojo init` does");
4358
+ process.exit(1);
4359
+ }
4360
+ if (rootDir === OUT || rootDir.startsWith(`${OUT}/`)) {
4361
+ console.error(`The project already reads from ${OUT}: set tilua.config.json up by hand`);
4362
+ process.exit(1);
4363
+ }
4364
+ const out2 = (0, import_node_path3.join)(folder, OUT);
4365
+ if ((0, import_node_fs4.existsSync)(out2) && (0, import_node_fs4.readdirSync)(out2).length) {
4366
+ console.error(`${OUT} is already here and not empty: the build would replace what is in it`);
4367
+ process.exit(1);
4368
+ }
4369
+ const options = existing ? { ...existing.options, rootDir: existing.options.rootDir ?? rootDir, outDir: OUT } : { types: ["roblox"], output: "files", rootDir, outDir: OUT };
4370
+ (0, import_node_fs4.writeFileSync)(existing?.path ?? (0, import_node_path3.join)(folder, "tilua.config.json"), JSON.stringify(options, null, 4) + "\n");
4371
+ console.log(`${(0, import_node_path3.basename)(existing?.path ?? "tilua.config.json")}: ${String(options.rootDir)} compiled to ${OUT}`);
4372
+ const escaped = rootDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4373
+ let changed = 0;
4374
+ const pointed = text.replace(new RegExp(`("\\$path"\\s*:\\s*")(\\./)?${escaped}(?=/|")`, "g"), (_, head) => {
4375
+ changed++;
4376
+ return `${head}${OUT}`;
4377
+ });
4378
+ (0, import_node_fs4.writeFileSync)(project, pointed);
4379
+ console.log(`default.project.json: ${changed} $path${changed === 1 ? "" : "s"} now read from ${OUT}`);
4380
+ const ignore = (0, import_node_path3.join)(folder, ".gitignore");
4381
+ const ignored = (0, import_node_fs4.existsSync)(ignore) ? (0, import_node_fs4.readFileSync)(ignore, "utf8") : "";
4382
+ if (!ignored.split(/\r?\n/).some((line) => [OUT, `/${OUT}`, `${OUT}/`, `/${OUT}/`].includes(line.trim()))) {
4383
+ (0, import_node_fs4.writeFileSync)(ignore, `${ignored}${ignored && !ignored.endsWith("\n") ? "\n" : ""}
4384
+ # What tilua builds from ${rootDir}
4385
+ /${OUT}
4386
+ `);
4387
+ console.log(`.gitignore: /${OUT}`);
4388
+ }
4389
+ console.log("Next: npm i -D @tilua/compiler @tilua-types/roblox, then run `npx tilua --watch` beside `rojo serve`.");
4390
+ process.exit(0);
4391
+ }
4392
+ function isInside(folder, path) {
4393
+ const below = (0, import_node_path3.relative)(folder, path);
4394
+ return below === "" || !below.startsWith("..") && !(0, import_node_path3.isAbsolute)(below);
3923
4395
  }
3924
- async function main(from) {
3925
- const found = resolveConfig(config, (0, import_node_path3.resolve)(from)).config;
4396
+ function filesUnder(folder) {
4397
+ return under(folder).files;
4398
+ }
4399
+ function under(folder) {
4400
+ const files = [];
4401
+ const folders = [];
4402
+ const walk2 = (at) => {
4403
+ let items;
4404
+ try {
4405
+ items = (0, import_node_fs4.readdirSync)(at, { withFileTypes: true });
4406
+ } catch {
4407
+ return;
4408
+ }
4409
+ for (const item of items) {
4410
+ if (item.name === "node_modules" || item.name === ".git") continue;
4411
+ const path = (0, import_node_path3.join)(at, item.name);
4412
+ if (item.isDirectory()) {
4413
+ folders.push(path);
4414
+ walk2(path);
4415
+ } else files.push(path);
4416
+ }
4417
+ };
4418
+ walk2(folder);
4419
+ return { files, folders };
4420
+ }
4421
+ function removeEmpty(folder, keep) {
4422
+ for (const item of (0, import_node_fs4.readdirSync)(folder, { withFileTypes: true })) {
4423
+ if (!item.isDirectory()) continue;
4424
+ const path = (0, import_node_path3.join)(folder, item.name);
4425
+ removeEmpty(path, keep);
4426
+ if (!(0, import_node_fs4.readdirSync)(path).length && !keep.has((0, import_node_path3.resolve)(path))) (0, import_node_fs4.rmdirSync)(path);
4427
+ }
4428
+ }
4429
+ function writtenByTilua(folder) {
4430
+ return filesUnder(folder).filter((path) => /\.luau?$/.test(path) && isTilua(path));
4431
+ }
4432
+ function isTilua(path) {
4433
+ try {
4434
+ const text = (0, import_node_fs4.readFileSync)(path, "utf8").trimEnd();
4435
+ return text.slice(text.lastIndexOf("\n") + 1).startsWith(WRITTEN_BY_TILUA);
4436
+ } catch {
4437
+ return false;
4438
+ }
4439
+ }
4440
+ async function build(from) {
4441
+ const found = resolveConfig(config, (0, import_node_path3.resolve)(from ?? "tilua.config.json")).config;
3926
4442
  const mode = output ?? found?.output ?? "single";
3927
- const entryFile = mode === "modules" ? void 0 : out ?? from.replace(/\.tilua$/, "") + ".luau";
3928
- const folder = entryFile !== void 0 ? (0, import_node_path3.dirname)(entryFile) : out ?? found?.directory ?? (0, import_node_path3.dirname)(from);
4443
+ if (mode !== "files" && from === void 0) {
4444
+ console.error(usage);
4445
+ process.exit(2);
4446
+ }
4447
+ const perFile = mode === "files";
4448
+ const entryFile = perFile || from === void 0 ? void 0 : out ?? from.replace(/\.tilua$/, "") + ".luau";
4449
+ const folder = (0, import_node_path3.resolve)(entryFile !== void 0 ? (0, import_node_path3.dirname)(entryFile) : out ?? found?.outDir ?? process.cwd());
4450
+ const rootDir = (0, import_node_path3.resolve)(found?.rootDir ?? process.cwd());
4451
+ const mirror = perFile && (0, import_node_path3.resolve)(folder) !== rootDir;
4452
+ if (mirror && (isInside(folder, rootDir) || isInside(rootDir, folder) || isInside(folder, found?.directory ?? process.cwd()))) {
4453
+ 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`);
4454
+ return false;
4455
+ }
3929
4456
  const result2 = await bundle({
3930
- entry: from,
4457
+ entry: perFile ? void 0 : from,
3931
4458
  config,
3932
4459
  typeCheck,
3933
4460
  output: mode,
3934
4461
  lineMap,
4462
+ minify,
4463
+ outDir: perFile ? out : void 0,
3935
4464
  outFile: entryFile !== void 0 ? (0, import_node_path3.basename)(entryFile) : void 0
3936
4465
  });
3937
4466
  for (const d of result2.diagnostics) {
3938
- console.error(`${(0, import_node_path3.relative)(process.cwd(), d.file)}:${d.line}:${d.column} ${d.severity === "warning" ? "warning: " : ""}${d.message}`);
4467
+ console.error(`${shown(d.file)}:${d.line}:${d.column} ${d.severity === "warning" ? "warning: " : ""}${d.message}`);
3939
4468
  }
3940
4469
  if (result2.code === void 0) {
3941
- console.error("no bundle written");
3942
- process.exit(1);
3943
- }
3944
- for (const file of result2.files) {
3945
- const target = (0, import_node_path3.join)(folder, file.path);
3946
- (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(target), { recursive: true });
3947
- (0, import_node_fs4.writeFileSync)(target, file.code);
3948
- console.log(`${target}: ${file.modules.length} module${file.modules.length === 1 ? "" : "s"}`);
4470
+ console.error("no output written");
4471
+ return false;
4472
+ }
4473
+ const targets = result2.files.map((file) => ({ ...file, target: (0, import_node_path3.join)(folder, file.path) }));
4474
+ if (mirror) {
4475
+ const compiled = new Map(targets.map((file) => [(0, import_node_path3.resolve)(file.target), file]));
4476
+ const copies = [];
4477
+ let clash = false;
4478
+ const leftovers = [];
4479
+ for (const source of filesUnder(rootDir)) {
4480
+ if (source.endsWith(".tilua")) continue;
4481
+ if (/\.luau?$/.test(source) && isTilua(source)) {
4482
+ leftovers.push(source);
4483
+ continue;
4484
+ }
4485
+ const target = (0, import_node_path3.resolve)(folder, (0, import_node_path3.relative)(rootDir, source));
4486
+ if (compiled.has(target)) {
4487
+ console.error(`${shown(source)}: a .tilua compiles to the same file \u2014 rename or remove one of them`);
4488
+ clash = true;
4489
+ continue;
4490
+ }
4491
+ copies.push({ source, target });
4492
+ }
4493
+ if (clash) return false;
4494
+ for (const leftover of leftovers) {
4495
+ (0, import_node_fs4.rmSync)(leftover);
4496
+ console.log(`${shown(leftover)}: removed \u2014 tilua compiled it beside its .tilua, and the output is in ${shown(folder)} now`);
4497
+ }
4498
+ for (const file of targets) {
4499
+ if ((0, import_node_fs4.existsSync)(file.target) && (0, import_node_fs4.readFileSync)(file.target, "utf8") === file.code) continue;
4500
+ (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(file.target), { recursive: true });
4501
+ (0, import_node_fs4.writeFileSync)(file.target, file.code);
4502
+ console.log(`${shown(file.target)}: ${file.modules.length} module${file.modules.length === 1 ? "" : "s"}`);
4503
+ }
4504
+ for (const { source, target } of copies) {
4505
+ const bytes = (0, import_node_fs4.readFileSync)(source);
4506
+ if ((0, import_node_fs4.existsSync)(target) && (0, import_node_fs4.readFileSync)(target).equals(bytes)) continue;
4507
+ (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(target), { recursive: true });
4508
+ (0, import_node_fs4.copyFileSync)(source, target);
4509
+ console.log(`${shown(target)}: copied`);
4510
+ }
4511
+ const kept = /* @__PURE__ */ new Set([...compiled.keys(), ...copies.map((copy) => copy.target)]);
4512
+ for (const stale of filesUnder(folder)) {
4513
+ if (kept.has((0, import_node_path3.resolve)(stale))) continue;
4514
+ (0, import_node_fs4.rmSync)(stale);
4515
+ console.log(`${shown(stale)}: removed`);
4516
+ }
4517
+ const folders = /* @__PURE__ */ new Set([folder, ...under(rootDir).folders.map((path) => (0, import_node_path3.resolve)(folder, (0, import_node_path3.relative)(rootDir, path)))]);
4518
+ for (const path of folders) (0, import_node_fs4.mkdirSync)(path, { recursive: true });
4519
+ removeEmpty(folder, folders);
4520
+ return !result2.diagnostics.some((d) => d.severity !== "warning");
4521
+ }
4522
+ if (perFile) {
4523
+ const theirs = targets.filter((file) => (0, import_node_fs4.existsSync)(file.target) && !isTilua(file.target));
4524
+ for (const file of theirs) {
4525
+ 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`);
4526
+ }
4527
+ if (theirs.length) return false;
4528
+ }
4529
+ for (const file of targets) {
4530
+ if ((0, import_node_fs4.existsSync)(file.target) && (0, import_node_fs4.readFileSync)(file.target, "utf8") === file.code) continue;
4531
+ (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(file.target), { recursive: true });
4532
+ (0, import_node_fs4.writeFileSync)(file.target, file.code);
4533
+ console.log(`${shown(file.target)}: ${file.modules.length} module${file.modules.length === 1 ? "" : "s"}`);
4534
+ }
4535
+ if (perFile) {
4536
+ const kept = new Set(targets.map((file) => (0, import_node_path3.resolve)(file.target)));
4537
+ for (const stale of writtenByTilua(folder)) {
4538
+ if (kept.has((0, import_node_path3.resolve)(stale))) continue;
4539
+ (0, import_node_fs4.rmSync)(stale);
4540
+ console.log(`${shown(stale)}: removed, its .tilua is gone`);
4541
+ }
3949
4542
  }
3950
- process.exit(result2.diagnostics.some((d) => d.severity !== "warning") ? 1 : 0);
4543
+ return !result2.diagnostics.some((d) => d.severity !== "warning");
4544
+ }
4545
+ async function watchAndBuild(from) {
4546
+ const found = resolveConfig(config, (0, import_node_path3.resolve)(from ?? "tilua.config.json")).config;
4547
+ const folder = found?.directory ?? (from !== void 0 ? (0, import_node_path3.dirname)((0, import_node_path3.resolve)(from)) : process.cwd());
4548
+ const rootDir = found?.rootDir;
4549
+ const outDir = (0, import_node_path3.resolve)(out ?? found?.outDir ?? folder);
4550
+ const mirrored = (output ?? found?.output) === "files" && rootDir !== void 0 && outDir !== (0, import_node_path3.resolve)(rootDir);
4551
+ let running = false;
4552
+ let again = false;
4553
+ let timer;
4554
+ const run = async () => {
4555
+ if (running) {
4556
+ again = true;
4557
+ return;
4558
+ }
4559
+ running = true;
4560
+ const ok = await build(from).catch((error) => {
4561
+ console.error(error instanceof Error ? error.message : error);
4562
+ return false;
4563
+ });
4564
+ console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] ${ok ? "built" : "built with errors"}; watching ${shown(folder)}`);
4565
+ running = false;
4566
+ if (again) {
4567
+ again = false;
4568
+ await run();
4569
+ }
4570
+ };
4571
+ await run();
4572
+ (0, import_node_fs4.watch)(folder, { recursive: true }, (_event, name) => {
4573
+ if (!name) return;
4574
+ const path = (0, import_node_path3.resolve)(folder, name.toString());
4575
+ if ((0, import_node_path3.relative)(folder, path).split(/[\\/]/).includes("node_modules") || isInside(outDir, path) && mirrored) return;
4576
+ const read3 = /\.tilua$|tilua\.config\.jsonc?$|\.project\.json$/.test(path) || mirrored && isInside(rootDir, path);
4577
+ if (!read3) return;
4578
+ clearTimeout(timer);
4579
+ timer = setTimeout(() => void run(), 100);
4580
+ });
4581
+ }
4582
+ async function main() {
4583
+ if (entry === "init") init();
4584
+ if (watching) return watchAndBuild(entry);
4585
+ process.exit(await build(entry) ? 0 : 1);
3951
4586
  }
3952
- main(entry).catch((error) => {
4587
+ main().catch((error) => {
3953
4588
  console.error(error instanceof Error ? error.message : error);
3954
4589
  process.exit(1);
3955
4590
  });