@tilua/compiler 8.0.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,12 +3083,75 @@ 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
- function lineMapSource(map) {
3100
+ var VLQ_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3101
+ function vlq(n) {
3102
+ let value = n < 0 ? -n * 2 + 1 : n * 2;
3103
+ let out2 = "";
3104
+ do {
3105
+ const digit = value % 32;
3106
+ value = Math.floor(value / 32);
3107
+ out2 += VLQ_DIGITS[value > 0 ? digit + 32 : digit];
3108
+ } while (value > 0);
3109
+ return out2;
3110
+ }
3111
+ function lineMapSource(map, unpack) {
3074
3112
  const entries = [...map.entries()].sort((a, b) => a[0] - b[0]);
3075
- const quoted = (s) => (0, import_parser6.luauString)(s.replace(/\\/g, "/"));
3076
- return `{${entries.map(([line, o]) => `[${line}]={${quoted(o.file)},${o.line}}`).join(",")}}`;
3113
+ const files = [];
3114
+ const indexes = /* @__PURE__ */ new Map();
3115
+ let data = "";
3116
+ let line = 0, file = 0, source = 0;
3117
+ for (const [at, origin] of entries) {
3118
+ const name = origin.file.replace(/\\/g, "/");
3119
+ if (!indexes.has(name)) indexes.set(name, files.push(name));
3120
+ const index2 = indexes.get(name);
3121
+ data += vlq(at - line) + vlq(index2 - file) + vlq(origin.line - source);
3122
+ line = at;
3123
+ file = index2;
3124
+ source = origin.line;
3125
+ }
3126
+ return `${unpack}({${files.map(import_parser6.luauString).join(",")}},${(0, import_parser6.luauString)(data)})`;
3127
+ }
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
+ }
3077
3155
  }
3078
3156
 
3079
3157
  // src/lowering.ts
@@ -3092,7 +3170,7 @@ async function loadLowerings(modules) {
3092
3170
  });
3093
3171
  continue;
3094
3172
  }
3095
- lowerings.push({ plugin: plugin3, from: module2.from, library: module2.library });
3173
+ lowerings.push({ plugin: plugin3, from: module2.from, library: module2.library, options: module2.options });
3096
3174
  } catch (error) {
3097
3175
  problems.push({
3098
3176
  file: module2.file,
@@ -3104,17 +3182,30 @@ async function loadLowerings(modules) {
3104
3182
  }
3105
3183
 
3106
3184
  // src/bundle.ts
3185
+ var WRITTEN_BY_TILUA = "-- Written by tilua";
3107
3186
  async function bundle(options) {
3108
- const entry2 = (0, import_node_path2.resolve)(options.entry);
3109
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");
3110
3189
  const inline = options.config !== void 0 && typeof options.config !== "string";
3111
- 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()));
3112
3191
  for (const p of problems) diagnostics.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
3113
- 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));
3114
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');
3115
3205
  const sources = /* @__PURE__ */ new Map();
3116
3206
  const directives = /* @__PURE__ */ new Map();
3117
- 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) || "."}`);
3118
3209
  while (queue.length) {
3119
3210
  const file = queue.shift();
3120
3211
  if (sources.has(file)) continue;
@@ -3132,6 +3223,22 @@ async function bundle(options) {
3132
3223
  if (target && !target.endsWith(".d.tilua")) queue.push(target);
3133
3224
  }
3134
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
+ }
3135
3242
  const analysis = analyzeModules([...sources.values()], config2);
3136
3243
  const { lowerings, problems: loweringProblems } = await loadLowerings(analysis.lowerings);
3137
3244
  for (const p of loweringProblems) {
@@ -3142,23 +3249,61 @@ async function bundle(options) {
3142
3249
  const G = names.fresh("G");
3143
3250
  const requireExpression = member(identifier(G), "require");
3144
3251
  const importExpression = member(identifier(G), "import");
3145
- const mode = options.output ?? config2?.output ?? "single";
3146
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;
3147
3289
  const modules = /* @__PURE__ */ new Map();
3148
3290
  const origins = /* @__PURE__ */ new WeakMap();
3149
3291
  const shared = new SharedRuntime(names);
3150
3292
  const shaking = options.treeShaking ?? config2?.treeShaking ?? false;
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);
3151
3296
  const resolveIn = (file) => (specifier) => {
3152
3297
  const found = (0, import_parser7.resolveModulePath)(file, specifier, config2);
3153
3298
  return found !== void 0 && sources.has(found) ? found : void 0;
3154
3299
  };
3155
- const shaken = shaking ? treeShake([...sources.values()].map((s) => ({ file: s.file, program: s.program, scopes: s.scopes, resolve: resolveIn(s.file) })), entry2) : void 0;
3156
- 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];
3157
3302
  while (pending.length) {
3158
3303
  const file = pending.shift();
3159
3304
  if (modules.has(file)) continue;
3160
3305
  const source = sources.get(file);
3161
- const shown = (0, import_node_path2.relative)(root, file).replace(/\\/g, "/");
3306
+ const shown2 = (0, import_node_path2.relative)(root, file).replace(/\\/g, "/");
3162
3307
  const imports = /* @__PURE__ */ new Set();
3163
3308
  const dynamicImports = /* @__PURE__ */ new Set();
3164
3309
  const lowered2 = lower(source.program, source.scopes, {
@@ -3167,7 +3312,7 @@ async function bundle(options) {
3167
3312
  lowerings,
3168
3313
  noLib: config2?.noLib,
3169
3314
  source: source.text,
3170
- file: shown,
3315
+ file: shown2,
3171
3316
  shared,
3172
3317
  shaken: shaken && {
3173
3318
  ...shaken.modules.get(file),
@@ -3186,9 +3331,16 @@ async function bundle(options) {
3186
3331
  import: importExpression,
3187
3332
  // One file has one line map. Several have one each, and a
3188
3333
  // module reads the one of the file it is in.
3189
- lines: mode === "single" ? `${G}.lines` : `${G}.maps[${(0, import_parser7.luauString)(source.name)}]`,
3190
- fail: `${G}.fail`,
3334
+ lines: !mapping ? void 0 : mode === "single" ? `${G}.lines` : `${G}.maps[${(0, import_parser7.luauString)(source.name)}]`,
3335
+ fail: mapping ? `${G}.fail` : void 0,
3191
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
+ },
3192
3344
  resolve: (specifier, dynamic) => {
3193
3345
  const found = (0, import_parser7.resolveModulePath)(file, specifier, config2);
3194
3346
  if (!found || found.endsWith(".d.tilua") || !sources.has(found)) return void 0;
@@ -3202,7 +3354,7 @@ async function bundle(options) {
3202
3354
  for (const d of lowered2.diagnostics) diagnostics.push({ file, ...d, category: "module" });
3203
3355
  for (const statement of allStatements(lowered2.statements)) {
3204
3356
  const line = lowered2.origins.get(statement);
3205
- if (line !== void 0) origins.set(statement, { file: shown, line });
3357
+ if (line !== void 0) origins.set(statement, { file: shown2, line });
3206
3358
  }
3207
3359
  modules.set(file, {
3208
3360
  name: source.name,
@@ -3240,6 +3392,102 @@ async function bundle(options) {
3240
3392
  return { files: [], modules: moduleNames, diagnostics };
3241
3393
  }
3242
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
+ }
3243
3491
  const several = mode !== "single";
3244
3492
  const layout = layoutOf(mode, modules.get(entry2), lowered, options.outFile);
3245
3493
  const usesScriptArgs = lowered.some((m) => m.usesScriptArgs);
@@ -3247,17 +3495,18 @@ async function bundle(options) {
3247
3495
  for (const file of layout.slice(1)) {
3248
3496
  for (const module2 of file.modules) elsewhere.set(module2.name, loadPath(layout[0].path, file.path, byPath));
3249
3497
  }
3250
- const program2 = (0, import_luau_parser2.parse)(runtime(G, {
3498
+ const program2 = (0, import_luau_parser3.parse)(runtime(G, {
3251
3499
  several,
3252
3500
  byPath,
3253
3501
  files: elsewhere,
3254
- imports: lowered.some((m) => m.dynamicImports.size > 0)
3502
+ imports: lowered.some((m) => m.dynamicImports.size > 0),
3503
+ mapping
3255
3504
  }));
3256
3505
  const assignment = program2.body.statements[1];
3257
3506
  if (usesScriptArgs) {
3258
3507
  program2.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
3259
- program2.body.statements.push(...(0, import_luau_parser2.parse)(`${G}.arrays[scriptArgs] = true`).body.statements);
3260
- 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);
3261
3510
  }
3262
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;
3263
3512
  if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
@@ -3268,31 +3517,42 @@ async function bundle(options) {
3268
3517
  program2.body.statements.unshift(...helpers.declare);
3269
3518
  const ok = names.fresh("ok");
3270
3519
  const result2 = names.fresh("result");
3271
- const entryStatements = (0, import_luau_parser2.parse)(`
3520
+ const entryName = (0, import_parser7.luauString)(sources.get(entry2).name);
3521
+ const returnsExports = modules.get(entry2).info.exports;
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})`}` : `
3272
3527
  local ${ok}, ${result2} = xpcall(function()
3273
- return ${G}.require(${(0, import_parser7.luauString)(sources.get(entry2).name)})
3528
+ return ${G}.require(${entryName})
3274
3529
  end, ${G}.fail)
3275
3530
  if not ${ok} then
3276
3531
  error(${result2}, 0)
3277
3532
  end
3278
- ${modules.get(entry2).info.exports ? `return ${result2}` : ""}
3533
+ ${declarations}
3534
+ ${returned ? `return ${returned.value}` : ""}
3279
3535
  `).body.statements;
3280
3536
  program2.body.statements.push(...entryStatements);
3281
- const map = buildLineMap(program2, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(program2)), origins);
3282
- if (map.size || several) {
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();
3540
+ if (mapping && (map.size || several)) {
3283
3541
  const mapStatements = [assign(
3284
3542
  [member(identifier(G), "lines")],
3285
- [(0, import_luau_parser2.parseExpressionFromSource)(lineMapSource(map))]
3543
+ [(0, import_luau_parser3.parseExpressionFromSource)(lineMapSource(map, `${G}.lineMap`))]
3286
3544
  )];
3287
3545
  if (several) mapStatements.push(...mapRegistration(G, layout[0], `${G}.lines`));
3288
3546
  program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...mapStatements);
3289
3547
  }
3290
3548
  program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...helpers.define);
3291
- 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) }];
3292
3552
  for (const file of layout.slice(1)) {
3293
3553
  const fileHelpers = shared.chunk(new Set(file.modules.flatMap((m) => m.shared)));
3294
3554
  const body = [...fileHelpers.declare];
3295
- 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);
3296
3556
  for (const module2 of file.modules) {
3297
3557
  body.push(assign(
3298
3558
  [index(member(identifier(G), "modules"), string(module2.name))],
@@ -3300,12 +3560,35 @@ ${modules.get(entry2).info.exports ? `return ${result2}` : ""}
3300
3560
  ));
3301
3561
  }
3302
3562
  const chunk = program([returns([functionExpression(functionBody([G], body))])]);
3303
- const lines = buildLineMap(chunk, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(chunk)), origins);
3304
- body.push(...mapRegistration(G, file, lineMapSource(lines)), ...fileHelpers.define);
3305
- files.push({ path: file.path, code: (0, import_luau_parser2.print)(chunk) + "\n", modules: file.modules.map((m) => m.name) });
3563
+ if (mapping) {
3564
+ const lines = buildLineMap(chunk, (0, import_luau_parser3.parse)((0, import_luau_parser3.print)(chunk, printing)), origins);
3565
+ body.push(...mapRegistration(G, file, lineMapSource(lines, `${G}.lineMap`)));
3566
+ }
3567
+ body.push(...fileHelpers.define);
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) });
3306
3571
  }
3307
3572
  return { code: files[0].code, files, modules: moduleNames, diagnostics };
3308
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
+ }
3309
3592
  function moduleTable(module2) {
3310
3593
  const { names: exported, links, stars } = module2.info;
3311
3594
  const fields = [];
@@ -3325,10 +3608,6 @@ function layoutOf(mode, entry2, modules, outFile) {
3325
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);
3326
3609
  const others = modules.filter((m) => m !== entry2);
3327
3610
  if (mode === "single") return [{ path: `${entryStem}${extension}`, modules: [entry2, ...others] }];
3328
- if (mode === "modules") {
3329
- const pathOf = (m) => `${m.name.split("/").map((part) => part === ".." ? "__" : part).join("/")}${extension}`;
3330
- return [entry2, ...others].map((m) => ({ path: pathOf(m), modules: [m] }));
3331
- }
3332
3611
  const byName = new Map(modules.map((m) => [m.name, m]));
3333
3612
  const reach = (root) => {
3334
3613
  const seen = /* @__PURE__ */ new Set();
@@ -3364,16 +3643,75 @@ function layoutOf(mode, entry2, modules, outFile) {
3364
3643
  return files;
3365
3644
  }
3366
3645
  function loadPath(entryPath, file, byPath) {
3367
- const path = import_node_path2.posix.relative(import_node_path2.posix.dirname(entryPath), file);
3368
- 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);
3369
3681
  const bare = path.slice(0, path.length - import_node_path2.posix.extname(path).length);
3370
3682
  return bare.startsWith("../") ? bare : `./${bare}`;
3371
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
+ }
3372
3710
  function mapRegistration(G, file, map) {
3373
3711
  const extension = import_node_path2.posix.extname(file.path);
3374
3712
  const parts = file.path.slice(0, file.path.length - extension.length).split(/[/.]/).filter((part) => part !== "");
3375
3713
  const entry2 = `${G}.sources[#${G}.sources + 1]`;
3376
- return (0, import_luau_parser2.parse)([
3714
+ return (0, import_luau_parser3.parse)([
3377
3715
  `${entry2} = { { ${parts.map(import_parser7.luauString).join(", ")} }, ${map}, ${(0, import_parser7.luauString)(extension.slice(1))} }`,
3378
3716
  ...file.modules.map((module2) => `${G}.maps[${(0, import_parser7.luauString)(module2.name)}] = ${G}.sources[#${G}.sources][2]`)
3379
3717
  ].join("\n")).body.statements;
@@ -3399,7 +3737,8 @@ function loader(G, byPath) {
3399
3737
  require(file)(${G})`;
3400
3738
  }
3401
3739
  function runtime(G, options) {
3402
- const { several } = options;
3740
+ const { several, mapping } = options;
3741
+ const perFile = options.perFile ?? false;
3403
3742
  const files = [...options.files].map(([name, file]) => `[${(0, import_parser7.luauString)(name)}] = ${(0, import_parser7.luauString)(file)}`).join(", ");
3404
3743
  return `
3405
3744
  local ${G}
@@ -3407,7 +3746,7 @@ ${G} = {
3407
3746
  modules = {},
3408
3747
  -- Every array the program made, so \`typeof\` can tell one from an object.
3409
3748
  -- The keys are held weakly: being written down keeps no array alive.
3410
- arrays = setmetatable({}, { __mode = "k" }),
3749
+ arrays = setmetatable({}, { __mode = "k" }),${mapping ? `
3411
3750
  -- Filled in just before the entry runs: bundle line -> { file, line }.
3412
3751
  lines = nil,
3413
3752
  -- What an error nothing caught says, in the project's files: the bundle's
@@ -3575,32 +3914,96 @@ ${several ? `
3575
3914
  end
3576
3915
  return mapped .. "\\n" .. table.concat(frames, "\\n")
3577
3916
  end,
3578
- records = {},${several ? `
3917
+ -- A line map as it is written: the files it names, and base-64 VLQ
3918
+ -- numbers, three to a line \u2014 how far down the bundle, which file, how far
3919
+ -- down that file, each counted from the line before. It is read out the
3920
+ -- first time a line is looked up, which is when something went wrong.
3921
+ lineMap = function(files, data)
3922
+ local map = {}
3923
+ return setmetatable(map, { __index = function(_, key)
3924
+ setmetatable(map, nil)
3925
+ local digits = {}
3926
+ for i = 1, 64 do
3927
+ digits[("${VLQ_DIGITS}"):byte(i)] = i - 1
3928
+ end
3929
+ local at = 1
3930
+ local function read()
3931
+ local value, scale = 0, 1
3932
+ repeat
3933
+ local digit = digits[data:byte(at)]
3934
+ at = at + 1
3935
+ value = value + digit % 32 * scale
3936
+ scale = scale * 32
3937
+ until digit < 32
3938
+ if value % 2 == 1 then
3939
+ return -math.floor(value / 2)
3940
+ end
3941
+ return math.floor(value / 2)
3942
+ end
3943
+ local line, file, source = 0, 0, 0
3944
+ while at <= #data do
3945
+ line = line + read()
3946
+ file = file + read()
3947
+ source = source + read()
3948
+ map[line] = { files[file], source }
3949
+ end
3950
+ return map[key]
3951
+ end })
3952
+ end,` : ""}
3953
+ records = {},${several && !perFile ? `
3579
3954
  -- The file each module outside this one is in, as \`load\` is handed it.
3580
3955
  files = { ${files} },
3581
- loaded = {},
3956
+ loaded = {},` : ""}${several && mapping ? `
3582
3957
  -- Every file's line map: by the module, and by the file's name.
3583
3958
  maps = {},
3584
- sources = {},
3959
+ sources = {},` : ""}${several && !perFile ? `
3585
3960
  -- Another of the bundle's files: a function that adds its modules here.
3586
3961
  load = function(file)
3587
3962
  if ${G}.loaded[file] then
3588
3963
  return
3589
3964
  end
3590
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
3591
3982
  end,` : ""}${options.imports ? `
3592
3983
  -- \`import("./x")\`: the module's exports, asked for where the code runs
3593
3984
  -- rather than before its module does. \`require\` does the rest \u2014 and
3594
3985
  -- loads the file the module is in, if it is not this one.
3595
- import = function(name)
3596
- return ${G}.require(name)
3986
+ import = function(name${perFile ? ", load" : ""})
3987
+ return ${G}.require(name${perFile ? ", load" : ""})
3597
3988
  end,` : ""}
3598
- require = function(name)
3989
+ require = function(name${perFile ? ", load" : ""})
3599
3990
  local record = ${G}.records[name]
3600
3991
  if record ~= nil then
3601
3992
  return record.exports
3602
3993
  end
3603
- 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 ? `
3604
4007
  if module == nil then
3605
4008
  ${G}.load(${G}.files[name])
3606
4009
  module = ${G}.modules[name]
@@ -3706,7 +4109,7 @@ var moduleAnalyses = /* @__PURE__ */ new Map();
3706
4109
  function analyzeModules(modules, config2) {
3707
4110
  const out2 = [];
3708
4111
  const analyses = /* @__PURE__ */ new Map();
3709
- const libraries = config2 ? (0, import_parser7.resolveTypeLibraries)(config2) : { files: [], lowerings: [], problems: [] };
4112
+ const libraries = config2 ? (0, import_parser7.resolveTypeLibraries)(config2) : { files: [], lowerings: [], options: [], problems: [] };
3710
4113
  for (const p of libraries.problems) out2.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
3711
4114
  const libs = libraries.files.map(parsedLibrary);
3712
4115
  const noLib = config2?.noLib ?? false;
@@ -3745,7 +4148,7 @@ function analyzeModules(modules, config2) {
3745
4148
  const at = d.node;
3746
4149
  said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type", severity: d.severity });
3747
4150
  }
3748
- const result2 = { types, diagnostics: said };
4151
+ const result2 = { types, diagnostics: said, scopes };
3749
4152
  if (partials === before) {
3750
4153
  analyzed.set(file, result2);
3751
4154
  const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
@@ -3802,7 +4205,16 @@ function analyzeModules(modules, config2) {
3802
4205
  analyses.set(module2.file, types);
3803
4206
  out2.push(...diagnostics);
3804
4207
  }
3805
- return { types: analyses, diagnostics: out2, lowerings: [...libraries.lowerings] };
4208
+ const exportsOf = (file) => {
4209
+ const cached = exportsCache.get(file);
4210
+ if (cached) return cached;
4211
+ const module2 = byFile.get(file);
4212
+ const { types, scopes } = analyzeModule(file, module2.program);
4213
+ const exports2 = (0, import_parser7.moduleExports)(module2.program, scopes, types, resolverFor(file));
4214
+ exportsCache.set(file, exports2);
4215
+ return exports2;
4216
+ };
4217
+ return { types: analyses, diagnostics: out2, lowerings: [...libraries.lowerings], exportsOf };
3806
4218
  }
3807
4219
  function allStatements(statements) {
3808
4220
  const out2 = [];
@@ -3822,25 +4234,35 @@ function allStatements(statements) {
3822
4234
  }
3823
4235
 
3824
4236
  // src/cli.ts
3825
- var usage = "usage: tilua <entry> [--out <file>] [--output single|chunks|modules] [--config <tilua.config.json>] [--noCheck]";
4237
+ var usage = "usage: tilua [<entry>] [--out <file>] [--output single|chunks|files] [--config <tilua.config.json>] [--noCheck] [--noLineMap] [--minify] [--watch]\n tilua init";
3826
4238
  var args = process.argv.slice(2);
3827
4239
  var entry;
3828
4240
  var out;
3829
4241
  var config;
3830
4242
  var typeCheck = true;
4243
+ var lineMap;
4244
+ var minify;
3831
4245
  var output;
4246
+ var watching = false;
3832
4247
  for (let i = 0; i < args.length; i++) {
3833
4248
  const arg = args[i];
3834
4249
  if (arg === "--out") out = args[++i];
3835
4250
  else if (arg === "--config") config = args[++i];
3836
4251
  else if (arg === "--noCheck") typeCheck = false;
4252
+ else if (arg === "--noLineMap") lineMap = false;
4253
+ else if (arg === "--minify") minify = true;
4254
+ else if (arg === "--watch" || arg === "-w") watching = true;
3837
4255
  else if (arg === "--target") {
3838
4256
  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'`);
3839
4257
  process.exit(2);
3840
4258
  } else if (arg === "--output") {
3841
4259
  const value = args[++i];
3842
- if (value !== "single" && value !== "chunks" && value !== "modules") {
3843
- console.error(`--output must be 'single', 'chunks' or 'modules', not '${value}'`);
4260
+ if (value === "modules") {
4261
+ console.error("--output modules is gone: --output files writes one file per module, each of which returns its exports");
4262
+ process.exit(2);
4263
+ }
4264
+ if (value !== "single" && value !== "chunks" && value !== "files") {
4265
+ console.error(`--output must be 'single', 'chunks' or 'files', not '${value}'`);
3844
4266
  process.exit(2);
3845
4267
  }
3846
4268
  output = value;
@@ -3849,38 +4271,162 @@ for (let i = 0; i < args.length; i++) {
3849
4271
  process.exit(0);
3850
4272
  } else entry = arg;
3851
4273
  }
3852
- if (!entry) {
3853
- console.error(usage);
3854
- process.exit(2);
4274
+ var shown = (path) => (0, import_node_path3.relative)(process.cwd(), path) || ".";
4275
+ function init() {
4276
+ const folder = process.cwd();
4277
+ for (const name of ["tilua.config.json", "tilua.config.jsonc"]) {
4278
+ if ((0, import_node_fs4.existsSync)((0, import_node_path3.join)(folder, name))) {
4279
+ console.error(`${name} is already here`);
4280
+ process.exit(1);
4281
+ }
4282
+ }
4283
+ let rootDir = "src";
4284
+ const project = (0, import_node_path3.join)(folder, "default.project.json");
4285
+ if ((0, import_node_fs4.existsSync)(project)) {
4286
+ const paths = [];
4287
+ const walk2 = (node) => {
4288
+ if (!node || typeof node !== "object") return;
4289
+ const path = node.$path;
4290
+ const named = typeof path === "string" ? path : path?.optional;
4291
+ if (typeof named === "string" && !/\.\w+$/.test(named)) paths.push(named);
4292
+ for (const [key, child] of Object.entries(node)) if (!key.startsWith("$")) walk2(child);
4293
+ };
4294
+ try {
4295
+ walk2(JSON.parse((0, import_node_fs4.readFileSync)(project, "utf8")).tree);
4296
+ } catch (error) {
4297
+ console.error(`default.project.json: ${error.message}`);
4298
+ process.exit(1);
4299
+ }
4300
+ const split = paths.map((path) => path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "").split("/"));
4301
+ const shared = [];
4302
+ for (let i = 0; split.length && split.every((parts) => parts.length > i && parts[i] === split[0][i]); i++) shared.push(split[0][i]);
4303
+ if (split.length) rootDir = shared.join("/") || ".";
4304
+ } else {
4305
+ console.error('No default.project.json here: rootDir is "src". Change it if the project reads its files from elsewhere.');
4306
+ }
4307
+ const written = { types: ["roblox"], output: "files", rootDir };
4308
+ (0, import_node_fs4.writeFileSync)((0, import_node_path3.join)(folder, "tilua.config.json"), JSON.stringify(written, null, 4) + "\n");
4309
+ console.log("tilua.config.json: a .luau beside each .tilua under " + rootDir);
4310
+ console.log("Next: npm i -D @tilua/compiler @tilua-types/roblox, then run `npx tilua --watch` beside `rojo serve`.");
4311
+ process.exit(0);
4312
+ }
4313
+ function writtenByTilua(folder) {
4314
+ const found = [];
4315
+ const walk2 = (at) => {
4316
+ let items;
4317
+ try {
4318
+ items = (0, import_node_fs4.readdirSync)(at, { withFileTypes: true });
4319
+ } catch {
4320
+ return;
4321
+ }
4322
+ for (const item of items) {
4323
+ if (item.name.startsWith(".") || item.name === "node_modules") continue;
4324
+ const path = (0, import_node_path3.join)(at, item.name);
4325
+ if (item.isDirectory()) walk2(path);
4326
+ else if (/\.luau?$/.test(item.name) && isTilua(path)) found.push(path);
4327
+ }
4328
+ };
4329
+ walk2(folder);
4330
+ return found;
4331
+ }
4332
+ function isTilua(path) {
4333
+ try {
4334
+ const text = (0, import_node_fs4.readFileSync)(path, "utf8").trimEnd();
4335
+ return text.slice(text.lastIndexOf("\n") + 1).startsWith(WRITTEN_BY_TILUA);
4336
+ } catch {
4337
+ return false;
4338
+ }
3855
4339
  }
3856
- async function main(from) {
3857
- const found = resolveConfig(config, (0, import_node_path3.resolve)(from)).config;
4340
+ async function build(from) {
4341
+ const found = resolveConfig(config, (0, import_node_path3.resolve)(from ?? "tilua.config.json")).config;
3858
4342
  const mode = output ?? found?.output ?? "single";
3859
- const entryFile = mode === "modules" ? void 0 : out ?? from.replace(/\.tilua$/, "") + ".luau";
3860
- const folder = entryFile !== void 0 ? (0, import_node_path3.dirname)(entryFile) : out ?? found?.directory ?? (0, import_node_path3.dirname)(from);
4343
+ if (mode !== "files" && from === void 0) {
4344
+ console.error(usage);
4345
+ process.exit(2);
4346
+ }
4347
+ const perFile = mode === "files";
4348
+ const entryFile = perFile || from === void 0 ? void 0 : out ?? from.replace(/\.tilua$/, "") + ".luau";
4349
+ const folder = (0, import_node_path3.resolve)(entryFile !== void 0 ? (0, import_node_path3.dirname)(entryFile) : out ?? found?.outDir ?? process.cwd());
3861
4350
  const result2 = await bundle({
3862
- entry: from,
4351
+ entry: perFile ? void 0 : from,
3863
4352
  config,
3864
4353
  typeCheck,
3865
4354
  output: mode,
4355
+ lineMap,
4356
+ minify,
4357
+ outDir: perFile ? out : void 0,
3866
4358
  outFile: entryFile !== void 0 ? (0, import_node_path3.basename)(entryFile) : void 0
3867
4359
  });
3868
4360
  for (const d of result2.diagnostics) {
3869
- console.error(`${(0, import_node_path3.relative)(process.cwd(), d.file)}:${d.line}:${d.column} ${d.severity === "warning" ? "warning: " : ""}${d.message}`);
4361
+ console.error(`${shown(d.file)}:${d.line}:${d.column} ${d.severity === "warning" ? "warning: " : ""}${d.message}`);
3870
4362
  }
3871
4363
  if (result2.code === void 0) {
3872
- console.error("no bundle written");
3873
- process.exit(1);
3874
- }
3875
- for (const file of result2.files) {
3876
- const target = (0, import_node_path3.join)(folder, file.path);
3877
- (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(target), { recursive: true });
3878
- (0, import_node_fs4.writeFileSync)(target, file.code);
3879
- console.log(`${target}: ${file.modules.length} module${file.modules.length === 1 ? "" : "s"}`);
4364
+ console.error("no output written");
4365
+ return false;
4366
+ }
4367
+ const targets = result2.files.map((file) => ({ ...file, target: (0, import_node_path3.join)(folder, file.path) }));
4368
+ if (perFile) {
4369
+ const theirs = targets.filter((file) => (0, import_node_fs4.existsSync)(file.target) && !isTilua(file.target));
4370
+ for (const file of theirs) {
4371
+ 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`);
4372
+ }
4373
+ if (theirs.length) return false;
4374
+ }
4375
+ for (const file of targets) {
4376
+ if ((0, import_node_fs4.existsSync)(file.target) && (0, import_node_fs4.readFileSync)(file.target, "utf8") === file.code) continue;
4377
+ (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(file.target), { recursive: true });
4378
+ (0, import_node_fs4.writeFileSync)(file.target, file.code);
4379
+ console.log(`${shown(file.target)}: ${file.modules.length} module${file.modules.length === 1 ? "" : "s"}`);
4380
+ }
4381
+ if (perFile) {
4382
+ const kept = new Set(targets.map((file) => (0, import_node_path3.resolve)(file.target)));
4383
+ for (const stale of writtenByTilua(folder)) {
4384
+ if (kept.has((0, import_node_path3.resolve)(stale))) continue;
4385
+ (0, import_node_fs4.rmSync)(stale);
4386
+ console.log(`${shown(stale)}: removed, its .tilua is gone`);
4387
+ }
3880
4388
  }
3881
- process.exit(result2.diagnostics.some((d) => d.severity !== "warning") ? 1 : 0);
4389
+ return !result2.diagnostics.some((d) => d.severity !== "warning");
4390
+ }
4391
+ async function watchAndBuild(from) {
4392
+ const found = resolveConfig(config, (0, import_node_path3.resolve)(from ?? "tilua.config.json")).config;
4393
+ const folder = found?.directory ?? (from !== void 0 ? (0, import_node_path3.dirname)((0, import_node_path3.resolve)(from)) : process.cwd());
4394
+ let running = false;
4395
+ let again = false;
4396
+ let timer;
4397
+ const run = async () => {
4398
+ if (running) {
4399
+ again = true;
4400
+ return;
4401
+ }
4402
+ running = true;
4403
+ const ok = await build(from).catch((error) => {
4404
+ console.error(error instanceof Error ? error.message : error);
4405
+ return false;
4406
+ });
4407
+ console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] ${ok ? "built" : "built with errors"}; watching ${shown(folder)}`);
4408
+ running = false;
4409
+ if (again) {
4410
+ again = false;
4411
+ await run();
4412
+ }
4413
+ };
4414
+ await run();
4415
+ (0, import_node_fs4.watch)(folder, { recursive: true }, (_event, name) => {
4416
+ if (!name) return;
4417
+ const path = name.toString();
4418
+ if (path.split(import_node_path3.sep).includes("node_modules")) return;
4419
+ if (!/\.tilua$|tilua\.config\.jsonc?$|\.project\.json$/.test(path)) return;
4420
+ clearTimeout(timer);
4421
+ timer = setTimeout(() => void run(), 100);
4422
+ });
4423
+ }
4424
+ async function main() {
4425
+ if (entry === "init") init();
4426
+ if (watching) return watchAndBuild(entry);
4427
+ process.exit(await build(entry) ? 0 : 1);
3882
4428
  }
3883
- main(entry).catch((error) => {
4429
+ main().catch((error) => {
3884
4430
  console.error(error instanceof Error ? error.message : error);
3885
4431
  process.exit(1);
3886
4432
  });