@tilua/compiler 4.0.0 → 5.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
@@ -6,13 +6,13 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
6
6
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
7
7
 
8
8
  // src/cli.ts
9
- var import_node_fs3 = require("fs");
9
+ var import_node_fs4 = require("fs");
10
10
  var import_node_path3 = require("path");
11
11
 
12
12
  // src/bundle.ts
13
- var import_node_fs2 = require("fs");
13
+ var import_node_fs3 = require("fs");
14
14
  var import_node_path2 = require("path");
15
- var import_parser6 = require("@tilua/parser");
15
+ var import_parser7 = require("@tilua/parser");
16
16
  var import_luau_parser2 = require("luau-parser");
17
17
 
18
18
  // src/config.ts
@@ -27,7 +27,7 @@ function resolveConfig(input, entry2, cwd = process.cwd()) {
27
27
  }
28
28
 
29
29
  // src/lower.ts
30
- var import_parser4 = require("@tilua/parser");
30
+ var import_parser5 = require("@tilua/parser");
31
31
  var import_luau_parser = require("luau-parser");
32
32
 
33
33
  // src/luau.ts
@@ -91,6 +91,7 @@ var functionBody = (params, statements, hasVarargs = false) => ({
91
91
  ...span()
92
92
  });
93
93
  var localFunction = (name, func) => ({ type: "LocalFunctionStatement", name: identifier(name), func, ...span() });
94
+ var program = (statements) => ({ type: "Program", body: block(statements), ...span() });
94
95
 
95
96
  // src/names.ts
96
97
  var Names = class _Names {
@@ -114,7 +115,7 @@ var Names = class _Names {
114
115
  if (key !== "line" && key !== "column") walk(child);
115
116
  }
116
117
  };
117
- for (const program of programs) walk(program);
118
+ for (const program2 of programs) walk(program2);
118
119
  return new _Names(taken);
119
120
  }
120
121
  /** A copy that goes on to take names independently — one per module, each
@@ -188,9 +189,80 @@ var plugin = {
188
189
  };
189
190
  var languageLowering = { plugin, from: "tilua" };
190
191
 
192
+ // src/standard.ts
193
+ var import_node_fs2 = require("fs");
194
+ var import_parser4 = require("@tilua/parser");
195
+ var read2 = (name) => (0, import_node_fs2.readFileSync)(new URL(`../runtime/${name}`, importMetaUrl), "utf8");
196
+ var console2 = read2("console.luau");
197
+ var result = read2("result.luau");
198
+ var place = (at) => (0, import_parser4.luauString)(at.file === void 0 ? `line ${at.line}` : `${at.file}:${at.line}`);
199
+ var tag = (at) => (0, import_parser4.luauString)(`[${at.file === void 0 ? `line ${at.line}` : `${at.file.replace(/\.tilua$/, "")}:${at.line}`}]:`);
200
+ function consoleMeta(args2) {
201
+ const fields = [];
202
+ args2.forEach((argument, index2) => {
203
+ if (argument.spread || argument.typeText === void 0 || !(0, import_parser4.isFunctionType)(argument.type)) return;
204
+ const written = argument.declaration ?? argument.source;
205
+ fields.push(`[${index2 + 1}] = { ${(0, import_parser4.luauString)(argument.typeText)}, ${(0, import_parser4.luauString)(written)} }`);
206
+ });
207
+ return fields.length ? `{ ${fields.join(", ")} }` : "nil";
208
+ }
209
+ var CONSOLE_METHODS = /* @__PURE__ */ new Set(["log", "warn", "error"]);
210
+ var REPLACED = /* @__PURE__ */ new Map([
211
+ ["pcall", "pcall"],
212
+ ["xpcall", "xpcall"],
213
+ ["ypcall", "pcall"],
214
+ ["coroutine.resume", "resume"],
215
+ ["string.find", "find"],
216
+ ["string.match", "match"],
217
+ ["string.gsub", "gsub"],
218
+ ["string.byte", "byte"],
219
+ ["math.modf", "modf"],
220
+ ["math.frexp", "frexp"],
221
+ ["next", "next"],
222
+ ["select", "select"],
223
+ ["unpack", "unpack"],
224
+ ["table.unpack", "unpack"],
225
+ ["loadstring", "loadstring"],
226
+ ["pairs", "pairs"],
227
+ ["ipairs", "ipairs"],
228
+ ["string.gmatch", "gmatch"]
229
+ ]);
230
+ var plugin2 = {
231
+ runtime: { console: console2, result },
232
+ methodCall({ method, receiverGlobal, at, arguments: args2, use }) {
233
+ if (receiverGlobal === "console" && CONSOLE_METHODS.has(method)) {
234
+ return {
235
+ callee: `${use("console")}.${method}`,
236
+ prepend: [tag(at), consoleMeta(args2)],
237
+ passReceiver: false
238
+ };
239
+ }
240
+ return void 0;
241
+ },
242
+ // `print(x)` and `warn(x)` are left exactly as written: the place a line
243
+ // came from is what `console:log` / `console:warn` are for. `error` has to
244
+ // put the place inside the message it raises, so it goes through the
245
+ // runtime.
246
+ globalCall({ name, at, use }) {
247
+ if (name === "error") return { callee: `${use("console")}.raise`, prepend: [place(at)] };
248
+ const replaced = REPLACED.get(name);
249
+ if (replaced) return { callee: `${use("result")}.${replaced}` };
250
+ return void 0;
251
+ },
252
+ // Read as a value there is no call site to write down; the runtime finds
253
+ // one on the stack.
254
+ globalValue({ name, use }) {
255
+ if (name === "error") return `${use("console")}.raiseValue`;
256
+ const replaced = REPLACED.get(name);
257
+ if (replaced) return `${use("result")}.${replaced}`;
258
+ return void 0;
259
+ }
260
+ };
261
+ var standardLowering = { plugin: plugin2, from: "tilua (standard library)" };
262
+
191
263
  // src/lower.ts
192
- function lower(program, scopes, options = {}) {
193
- return new Lowerer(program, scopes, options).run();
264
+ function lower(program2, scopes, options = {}) {
265
+ return new Lowerer(program2, scopes, options).run();
194
266
  }
195
267
  var Lowerer = class {
196
268
  constructor(source, scopes, options) {
@@ -203,6 +275,18 @@ var Lowerer = class {
203
275
  for (const binding of scopes.bindings.values()) {
204
276
  if (binding.declarationNode) this.bindingByDeclaration.set(binding.declarationNode, binding);
205
277
  }
278
+ const visit = (value) => {
279
+ if (!value || typeof value !== "object") return;
280
+ if (Array.isArray(value)) return void value.forEach(visit);
281
+ const node = value;
282
+ if (node.type === "VariableDeclaration" && node.name?.type === "IdentifierPattern" && node.name.name) {
283
+ let init = node.init;
284
+ while (init?.type === "ParenthesizedExpression") init = init.expression;
285
+ if (init?.type === "ClassExpression") this.classExpressionNames.set(init, node.name.name);
286
+ }
287
+ for (const [key, child] of Object.entries(value)) if (key !== "line" && key !== "column") visit(child);
288
+ };
289
+ visit(source.body);
206
290
  }
207
291
  source;
208
292
  scopes;
@@ -224,6 +308,8 @@ var Lowerer = class {
224
308
  lua51;
225
309
  /** The source line each emitted statement came from. */
226
310
  origins = /* @__PURE__ */ new WeakMap();
311
+ /** The name each class written as a value is known by. */
312
+ classExpressionNames = /* @__PURE__ */ new WeakMap();
227
313
  run() {
228
314
  const body = this.options.module ? this.module(this.options.module) : this.topLevel(this.source.body.statements);
229
315
  const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [this.array(table([{ type: "TableFieldPositional", value: vararg() }]))])] : [];
@@ -277,6 +363,10 @@ var Lowerer = class {
277
363
  }
278
364
  return identifier(local2);
279
365
  }
366
+ /** `table.move`. Lua 5.1 has none, and gets a function that does the same. */
367
+ move() {
368
+ return this.lua51 ? this.helper("move") : member(this.builtin("table"), "move");
369
+ }
280
370
  helper(kind) {
281
371
  let name = this.helpers.get(kind);
282
372
  if (!name) {
@@ -377,7 +467,7 @@ var Lowerer = class {
377
467
  const type = this.options.types?.typeOf.get(argument);
378
468
  return {
379
469
  type,
380
- typeText: type ? (0, import_parser4.formatType)(type) : void 0,
470
+ typeText: type ? (0, import_parser5.formatType)(type) : void 0,
381
471
  source: this.writtenSource(argument),
382
472
  declaration: declaration ? this.writtenSource(declaration) : void 0,
383
473
  spread: argument.type === "SpreadElement"
@@ -399,7 +489,11 @@ var Lowerer = class {
399
489
  /** The language's own lowering, then each library's, in load order. A
400
490
  * runtime is named by its lowering's place here (`loweringRuntime`). */
401
491
  get lowerings() {
402
- return this.allLowerings ??= [languageLowering, ...this.options.lowerings ?? []];
492
+ return this.allLowerings ??= [
493
+ languageLowering,
494
+ ...this.options.noLib ? [] : [standardLowering],
495
+ ...this.options.lowerings ?? []
496
+ ];
403
497
  }
404
498
  allLowerings;
405
499
  /** What `receiver:method(...)` becomes, when Luau alone would not run it.
@@ -413,22 +507,28 @@ var Lowerer = class {
413
507
  * (`console:log`), the last library loaded first. No answer leaves an
414
508
  * ordinary Luau method call: `text:upper()` reaches Lua's own. */
415
509
  loweredMethodCall(node) {
510
+ return this.loweredMethod(node.method.name, this.options.types?.methodSources.get(node), node.object, node.arguments, node.method, node);
511
+ }
512
+ /** `loweredMethodCall`, for a method named `method` of what `object`
513
+ * holds — called with `:` on it, or read off it with `.` and handed its
514
+ * `self` like any other argument. `args` are the arguments after the
515
+ * receiver; a read that is not called has none. */
516
+ loweredMethod(method, source, object2, args2, name, node) {
416
517
  const lowerings = this.lowerings;
417
- const source = this.options.types?.methodSources.get(node);
418
518
  const asked = source?.origin === "language" ? [0] : source?.origin === "library" ? lowerings.flatMap((l, i) => l.library === source.library ? [i] : []) : source?.origin === "program" ? [] : lowerings.map((_, i) => i).slice(1).reverse();
419
519
  if (!asked.some((i) => lowerings[i].plugin.methodCall)) return void 0;
420
- const receiver = this.options.types?.typeOf.get(node.object);
421
- const receiverGlobal = this.globalName(node.object);
422
- const info = this.lazyArguments(node.arguments);
520
+ const receiver = this.options.types?.typeOf.get(object2);
521
+ const receiverGlobal = this.globalName(object2);
522
+ const info = this.lazyArguments(args2);
423
523
  for (const i of asked) {
424
- const plugin2 = lowerings[i].plugin;
425
- if (!plugin2.methodCall) continue;
426
- const answer = plugin2.methodCall({
427
- method: node.method.name,
524
+ const plugin3 = lowerings[i].plugin;
525
+ if (!plugin3.methodCall) continue;
526
+ const answer = plugin3.methodCall({
527
+ method,
428
528
  receiver,
429
529
  receiverGlobal,
430
- argumentCount: node.arguments.length,
431
- at: this.callSite(node.method),
530
+ argumentCount: args2.length,
531
+ at: this.callSite(name),
432
532
  get arguments() {
433
533
  return info();
434
534
  },
@@ -452,9 +552,9 @@ var Lowerer = class {
452
552
  if (name === void 0) return void 0;
453
553
  const info = this.lazyArguments(node.arguments);
454
554
  for (let i = lowerings.length - 1; i >= 0; i--) {
455
- const plugin2 = lowerings[i].plugin;
456
- if (!plugin2.globalCall) continue;
457
- const answer = plugin2.globalCall({
555
+ const plugin3 = lowerings[i].plugin;
556
+ if (!plugin3.globalCall) continue;
557
+ const answer = plugin3.globalCall({
458
558
  name,
459
559
  at: this.callSite(node.callee),
460
560
  get arguments() {
@@ -506,6 +606,31 @@ var Lowerer = class {
506
606
  const out2 = [];
507
607
  const runtimes = this.runtimeDefinitions();
508
608
  if (this.helpers.has("concat")) this.helper("array");
609
+ if (this.helpers.has("concat")) this.move();
610
+ const move = this.helpers.get("move");
611
+ if (move) {
612
+ out2.push(...(0, import_luau_parser.parse)(`
613
+ local function ${move}(from, first, last, at, to)
614
+ for i = first, last do
615
+ to[at + i - first] = from[i]
616
+ end
617
+ return to
618
+ end`).body.statements);
619
+ }
620
+ const each = this.helpers.get("each");
621
+ if (each) {
622
+ out2.push(...(0, import_luau_parser.parse)(`
623
+ local function ${each}(source, ...)
624
+ if ${this.builtin("type").name}(source) == "function" then
625
+ return source, ...
626
+ end
627
+ local meta = ${this.builtin("getmetatable").name}(source)
628
+ if meta ~= nil and meta.__iter ~= nil then
629
+ return meta.__iter(source)
630
+ end
631
+ return ${this.builtin("next").name}, source
632
+ end`).body.statements);
633
+ }
509
634
  const marker = this.helpers.get("array");
510
635
  const typeOf = this.helpers.get("typeof");
511
636
  if (marker || typeOf) {
@@ -544,19 +669,72 @@ local function ${typeOf}(value, known)
544
669
  end`).body.statements);
545
670
  }
546
671
  }
672
+ const instanceOf = this.helpers.get("instanceof");
673
+ if (instanceOf) {
674
+ out2.push(...(0, import_luau_parser.parse)(`
675
+ local function ${instanceOf}(value, class)
676
+ if ${this.builtin("type").name}(value) ~= "table" then
677
+ return false
678
+ end
679
+ local current = value.ClassObject
680
+ while current ~= nil do
681
+ if current == class then
682
+ return true
683
+ end
684
+ current = current.ParentClass
685
+ end
686
+ return false
687
+ end`).body.statements);
688
+ }
689
+ const accessors = this.helpers.get("accessors");
690
+ if (accessors) {
691
+ out2.push(...(0, import_luau_parser.parse)(`
692
+ local function ${accessors}(target, getters, setters)
693
+ return ${this.builtin("setmetatable").name}(target, {
694
+ -- Read by a spread, which copies what the getters give.
695
+ __tiluaGetters = getters,
696
+ __index = function(self, key)
697
+ local get = getters[key]
698
+ if get ~= nil then
699
+ return get(self)
700
+ end
701
+ return nil
702
+ end,
703
+ __newindex = function(self, key, value)
704
+ local set = setters[key]
705
+ if set ~= nil then
706
+ set(self, value)
707
+ return
708
+ end
709
+ if getters[key] ~= nil then
710
+ ${this.builtin("error").name}("Cannot assign to '" .. ${this.builtin("tostring").name}(key) .. "': it has a getter and no setter", 2)
711
+ end
712
+ ${this.builtin("rawset").name}(self, key, value)
713
+ end,
714
+ })
715
+ end`).body.statements);
716
+ }
547
717
  const assign2 = this.helpers.get("assign");
548
718
  if (assign2) {
549
- out2.push(localFunction(assign2, functionBody(["target"], [
550
- numericFor("i", number(1), selectCount(this.builtin("select")), [
551
- local(["source"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
552
- ifThen(binary("~=", identifier("source"), nil()), [
553
- genericFor(["key", "value"], [call(this.builtin("pairs"), [identifier("source")])], [
554
- assign([index(identifier("target"), identifier("key"))], [identifier("value")])
555
- ])
556
- ])
557
- ]),
558
- returns([identifier("target")])
559
- ], true)));
719
+ out2.push(...(0, import_luau_parser.parse)(`
720
+ local function ${assign2}(target, ...)
721
+ for i = 1, ${this.builtin("select").name}("#", ...) do
722
+ local source = (${this.builtin("select").name}(i, ...))
723
+ if source ~= nil then
724
+ for key, value in ${this.builtin("pairs").name}(source) do
725
+ target[key] = value
726
+ end
727
+ local meta = ${this.builtin("type").name}(source) == "table" and ${this.builtin("getmetatable").name}(source) or nil
728
+ local getters = ${this.builtin("type").name}(meta) == "table" and ${this.builtin("rawget").name}(meta, "__tiluaGetters") or nil
729
+ if getters ~= nil then
730
+ for key, get in ${this.builtin("pairs").name}(getters) do
731
+ target[key] = get(source)
732
+ end
733
+ end
734
+ end
735
+ end
736
+ return target
737
+ end`).body.statements);
560
738
  }
561
739
  out2.push(...runtimes);
562
740
  const concat = this.helpers.get("concat");
@@ -565,7 +743,7 @@ end`).body.statements);
565
743
  local(["result"], [this.array(table([]))]),
566
744
  numericFor("i", number(1), selectCount(this.builtin("select")), [
567
745
  local(["part"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
568
- callStatement(call(member(this.builtin("table"), "move"), [
746
+ callStatement(call(this.move(), [
569
747
  identifier("part"),
570
748
  number(1),
571
749
  unary("#", identifier("part")),
@@ -582,8 +760,8 @@ end`).body.statements);
582
760
  runtimeDefinitions() {
583
761
  const out2 = [];
584
762
  for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
585
- const { plugin: plugin2, from } = this.lowerings[index2];
586
- const source = plugin2.runtime?.[runtime2];
763
+ const { plugin: plugin3, from } = this.lowerings[index2];
764
+ const source = plugin3.runtime?.[runtime2];
587
765
  if (source === void 0) {
588
766
  this.diagnostics.push({
589
767
  message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
@@ -616,7 +794,7 @@ end`).body.statements);
616
794
  const requires = [];
617
795
  const moduleLocals = /* @__PURE__ */ new Map();
618
796
  const importedFrom = /* @__PURE__ */ new Map();
619
- const resolve3 = (source) => {
797
+ const resolve4 = (source) => {
620
798
  const key = context.resolve(source.value);
621
799
  if (key === void 0) this.report(source, `Cannot find module '${source.value}'`);
622
800
  return key;
@@ -655,7 +833,7 @@ end`).body.statements);
655
833
  ];
656
834
  const used = specifiers.filter((s) => (this.bindingByDeclaration.get(s.local)?.references.length ?? 0) > 0);
657
835
  if (!used.length) continue;
658
- const key = resolve3(statement.source);
836
+ const key = resolve4(statement.source);
659
837
  if (key === void 0) continue;
660
838
  const local2 = moduleLocal(key);
661
839
  for (const s of used) {
@@ -678,8 +856,9 @@ end`).body.statements);
678
856
  names.push("default");
679
857
  break;
680
858
  case "ExportNamedStatement": {
859
+ if (statement.typeOnly) break;
681
860
  if (statement.source) {
682
- const key = resolve3(statement.source);
861
+ const key = resolve4(statement.source);
683
862
  if (key === void 0) break;
684
863
  load(key);
685
864
  for (const s of statement.specifiers) links.push({ name: s.exported.name, module: key, imported: s.local.name });
@@ -695,7 +874,7 @@ end`).body.statements);
695
874
  break;
696
875
  }
697
876
  case "ExportAllStatement": {
698
- const key = resolve3(statement.source);
877
+ const key = resolve4(statement.source);
699
878
  if (key === void 0) break;
700
879
  load(key);
701
880
  stars.push(key);
@@ -761,6 +940,26 @@ end`).body.statements);
761
940
  for (let i = 0; i < locals.length; i += 100) declarations.push(local(locals.slice(i, i + 100), []));
762
941
  return [...declarations, ...hoisted, ...requires, ...body];
763
942
  }
943
+ /** `import("./x")`: the bundle's `import`, called with the module's name.
944
+ * It answers the module's exports table — what `import * as` names. */
945
+ importExpression(node) {
946
+ const context = this.options.module;
947
+ if (!context) {
948
+ this.report(node, "import() needs a bundle: build the project with @tilua/compiler");
949
+ return nil();
950
+ }
951
+ const source = node.source;
952
+ if (source.type !== "StringLiteral") {
953
+ this.report(source, "import() takes a string literal: which module it loads is decided when the project is built");
954
+ return nil();
955
+ }
956
+ const key = context.resolve(source.value, true);
957
+ if (key === void 0) {
958
+ this.report(source, `Cannot find module '${source.value}'`);
959
+ return nil();
960
+ }
961
+ return call(context.import ?? context.require, [string(key)]);
962
+ }
764
963
  isRewritten(node) {
765
964
  const binding = this.bindingByDeclaration.get(node);
766
965
  return binding !== void 0 && this.rewrites.has(binding.id);
@@ -907,7 +1106,7 @@ end`).body.statements);
907
1106
  case "VariableDeclaration":
908
1107
  return this.variableDeclaration(node, "declare");
909
1108
  case "FunctionDeclaration":
910
- return [{ ...localFunction(this.name(node.name.name), this.functionBody(node.func)), attributes: node.attributes }];
1109
+ return [{ ...localFunction(this.name(node.name.name), this.functionBody(node.func)), attributes: this.lua51 ? void 0 : node.attributes }];
911
1110
  case "FunctionDeclarationStatement":
912
1111
  return [this.functionDeclarationStatement(node)];
913
1112
  case "ClassDeclaration":
@@ -938,8 +1137,13 @@ end`).body.statements);
938
1137
  // kept — Lua has no expression statement — and it cannot run
939
1138
  // anything, since a call is a `CallStatement` instead, so dropping
940
1139
  // it changes nothing but the noise.
941
- case "ExpressionStatement":
942
- return [];
1140
+ case "ExpressionStatement": {
1141
+ let expression = node.expression;
1142
+ while (expression.type === "ParenthesizedExpression") expression = expression.expression;
1143
+ if (expression.type !== "ImportExpression") return [];
1144
+ const call2 = this.importExpression(expression);
1145
+ return call2.type === "CallExpression" ? [this.from(callStatement(call2), node)] : [];
1146
+ }
943
1147
  case "DoStatement":
944
1148
  return [{ type: "DoStatement", body: this.block(node.body), ...spanOf(node) }];
945
1149
  case "WhileStatement":
@@ -991,7 +1195,11 @@ end`).body.statements);
991
1195
  let source = this.expression(node.iterator);
992
1196
  if (form.viaIter) source = methodCall(source, "__iter", []);
993
1197
  const variables = form.walks === "values" ? [this.names.fresh("_"), item] : [item];
994
- const iterators = form.walks === "iteration" ? [call(this.builtin("unpack"), [source, number(1), number(3)])] : [source];
1198
+ let iterators = form.walks === "iteration" ? [call(this.builtin("unpack"), [source, number(1), number(3)])] : [source];
1199
+ if (this.lua51 && form.walks === "values") {
1200
+ const kind = form.viaIter ? void 0 : this.options.types?.typeOf.get(node.iterator)?.kind;
1201
+ iterators = [call(kind === "array" || kind === "tuple" ? this.builtin("ipairs") : this.helper("each"), [source])];
1202
+ }
995
1203
  return [genericFor(
996
1204
  variables,
997
1205
  iterators,
@@ -1071,11 +1279,12 @@ end`).body.statements);
1071
1279
  const { build, accessors } = this.classHelpers;
1072
1280
  const metamethods = `{ ${CLASS_METAMETHODS.map((name) => `"${name}"`).join(", ")} }`;
1073
1281
  return (0, import_luau_parser.parse)(`
1074
- local function ${build}(base)
1282
+ local function ${build}(base, name)
1075
1283
  local class = { __getters = {}, __setters = {} }
1076
1284
  class.__index = class
1077
1285
  class.ClassObject = class
1078
1286
  class.ParentClass = base
1287
+ class.name = name
1079
1288
  if base ~= nil then
1080
1289
  setmetatable(class, { __index = base })
1081
1290
  setmetatable(class.__getters, { __index = base.__getters })
@@ -1129,7 +1338,11 @@ end
1129
1338
  * table is assigned to `self`, which a module has already declared. */
1130
1339
  classStatements(node, self, declare) {
1131
1340
  const { build, accessors } = this.classRuntime();
1132
- const create = call(identifier(build), [node.superclass ? this.reference(node.superclass) : nil()]);
1341
+ const called = node.name?.name ?? (node.type === "ClassExpression" ? this.classExpressionNames.get(node) : void 0) ?? "";
1342
+ const create = call(identifier(build), [
1343
+ node.superclass ? this.reference(node.superclass) : nil(),
1344
+ string(called)
1345
+ ]);
1133
1346
  const out2 = [declare !== void 0 ? local([declare], [create]) : assign([self], [create])];
1134
1347
  const previous = this.classContext;
1135
1348
  this.classContext = node;
@@ -1244,7 +1457,8 @@ end
1244
1457
  },
1245
1458
  isMethod,
1246
1459
  func,
1247
- attributes: node.attributes,
1460
+ // `@native` and the like are Luau's.
1461
+ attributes: this.lua51 ? void 0 : node.attributes,
1248
1462
  ...spanOf(node)
1249
1463
  };
1250
1464
  }
@@ -1345,7 +1559,7 @@ end
1345
1559
  if (element) bind(element.value, index(source, number(i + 1)), element.default);
1346
1560
  });
1347
1561
  if (pattern.rest) {
1348
- const elements = call(member(this.builtin("table"), "move"), [
1562
+ const elements = call(this.move(), [
1349
1563
  source,
1350
1564
  number(pattern.elements.length + 1),
1351
1565
  unary("#", source),
@@ -1456,8 +1670,15 @@ end
1456
1670
  case "ArrayExpression":
1457
1671
  return this.arrayExpression(node);
1458
1672
  case "BinaryExpression": {
1673
+ if (node.operator === "??") return this.nullish(node);
1459
1674
  const left = this.expression(node.left);
1460
1675
  const right = this.expression(node.right);
1676
+ if (node.operator === "in") {
1677
+ return parenthesized(binary("~=", index(this.keptToOne(right), this.keptToOne(left)), nil()));
1678
+ }
1679
+ if (node.operator === "instanceof") {
1680
+ return call(this.helper("instanceof"), [this.keptToOne(left), this.keptToOne(right)]);
1681
+ }
1461
1682
  if (this.lua51 && node.operator === "//") return this.floorDivide(left, right);
1462
1683
  return binary(node.operator, left, right);
1463
1684
  }
@@ -1484,6 +1705,8 @@ end
1484
1705
  }
1485
1706
  case "ParenthesizedExpression":
1486
1707
  return parenthesized(this.expression(node.expression));
1708
+ case "ImportExpression":
1709
+ return this.importExpression(node);
1487
1710
  // Types only: the value is the expression itself.
1488
1711
  case "TypeAssertionExpression":
1489
1712
  case "SatisfiesExpression":
@@ -1537,7 +1760,7 @@ end
1537
1760
  const last2 = list.length - 1;
1538
1761
  return lowered.length && !many.has(list[last2]) ? [...lowered.slice(0, -1), this.keptToOne(lowered[last2])] : lowered;
1539
1762
  }
1540
- const unpack = (value) => call(member(this.builtin("table"), "unpack"), [value]);
1763
+ const unpack = (value) => call(this.lua51 ? this.builtin("unpack") : member(this.builtin("table"), "unpack"), [value]);
1541
1764
  const last = list[list.length - 1];
1542
1765
  if (spreads.length === 1 && last.type === "SpreadElement") {
1543
1766
  return [...list.slice(0, -1).map((a) => this.expression(a)), unpack(this.expression(last.argument))];
@@ -1551,11 +1774,21 @@ end
1551
1774
  /** One link of an access chain, read from `object`. */
1552
1775
  link(node, object2) {
1553
1776
  switch (node.type) {
1554
- case "MemberExpression":
1777
+ case "MemberExpression": {
1778
+ if (this.options.types?.lengths.has(node)) return unary("#", object2);
1779
+ const source = this.options.types?.methodReads.get(node);
1780
+ const lowered = source && this.loweredMethod(node.property.name, source, node.object, [], node.property, node);
1781
+ if (lowered) {
1782
+ const forwarded = call(calleePath(lowered.callee), [...lowered.prepend, vararg()]);
1783
+ return functionExpression(functionBody(lowered.passReceiver ? [] : ["_"], [returns([forwarded])], true));
1784
+ }
1555
1785
  return member(object2, node.property.name);
1786
+ }
1556
1787
  case "IndexExpression":
1557
1788
  return index(object2, this.expression(node.index));
1558
1789
  case "CallExpression": {
1790
+ const method = this.methodCalled(node);
1791
+ if (method) return method;
1559
1792
  const lowered = this.loweredGlobalCall(node);
1560
1793
  const args2 = this.arguments(node.arguments);
1561
1794
  if (!lowered) return call(object2, args2);
@@ -1578,6 +1811,24 @@ end
1578
1811
  }
1579
1812
  }
1580
1813
  }
1814
+ /** `names.filter(names, f)`: a method of the value's metatable, read with
1815
+ * `.` and handed its `self` as the first argument. Written as a call of
1816
+ * the function the method really is — `array.filter(names, f)` — where
1817
+ * whoever declared the metatable says so; `undefined` leaves an ordinary
1818
+ * call, which is right for a string's Lua methods (`text.upper(text)`
1819
+ * reaches Lua's own `string.upper`). */
1820
+ methodCalled(node) {
1821
+ const callee = node.callee;
1822
+ if (callee.type !== "MemberExpression") return void 0;
1823
+ const source = this.options.types?.methodReads.get(callee);
1824
+ if (!source) return void 0;
1825
+ const spreads = node.arguments.map((a, i) => a.type === "SpreadElement" ? i : -1).filter((i) => i >= 0);
1826
+ if (!node.arguments.length || spreads.includes(0) || spreads.some((i) => i !== node.arguments.length - 1)) return void 0;
1827
+ const lowered = this.loweredMethod(callee.property.name, source, callee.object, node.arguments.slice(1), callee.property, node);
1828
+ if (!lowered) return void 0;
1829
+ const [self, ...rest] = this.arguments(node.arguments);
1830
+ return call(calleePath(lowered.callee), [...lowered.prepend, ...lowered.passReceiver ? [self] : [], ...rest]);
1831
+ }
1581
1832
  // --------------------------------------------------------
1582
1833
  // Optional chains
1583
1834
  // --------------------------------------------------------
@@ -1623,7 +1874,7 @@ end
1623
1874
  const segments = this.segments(chain);
1624
1875
  const base = this.expression(chain.base);
1625
1876
  const last = chain.links[chain.links.length - 1];
1626
- if (base.type === "Identifier" && segments.length === 1 && last.type !== "CallExpression" && last.type !== "MethodCallExpression") {
1877
+ if (!this.lua51 && base.type === "Identifier" && segments.length === 1 && last.type !== "CallExpression" && last.type !== "MethodCallExpression") {
1627
1878
  return {
1628
1879
  type: "IfElseExpression",
1629
1880
  clauses: [{ condition: binary("==", base, nil()), body: nil() }],
@@ -1644,6 +1895,32 @@ end
1644
1895
  const func = functionExpression(functionBody([], statements, passesVarargs));
1645
1896
  return call(parenthesized(func), passesVarargs ? [vararg()] : []);
1646
1897
  }
1898
+ /** `a ?? b`: `a`, or `b` where `a` is nil — read only then. A `false`
1899
+ * stays, which is what sets it apart from `a or b`. A name is tested
1900
+ * in place; anything else is read once, into a local. */
1901
+ nullish(node) {
1902
+ const left = this.keptToOne(this.expression(node.left));
1903
+ const right = this.keptToOne(this.expression(node.right));
1904
+ if (!this.lua51 && left.type === "Identifier") {
1905
+ return {
1906
+ type: "IfElseExpression",
1907
+ clauses: [{ condition: binary("~=", left, nil()), body: left }],
1908
+ alternate: right,
1909
+ line: left.line,
1910
+ column: left.column
1911
+ };
1912
+ }
1913
+ const ref = this.names.fresh("value");
1914
+ const at = identifier(ref);
1915
+ const statements = [
1916
+ local([ref], [left]),
1917
+ ifThen(binary("~=", at, nil()), [returns([at])]),
1918
+ returns([right])
1919
+ ];
1920
+ const passesVarargs = statements.some(usesVararg);
1921
+ const func = functionExpression(functionBody([], statements, passesVarargs));
1922
+ return call(parenthesized(func), passesVarargs ? [vararg()] : []);
1923
+ }
1647
1924
  /** `` `${a} any` `` -> `("%s any"):format(tostring(a))` */
1648
1925
  interpolatedString(node) {
1649
1926
  let format = "";
@@ -1725,12 +2002,12 @@ end
1725
2002
  }));
1726
2003
  const alternate = this.expression(node.alternate);
1727
2004
  if (clauses.every((c) => alwaysTruthy(c.body))) {
1728
- let result = alternate;
2005
+ let result2 = alternate;
1729
2006
  for (let i = clauses.length - 1; i >= 0; i--) {
1730
2007
  const { condition, body } = clauses[i];
1731
- result = binary("or", binary("and", condition, body), result);
2008
+ result2 = binary("or", binary("and", condition, body), result2);
1732
2009
  }
1733
- return parenthesized(result);
2010
+ return parenthesized(result2);
1734
2011
  }
1735
2012
  const statement = {
1736
2013
  type: "IfStatement",
@@ -1787,11 +2064,20 @@ end
1787
2064
  current = [];
1788
2065
  parts.push(this.expression(f.argument));
1789
2066
  break;
2067
+ case "TableFieldAccessor":
2068
+ break;
1790
2069
  }
1791
2070
  }
1792
- if (parts.length === 0) return table(current);
1793
- if (current.length) parts.push(table(current));
1794
- return call(this.helper("assign"), [table([]), ...parts]);
2071
+ let made;
2072
+ if (parts.length === 0) made = table(current);
2073
+ else {
2074
+ if (current.length) parts.push(table(current));
2075
+ made = call(this.helper("assign"), [table([]), ...parts]);
2076
+ }
2077
+ const accessors = node.fields.filter((f) => f.type === "TableFieldAccessor");
2078
+ if (!accessors.length) return made;
2079
+ const table2 = (kind) => table(accessors.filter((f) => f.kind === kind).map((f) => field(f.key.type === "Identifier" ? f.key.name : f.key.value, functionExpression(this.classFunctionBody(f.func)))));
2080
+ return call(this.helper("accessors"), [made, table2("get"), table2("set")]);
1795
2081
  }
1796
2082
  /** `[1, ...xs, 2]`. Without a spread it is a Luau sequence; with one, the
1797
2083
  * runs of plain elements and the spread arrays are joined by `concat`. */
@@ -2048,7 +2334,7 @@ var CLASS_METAMETHODS = [
2048
2334
  ];
2049
2335
 
2050
2336
  // src/linemap.ts
2051
- var import_parser5 = require("@tilua/parser");
2337
+ var import_parser6 = require("@tilua/parser");
2052
2338
  function statementsOf(node) {
2053
2339
  const out2 = [];
2054
2340
  const visit = (value) => {
@@ -2081,7 +2367,7 @@ function buildLineMap(emitted, printed, origins) {
2081
2367
  }
2082
2368
  function lineMapSource(map) {
2083
2369
  const entries = [...map.entries()].sort((a, b) => a[0] - b[0]);
2084
- const quoted = (s) => (0, import_parser5.luauString)(s.replace(/\\/g, "/"));
2370
+ const quoted = (s) => (0, import_parser6.luauString)(s.replace(/\\/g, "/"));
2085
2371
  return `{${entries.map(([line, o]) => `[${line}]={${quoted(o.file)},${o.line}}`).join(",")}}`;
2086
2372
  }
2087
2373
 
@@ -2093,15 +2379,15 @@ async function loadLowerings(modules) {
2093
2379
  for (const module2 of modules) {
2094
2380
  try {
2095
2381
  const loaded = await import((0, import_node_url.pathToFileURL)(module2.file).href);
2096
- const plugin2 = loaded.default;
2097
- if (!plugin2 || typeof plugin2 !== "object") {
2382
+ const plugin3 = loaded.default;
2383
+ if (!plugin3 || typeof plugin3 !== "object") {
2098
2384
  problems.push({
2099
2385
  file: module2.file,
2100
2386
  message: `'${module2.from}' lowering module has no default export`
2101
2387
  });
2102
2388
  continue;
2103
2389
  }
2104
- lowerings.push({ plugin: plugin2, from: module2.from, library: module2.library });
2390
+ lowerings.push({ plugin: plugin3, from: module2.from, library: module2.library });
2105
2391
  } catch (error) {
2106
2392
  problems.push({
2107
2393
  file: module2.file,
@@ -2128,16 +2414,17 @@ async function bundle(options) {
2128
2414
  const file = queue.shift();
2129
2415
  if (sources.has(file)) continue;
2130
2416
  const parsed = parseFile(file, diagnostics, directives);
2131
- if (!parsed) return { modules: [], diagnostics };
2132
- const { program: program2, text } = parsed;
2133
- const scopes = (0, import_parser6.analyzeScopes)(program2);
2417
+ if (!parsed) return { files: [], modules: [], diagnostics };
2418
+ const { program: program3, text } = parsed;
2419
+ const scopes = (0, import_parser7.analyzeScopes)(program3, { noLib: config2?.noLib });
2134
2420
  for (const d of scopes.diagnostics) {
2135
2421
  diagnostics.push({ file, message: d.message, line: d.node.line.start, column: d.node.column.start, category: "scope" });
2136
2422
  }
2137
- sources.set(file, { file, name: nameOf(file), text, program: program2, scopes });
2138
- for (const specifier of importedSpecifiers(program2)) {
2139
- const target = (0, import_parser6.resolveModulePath)(file, specifier, config2);
2140
- if (target && !target.endsWith(".d.tilua")) queue.push(target);
2423
+ sources.set(file, { file, name: nameOf(file), text, program: program3, scopes });
2424
+ const dynamic = (0, import_parser7.importExpressions)(program3).flatMap((node) => node.source.type === "StringLiteral" ? [node.source.value] : []);
2425
+ for (const specifier of [...importedSpecifiers(program3), ...dynamic]) {
2426
+ const target2 = (0, import_parser7.resolveModulePath)(file, specifier, config2);
2427
+ if (target2 && !target2.endsWith(".d.tilua")) queue.push(target2);
2141
2428
  }
2142
2429
  }
2143
2430
  const analysis = analyzeModules([...sources.values()], config2);
@@ -2149,6 +2436,9 @@ async function bundle(options) {
2149
2436
  const names = Names.from(...[...sources.values()].map((s) => s.program));
2150
2437
  const G = names.fresh("G");
2151
2438
  const requireExpression = member(identifier(G), "require");
2439
+ const importExpression = member(identifier(G), "import");
2440
+ const mode = options.output ?? config2?.output ?? "single";
2441
+ const target = options.target ?? config2?.target ?? "luau";
2152
2442
  const modules = /* @__PURE__ */ new Map();
2153
2443
  const origins = /* @__PURE__ */ new WeakMap();
2154
2444
  const pending = [entry2];
@@ -2157,104 +2447,228 @@ async function bundle(options) {
2157
2447
  if (modules.has(file)) continue;
2158
2448
  const source = sources.get(file);
2159
2449
  const shown = (0, import_node_path2.relative)(root, file).replace(/\\/g, "/");
2160
- const lowered = lower(source.program, source.scopes, {
2450
+ const imports = /* @__PURE__ */ new Set();
2451
+ const dynamicImports = /* @__PURE__ */ new Set();
2452
+ const lowered2 = lower(source.program, source.scopes, {
2161
2453
  names,
2162
2454
  types: analysis.types.get(file),
2163
2455
  lowerings,
2164
- target: options.target ?? config2?.target,
2456
+ target,
2457
+ noLib: config2?.noLib,
2165
2458
  source: source.text,
2166
2459
  file: shown,
2167
2460
  module: {
2168
2461
  name: source.name,
2169
2462
  require: requireExpression,
2170
- lines: `${G}.lines`,
2463
+ import: importExpression,
2464
+ // One file has one line map. Several have one each, and a
2465
+ // module reads the one of the file it is in.
2466
+ lines: mode === "single" ? `${G}.lines` : `${G}.maps[${(0, import_parser7.luauString)(source.name)}]`,
2171
2467
  fail: `${G}.fail`,
2172
2468
  arrays: `${G}.arrays`,
2173
- resolve: (specifier) => {
2174
- const target = (0, import_parser6.resolveModulePath)(file, specifier, config2);
2175
- if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
2176
- pending.push(target);
2177
- return sources.get(target).name;
2469
+ resolve: (specifier, dynamic) => {
2470
+ const found = (0, import_parser7.resolveModulePath)(file, specifier, config2);
2471
+ if (!found || found.endsWith(".d.tilua") || !sources.has(found)) return void 0;
2472
+ pending.push(found);
2473
+ const name = sources.get(found).name;
2474
+ (dynamic ? dynamicImports : imports).add(name);
2475
+ return name;
2178
2476
  }
2179
2477
  }
2180
2478
  });
2181
- for (const d of lowered.diagnostics) diagnostics.push({ file, ...d, category: "module" });
2182
- for (const statement of allStatements(lowered.statements)) {
2183
- const line = lowered.origins.get(statement);
2479
+ for (const d of lowered2.diagnostics) diagnostics.push({ file, ...d, category: "module" });
2480
+ for (const statement of allStatements(lowered2.statements)) {
2481
+ const line = lowered2.origins.get(statement);
2184
2482
  if (line !== void 0) origins.set(statement, { file: shown, line });
2185
2483
  }
2186
2484
  modules.set(file, {
2187
2485
  name: source.name,
2188
- statements: lowered.statements,
2189
- exportsName: lowered.exportsName,
2190
- info: lowered.module,
2191
- usesScriptArgs: lowered.usesScriptArgs
2486
+ statements: lowered2.statements,
2487
+ exportsName: lowered2.exportsName,
2488
+ info: lowered2.module,
2489
+ usesScriptArgs: lowered2.usesScriptArgs,
2490
+ imports,
2491
+ dynamicImports
2192
2492
  });
2193
2493
  }
2194
2494
  for (const [file, fileDirectives] of directives) {
2195
2495
  const semantic = diagnostics.filter((d) => d.file === file && (d.category === "scope" || d.category === "type"));
2196
- const { kept, unusedExpectErrors } = (0, import_parser6.applyDirectives)(fileDirectives, semantic, (d) => d.line);
2496
+ const { kept, unusedExpectErrors } = (0, import_parser7.applyDirectives)(fileDirectives, semantic, (d) => d.line);
2197
2497
  const suppressed = new Set(semantic.filter((d) => !kept.includes(d)));
2198
2498
  const remaining = diagnostics.filter((d) => !suppressed.has(d));
2199
2499
  diagnostics.length = 0;
2200
2500
  diagnostics.push(...remaining);
2201
2501
  if (options.typeCheck === false) continue;
2202
2502
  for (const d of unusedExpectErrors) {
2203
- diagnostics.push({ file, message: import_parser6.UNUSED_EXPECT_ERROR, line: d.line, column: d.column, category: "type" });
2503
+ diagnostics.push({ file, message: import_parser7.UNUSED_EXPECT_ERROR, line: d.line, column: d.column, category: "type" });
2204
2504
  }
2205
2505
  }
2206
2506
  const moduleProblems = new Set(diagnostics.filter((d) => d.category === "module").map((d) => `${d.file}:${d.line}:${d.column}:${d.message}`));
2207
2507
  const reported = diagnostics.filter((d) => d.category !== "type" || !moduleProblems.has(`${d.file}:${d.line}:${d.column}:${d.message}`));
2208
2508
  diagnostics.length = 0;
2209
2509
  diagnostics.push(...reported);
2210
- const moduleNames = [...modules.values()].map((m) => m.name);
2510
+ const lowered = [...modules.values()];
2511
+ const moduleNames = lowered.map((m) => m.name);
2211
2512
  if (diagnostics.some((d) => d.category === "syntax" || d.category === "scope" || d.category === "module")) {
2212
- return { modules: moduleNames, diagnostics };
2213
- }
2214
- const program = (0, import_luau_parser2.parse)(runtime(G));
2215
- const assignment = program.body.statements[1];
2216
- if ([...modules.values()].some((m) => m.usesScriptArgs)) {
2217
- program.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
2218
- program.body.statements.push(...(0, import_luau_parser2.parse)(`${G}.arrays[scriptArgs] = true`).body.statements);
2513
+ return { files: [], modules: moduleNames, diagnostics };
2514
+ }
2515
+ const several = mode !== "single";
2516
+ const layout = layoutOf(mode, modules.get(entry2), lowered, options.outFile);
2517
+ const usesScriptArgs = lowered.some((m) => m.usesScriptArgs);
2518
+ const elsewhere = /* @__PURE__ */ new Map();
2519
+ for (const file of layout.slice(1)) {
2520
+ for (const module2 of file.modules) elsewhere.set(module2.name, loadPath(layout[0].path, file.path, target));
2521
+ }
2522
+ const program2 = (0, import_luau_parser2.parse)(runtime(G, {
2523
+ several,
2524
+ target,
2525
+ files: elsewhere,
2526
+ imports: lowered.some((m) => m.dynamicImports.size > 0)
2527
+ }));
2528
+ const assignment = program2.body.statements[1];
2529
+ if (usesScriptArgs) {
2530
+ program2.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
2531
+ program2.body.statements.push(...(0, import_luau_parser2.parse)(`${G}.arrays[scriptArgs] = true`).body.statements);
2532
+ if (several) program2.body.statements.push(...(0, import_luau_parser2.parse)(`${G}.scriptArgs = scriptArgs`).body.statements);
2219
2533
  }
2220
2534
  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;
2221
2535
  if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
2222
- for (const module2 of modules.values()) {
2223
- const { names: exported, links, stars } = module2.info;
2224
- const fields = [];
2225
- if (exported.length) fields.push(field("names", table(exported.map((n) => field(n, boolean(true))))));
2226
- if (links.length) {
2227
- fields.push(field("links", table(links.map((link) => field(link.name, table([
2228
- { type: "TableFieldPositional", value: string(link.module) },
2229
- ...link.imported === void 0 ? [] : [{ type: "TableFieldPositional", value: string(link.imported) }]
2230
- ]))))));
2231
- }
2232
- if (stars.length) fields.push(field("stars", table(stars.map((star) => ({ type: "TableFieldPositional", value: string(star) })))));
2233
- fields.push(field("load", functionExpression(functionBody([module2.exportsName], module2.statements, true))));
2234
- modulesTable.fields.push({ type: "TableFieldComputed", key: string(module2.name), value: table(fields) });
2536
+ for (const module2 of layout[0].modules) {
2537
+ modulesTable.fields.push({ type: "TableFieldComputed", key: string(module2.name), value: moduleTable(module2) });
2235
2538
  }
2236
2539
  const ok = names.fresh("ok");
2237
- const result = names.fresh("result");
2540
+ const result2 = names.fresh("result");
2238
2541
  const entryStatements = (0, import_luau_parser2.parse)(`
2239
- local ${ok}, ${result} = xpcall(function()
2240
- return ${G}.require(${(0, import_parser6.luauString)(sources.get(entry2).name)})
2542
+ local ${ok}, ${result2} = xpcall(function()
2543
+ return ${G}.require(${(0, import_parser7.luauString)(sources.get(entry2).name)})
2241
2544
  end, ${G}.fail)
2242
2545
  if not ${ok} then
2243
- error(${result}, 0)
2546
+ error(${result2}, 0)
2244
2547
  end
2245
- ${modules.get(entry2).info.exports ? `return ${result}` : ""}
2548
+ ${modules.get(entry2).info.exports ? `return ${result2}` : ""}
2246
2549
  `).body.statements;
2247
- program.body.statements.push(...entryStatements);
2248
- const map = buildLineMap(program, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(program)), origins);
2249
- if (map.size) {
2250
- program.body.statements.splice(program.body.statements.length - entryStatements.length, 0, assign(
2550
+ program2.body.statements.push(...entryStatements);
2551
+ const map = buildLineMap(program2, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(program2)), origins);
2552
+ if (map.size || several) {
2553
+ const mapStatements = [assign(
2251
2554
  [member(identifier(G), "lines")],
2252
2555
  [(0, import_luau_parser2.parseExpressionFromSource)(lineMapSource(map))]
2253
- ));
2556
+ )];
2557
+ if (several) mapStatements.push(...mapRegistration(G, layout[0], `${G}.lines`));
2558
+ program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...mapStatements);
2254
2559
  }
2255
- return { code: (0, import_luau_parser2.print)(program) + "\n", modules: moduleNames, diagnostics };
2560
+ const files = [{ path: layout[0].path, code: (0, import_luau_parser2.print)(program2) + "\n", modules: layout[0].modules.map((m) => m.name) }];
2561
+ for (const file of layout.slice(1)) {
2562
+ const body = [];
2563
+ if (file.modules.some((m) => m.usesScriptArgs)) body.push(...(0, import_luau_parser2.parse)(`local scriptArgs = ${G}.scriptArgs`).body.statements);
2564
+ for (const module2 of file.modules) {
2565
+ body.push(assign(
2566
+ [index(member(identifier(G), "modules"), string(module2.name))],
2567
+ [moduleTable(module2)]
2568
+ ));
2569
+ }
2570
+ const chunk = program([returns([functionExpression(functionBody([G], body))])]);
2571
+ const lines = buildLineMap(chunk, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(chunk)), origins);
2572
+ body.push(...mapRegistration(G, file, lineMapSource(lines)));
2573
+ files.push({ path: file.path, code: (0, import_luau_parser2.print)(chunk) + "\n", modules: file.modules.map((m) => m.name) });
2574
+ }
2575
+ return { code: files[0].code, files, modules: moduleNames, diagnostics };
2576
+ }
2577
+ function moduleTable(module2) {
2578
+ const { names: exported, links, stars } = module2.info;
2579
+ const fields = [];
2580
+ if (exported.length) fields.push(field("names", table(exported.map((n) => field(n, boolean(true))))));
2581
+ if (links.length) {
2582
+ fields.push(field("links", table(links.map((link) => field(link.name, table([
2583
+ { type: "TableFieldPositional", value: string(link.module) },
2584
+ ...link.imported === void 0 ? [] : [{ type: "TableFieldPositional", value: string(link.imported) }]
2585
+ ]))))));
2586
+ }
2587
+ if (stars.length) fields.push(field("stars", table(stars.map((star) => ({ type: "TableFieldPositional", value: string(star) })))));
2588
+ fields.push(field("load", functionExpression(functionBody([module2.exportsName], module2.statements, true))));
2589
+ return table(fields);
2590
+ }
2591
+ function layoutOf(mode, entry2, modules, outFile) {
2592
+ const extension = outFile !== void 0 && (0, import_node_path2.extname)(outFile) || ".luau";
2593
+ const entryStem = outFile !== void 0 ? (0, import_node_path2.basename)(outFile, (0, import_node_path2.extname)(outFile)) : import_node_path2.posix.basename(entry2.name);
2594
+ const others = modules.filter((m) => m !== entry2);
2595
+ if (mode === "single") return [{ path: `${entryStem}${extension}`, modules: [entry2, ...others] }];
2596
+ if (mode === "modules") {
2597
+ const pathOf = (m) => `${m.name.split("/").map((part) => part === ".." ? "__" : part).join("/")}${extension}`;
2598
+ return [entry2, ...others].map((m) => ({ path: pathOf(m), modules: [m] }));
2599
+ }
2600
+ const byName = new Map(modules.map((m) => [m.name, m]));
2601
+ const reach = (root) => {
2602
+ const seen = /* @__PURE__ */ new Set();
2603
+ const stack = [root];
2604
+ while (stack.length) {
2605
+ const name = stack.pop();
2606
+ if (seen.has(name)) continue;
2607
+ seen.add(name);
2608
+ stack.push(...byName.get(name)?.imports ?? []);
2609
+ }
2610
+ return seen;
2611
+ };
2612
+ const inEntry = reach(entry2.name);
2613
+ const roots = [...new Set(modules.flatMap((m) => [...m.dynamicImports]))].filter((root) => !inEntry.has(root));
2614
+ const reached = new Map(roots.map((root) => [root, reach(root)]));
2615
+ const groups = /* @__PURE__ */ new Map();
2616
+ for (const module2 of others) {
2617
+ if (inEntry.has(module2.name)) continue;
2618
+ const by = roots.filter((root) => reached.get(root).has(module2.name));
2619
+ const key = by.join("\n");
2620
+ if (!groups.has(key)) groups.set(key, { roots: by, modules: [] });
2621
+ groups.get(key).modules.push(module2);
2622
+ }
2623
+ const files = [{ path: `${entryStem}${extension}`, modules: modules.filter((m) => inEntry.has(m.name)) }];
2624
+ const taken = /* @__PURE__ */ new Set([entryStem]);
2625
+ for (const group of groups.values()) {
2626
+ const label = group.roots.length === 1 ? group.roots[0].replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "chunk" : "shared";
2627
+ let stem = `${entryStem}_${label}`;
2628
+ for (let n = 2; taken.has(stem); n++) stem = `${entryStem}_${label}_${n}`;
2629
+ taken.add(stem);
2630
+ files.push({ path: `${stem}${extension}`, modules: group.modules });
2631
+ }
2632
+ return files;
2633
+ }
2634
+ function loadPath(entryPath, file, target) {
2635
+ const path = import_node_path2.posix.relative(import_node_path2.posix.dirname(entryPath), file);
2636
+ if (target === "lua51") return path;
2637
+ const bare = path.slice(0, path.length - import_node_path2.posix.extname(path).length);
2638
+ return bare.startsWith("../") ? bare : `./${bare}`;
2639
+ }
2640
+ function mapRegistration(G, file, map) {
2641
+ const extension = import_node_path2.posix.extname(file.path);
2642
+ const parts = file.path.slice(0, file.path.length - extension.length).split(/[/.]/).filter((part) => part !== "");
2643
+ const entry2 = `${G}.sources[#${G}.sources + 1]`;
2644
+ return (0, import_luau_parser2.parse)([
2645
+ `${entry2} = { { ${parts.map(import_parser7.luauString).join(", ")} }, ${map}, ${(0, import_parser7.luauString)(extension.slice(1))} }`,
2646
+ ...file.modules.map((module2) => `${G}.maps[${(0, import_parser7.luauString)(module2.name)}] = ${G}.sources[#${G}.sources][2]`)
2647
+ ].join("\n")).body.statements;
2648
+ }
2649
+ function loader(G, target) {
2650
+ if (target === "lua51") {
2651
+ return `
2652
+ -- From beside this file, wherever that is: its own path says where.
2653
+ local folder = ""
2654
+ if debug ~= nil and debug.getinfo ~= nil then
2655
+ local at = debug.getinfo(${G}.load, "S").source
2656
+ if at:sub(1, 1) == "@" then
2657
+ folder = at:match("^@(.*[/\\\\])") or ""
2658
+ end
2659
+ end
2660
+ local chunk, problem = loadfile(folder .. file)
2661
+ if chunk == nil then
2662
+ error(problem, 0)
2663
+ end
2664
+ chunk()(${G})`;
2665
+ }
2666
+ return `
2667
+ require(file)(${G})`;
2256
2668
  }
2257
- function runtime(G) {
2669
+ function runtime(G, options) {
2670
+ const { several } = options;
2671
+ const files = [...options.files].map(([name, file]) => `[${(0, import_parser7.luauString)(name)}] = ${(0, import_parser7.luauString)(file)}`).join(", ");
2258
2672
  return `
2259
2673
  local ${G}
2260
2674
  ${G} = {
@@ -2302,7 +2716,7 @@ ${G} = {
2302
2716
  return message
2303
2717
  end
2304
2718
  local lines = ${G}.lines
2305
- if lines == nil then
2719
+ if lines == nil ${several ? `and #${G}.sources == 0 ` : ""}then
2306
2720
  return message .. (trace or "")
2307
2721
  end
2308
2722
  -- The bundle's name, as a position in a message spells it: where this
@@ -2322,6 +2736,81 @@ ${G} = {
2322
2736
  chunk = info.short_src
2323
2737
  end
2324
2738
  end
2739
+ ${several ? `
2740
+ -- A position in this file is read by this file's map. One in another
2741
+ -- of the bundle's files is read by that file's: the one whose name
2742
+ -- the position ends with, by the most folders where several do. A
2743
+ -- message says nothing about which file it starts in, so nothing is
2744
+ -- guessed from it here.
2745
+ local prefix = nil
2746
+ if chunk ~= nil then
2747
+ prefix = chunk:gsub("%p", "%%%0")
2748
+ end
2749
+ local function place(line)
2750
+ local origin = lines ~= nil and lines[tonumber(line)] or nil
2751
+ if origin == nil then
2752
+ return nil
2753
+ end
2754
+ return origin[1] .. ":" .. tostring(origin[2])
2755
+ end
2756
+ local function elsewhere(at, line)
2757
+ -- Already a place in the project.
2758
+ if at:sub(-6) == ".tilua" then
2759
+ return nil
2760
+ end
2761
+ local parts = {}
2762
+ for part in at:gmatch("[^/\\\\%.]+") do
2763
+ parts[#parts + 1] = part
2764
+ end
2765
+ local map, most, tied = nil, 0, false
2766
+ for _, source in ipairs(${G}.sources) do
2767
+ local names = source[1]
2768
+ local last = #parts
2769
+ if parts[last] == source[3] then
2770
+ last = last - 1
2771
+ end
2772
+ local same = 0
2773
+ while same < #names and same < last and names[#names - same] == parts[last - same] do
2774
+ same = same + 1
2775
+ end
2776
+ if same > most then
2777
+ map, most, tied = source[2], same, false
2778
+ elseif same == most then
2779
+ tied = true
2780
+ end
2781
+ end
2782
+ -- Two files it could as well be: a wrong place is worse than none.
2783
+ local origin = map ~= nil and not tied and map[tonumber(line)] or nil
2784
+ if origin == nil then
2785
+ return nil
2786
+ end
2787
+ return origin[1] .. ":" .. tostring(origin[2])
2788
+ end
2789
+ local mapped = message
2790
+ if prefix ~= nil then
2791
+ mapped = mapped:gsub(prefix .. ":(%d+)", function(line)
2792
+ return place(line)
2793
+ end)
2794
+ end
2795
+ mapped = mapped:gsub("(%S+):(%d+)", elsewhere)
2796
+ local frames = {}
2797
+ for frame in (trace or ""):gmatch("[^\\n]+") do
2798
+ local at = nil
2799
+ if prefix ~= nil then
2800
+ local line = frame:match(prefix .. ":(%d+)")
2801
+ if line ~= nil then
2802
+ at = place(line)
2803
+ end
2804
+ end
2805
+ if at == nil then
2806
+ for source, line in frame:gmatch("(%S+):(%d+)") do
2807
+ at = elsewhere(source, line)
2808
+ if at ~= nil then
2809
+ break
2810
+ end
2811
+ end
2812
+ end
2813
+ if at ~= nil then` : `
2325
2814
  if chunk == nil then
2326
2815
  chunk = message:match("^(.-):%d+:")
2327
2816
  end
@@ -2343,8 +2832,9 @@ ${G} = {
2343
2832
  for frame in (trace or ""):gmatch("[^\\n]+") do
2344
2833
  local line = frame:match(prefix .. ":(%d+)")
2345
2834
  local at = line ~= nil and place(line) or nil
2346
- if at ~= nil then
2347
- local name = frame:match("function ([%w_.:]+)")
2835
+ if at ~= nil then`}
2836
+ -- Luau writes \`function name\`; Lua 5.1, \`function 'name'\`.
2837
+ local name = frame:match("function ([%w_.:]+)") or frame:match("function '([^']+)'")
2348
2838
  frames[#frames + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
2349
2839
  end
2350
2840
  end
@@ -2353,13 +2843,36 @@ ${G} = {
2353
2843
  end
2354
2844
  return mapped .. "\\n" .. table.concat(frames, "\\n")
2355
2845
  end,
2356
- records = {},
2846
+ records = {},${several ? `
2847
+ -- The file each module outside this one is in, as \`load\` is handed it.
2848
+ files = { ${files} },
2849
+ loaded = {},
2850
+ -- Every file's line map: by the module, and by the file's name.
2851
+ maps = {},
2852
+ sources = {},
2853
+ -- Another of the bundle's files: a function that adds its modules here.
2854
+ load = function(file)
2855
+ if ${G}.loaded[file] then
2856
+ return
2857
+ end
2858
+ ${G}.loaded[file] = true${loader(G, options.target)}
2859
+ end,` : ""}${options.imports ? `
2860
+ -- \`import("./x")\`: the module's exports, asked for where the code runs
2861
+ -- rather than before its module does. \`require\` does the rest \u2014 and
2862
+ -- loads the file the module is in, if it is not this one.
2863
+ import = function(name)
2864
+ return ${G}.require(name)
2865
+ end,` : ""}
2357
2866
  require = function(name)
2358
2867
  local record = ${G}.records[name]
2359
2868
  if record ~= nil then
2360
2869
  return record.exports
2361
2870
  end
2362
- local module = ${G}.modules[name]
2871
+ local module = ${G}.modules[name]${several ? `
2872
+ if module == nil then
2873
+ ${G}.load(${G}.files[name])
2874
+ module = ${G}.modules[name]
2875
+ end` : ""}
2363
2876
  local initialized = {}
2364
2877
  local exports = setmetatable({}, {
2365
2878
  __index = function(_, key)
@@ -2378,7 +2891,7 @@ ${G} = {
2378
2891
  return source[link[2]]
2379
2892
  end
2380
2893
  if module.stars ~= nil and key ~= "default" then
2381
- for _, star in module.stars do
2894
+ for _, star in ipairs(module.stars) do
2382
2895
  local value = ${G}.require(star)[key]
2383
2896
  if value ~= nil then
2384
2897
  return value
@@ -2403,7 +2916,7 @@ ${G} = {
2403
2916
  }
2404
2917
  function stampOf(file) {
2405
2918
  try {
2406
- const at = (0, import_node_fs2.statSync)(file);
2919
+ const at = (0, import_node_fs3.statSync)(file);
2407
2920
  return `${at.size}:${at.mtimeMs}`;
2408
2921
  } catch {
2409
2922
  return void 0;
@@ -2419,19 +2932,19 @@ function parseFile(file, diagnostics, directives) {
2419
2932
  }
2420
2933
  let text;
2421
2934
  try {
2422
- text = (0, import_node_fs2.readFileSync)(file, "utf8");
2935
+ text = (0, import_node_fs3.readFileSync)(file, "utf8");
2423
2936
  } catch {
2424
2937
  diagnostics.push({ file, message: "Cannot read file", line: 1, column: 1, category: "module" });
2425
2938
  return void 0;
2426
2939
  }
2427
2940
  try {
2428
- const program = (0, import_parser6.parse)(text);
2429
- const found = (0, import_parser6.directivesOf)(text);
2941
+ const program2 = (0, import_parser7.parse)(text);
2942
+ const found = (0, import_parser7.directivesOf)(text);
2430
2943
  directives.set(file, found);
2431
- if (stamp !== void 0) parsedFiles.set(file, { stamp, program, text, directives: found });
2432
- return { program, text };
2944
+ if (stamp !== void 0) parsedFiles.set(file, { stamp, program: program2, text, directives: found });
2945
+ return { program: program2, text };
2433
2946
  } catch (error) {
2434
- if (error instanceof import_parser6.ParseError || error instanceof import_parser6.LexError) {
2947
+ if (error instanceof import_parser7.ParseError || error instanceof import_parser7.LexError) {
2435
2948
  const { line, column } = error;
2436
2949
  diagnostics.push({ file, message: error.message, line, column, category: "syntax" });
2437
2950
  return void 0;
@@ -2439,31 +2952,33 @@ function parseFile(file, diagnostics, directives) {
2439
2952
  throw error;
2440
2953
  }
2441
2954
  }
2442
- function importedSpecifiers(program) {
2443
- return program.body.statements.flatMap((s) => s.type === "ImportStatement" || s.type === "ExportAllStatement" || s.type === "ExportNamedStatement" && s.source ? [s.source.value] : []);
2955
+ function importedSpecifiers(program2) {
2956
+ return program2.body.statements.flatMap((s) => s.type === "ImportStatement" || s.type === "ExportAllStatement" || s.type === "ExportNamedStatement" && s.source ? [s.source.value] : []);
2444
2957
  }
2445
2958
  var libraryCache = /* @__PURE__ */ new Map();
2446
2959
  function parsedLibrary(file) {
2447
2960
  let stamp;
2448
2961
  try {
2449
- const at = (0, import_node_fs2.statSync)(file);
2962
+ const at = (0, import_node_fs3.statSync)(file);
2450
2963
  stamp = `${at.size}:${at.mtimeMs}`;
2451
2964
  } catch {
2452
2965
  stamp = "";
2453
2966
  }
2454
2967
  const cached = libraryCache.get(file);
2455
2968
  if (cached && cached.stamp === stamp) return cached.program;
2456
- const program = (0, import_parser6.parse)((0, import_node_fs2.readFileSync)(file, "utf8"));
2457
- libraryCache.set(file, { stamp, program });
2458
- return program;
2969
+ const program2 = (0, import_parser7.parse)((0, import_node_fs3.readFileSync)(file, "utf8"));
2970
+ libraryCache.set(file, { stamp, program: program2 });
2971
+ return program2;
2459
2972
  }
2460
2973
  var moduleAnalyses = /* @__PURE__ */ new Map();
2461
2974
  function analyzeModules(modules, config2) {
2462
2975
  const out2 = [];
2463
2976
  const analyses = /* @__PURE__ */ new Map();
2464
- const libraries = config2 ? (0, import_parser6.resolveTypeLibraries)(config2) : { files: [], lowerings: [], problems: [] };
2977
+ const libraries = config2 ? (0, import_parser7.resolveTypeLibraries)(config2) : { files: [], lowerings: [], problems: [] };
2465
2978
  for (const p of libraries.problems) out2.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
2466
2979
  const libs = libraries.files.map(parsedLibrary);
2980
+ const noLib = config2?.noLib ?? false;
2981
+ const checked = libs.length > 0 || !noLib;
2467
2982
  const globals = libs.flatMap((lib) => lib.body.statements.flatMap((s) => s.type === "DeclareStatement" ? [s.name] : []));
2468
2983
  const byFile = new Map(modules.map((m) => [m.file, m]));
2469
2984
  const exportsCache = /* @__PURE__ */ new Map();
@@ -2472,53 +2987,54 @@ function analyzeModules(modules, config2) {
2472
2987
  let partials = 0;
2473
2988
  const readWhile = /* @__PURE__ */ new Map();
2474
2989
  const settingFor = `${libraries.files.join("|")}#${config2?.directory ?? ""}#${JSON.stringify(config2?.paths ?? {})}`;
2475
- const analyzeModule = (file, program) => {
2990
+ const analyzeModule = (file, program2) => {
2476
2991
  const done = analyzed.get(file);
2477
2992
  if (done) return done;
2478
2993
  const remembered = moduleAnalyses.get(file);
2479
- if (remembered && remembered.setting === settingFor && libs.length === remembered.libs.length && remembered.libs.every((lib, i) => lib === libs[i]) && remembered.program === program && [...remembered.read].every(([read2, stamp]) => stampOf(read2) === stamp)) {
2994
+ 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)) {
2480
2995
  analyzed.set(file, remembered.result);
2481
2996
  readWhile.set(file, remembered.read);
2482
2997
  return remembered.result;
2483
2998
  }
2484
- const scopes = (0, import_parser6.analyzeScopes)(program, { builtinGlobals: globals, reportUndeclared: libs.length > 0 });
2999
+ const scopes = (0, import_parser7.analyzeScopes)(program2, { builtinGlobals: globals, reportUndeclared: checked, noLib });
2485
3000
  const said = [];
2486
3001
  for (const d of scopes.diagnostics) {
2487
3002
  if (d.kind !== "undeclared") continue;
2488
3003
  said.push({ file, message: d.message, line: d.node.line.start, column: d.node.column.start, category: "type" });
2489
3004
  }
2490
3005
  const before = partials;
2491
- const types = (0, import_parser6.analyzeTypes)(program, scopes, {
3006
+ const types = (0, import_parser7.analyzeTypes)(program2, scopes, {
2492
3007
  libs,
2493
3008
  resolveModule: resolverFor(file),
2494
- reportUnknownTypes: libs.length > 0
3009
+ noLib,
3010
+ reportUnknownTypes: checked
2495
3011
  });
2496
3012
  for (const d of types.diagnostics) {
2497
3013
  const at = d.node;
2498
3014
  said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type" });
2499
3015
  }
2500
- const result = { types, diagnostics: said };
3016
+ const result2 = { types, diagnostics: said };
2501
3017
  if (partials === before) {
2502
- analyzed.set(file, result);
2503
- const read2 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
3018
+ analyzed.set(file, result2);
3019
+ const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2504
3020
  const own = stampOf(file);
2505
- if (own !== void 0) read2.set(file, own);
2506
- readWhile.set(file, read2);
2507
- if ([...read2.values()].every((stamp) => stamp !== void 0)) {
2508
- moduleAnalyses.set(file, { setting: settingFor, libs: [...libs], program, read: read2, result });
3021
+ if (own !== void 0) read3.set(file, own);
3022
+ readWhile.set(file, read3);
3023
+ if ([...read3.values()].every((stamp) => stamp !== void 0)) {
3024
+ moduleAnalyses.set(file, { setting: settingFor, libs: [...libs], program: program2, read: read3, result: result2 });
2509
3025
  }
2510
3026
  }
2511
- return result;
3027
+ return result2;
2512
3028
  };
2513
3029
  const resolverFor = (file) => (specifier) => {
2514
- const target = (0, import_parser6.resolveModulePath)(file, specifier, config2);
3030
+ const target = (0, import_parser7.resolveModulePath)(file, specifier, config2);
2515
3031
  if (!target) return void 0;
2516
3032
  const noteRead = () => {
2517
- const read2 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2518
- readWhile.set(file, read2);
3033
+ const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
3034
+ readWhile.set(file, read3);
2519
3035
  const stamp = stampOf(target);
2520
- if (stamp !== void 0) read2.set(target, stamp);
2521
- for (const [below, belowStamp] of readWhile.get(target) ?? []) read2.set(below, belowStamp);
3036
+ if (stamp !== void 0) read3.set(target, stamp);
3037
+ for (const [below, belowStamp] of readWhile.get(target) ?? []) read3.set(below, belowStamp);
2522
3038
  };
2523
3039
  if (inProgress.has(target)) {
2524
3040
  partials++;
@@ -2529,19 +3045,19 @@ function analyzeModules(modules, config2) {
2529
3045
  noteRead();
2530
3046
  return cached;
2531
3047
  }
2532
- let program = byFile.get(target)?.program;
2533
- if (!program) {
3048
+ let program2 = byFile.get(target)?.program;
3049
+ if (!program2) {
2534
3050
  try {
2535
- program = (0, import_parser6.parse)((0, import_node_fs2.readFileSync)(target, "utf8"));
3051
+ program2 = (0, import_parser7.parse)((0, import_node_fs3.readFileSync)(target, "utf8"));
2536
3052
  } catch {
2537
3053
  return void 0;
2538
3054
  }
2539
3055
  }
2540
3056
  inProgress.add(target);
2541
3057
  try {
2542
- const scopes = (0, import_parser6.analyzeScopes)(program, { builtinGlobals: globals });
2543
- const types = byFile.has(target) ? analyzeModule(target, program).types : (0, import_parser6.analyzeTypes)(program, scopes, { libs, resolveModule: resolverFor(target), diagnostics: false });
2544
- const exports2 = (0, import_parser6.moduleExports)(program, scopes, types, resolverFor(target));
3058
+ const scopes = (0, import_parser7.analyzeScopes)(program2, { builtinGlobals: globals, noLib });
3059
+ const types = byFile.has(target) ? analyzeModule(target, program2).types : (0, import_parser7.analyzeTypes)(program2, scopes, { libs, noLib, resolveModule: resolverFor(target), diagnostics: false });
3060
+ const exports2 = (0, import_parser7.moduleExports)(program2, scopes, types, resolverFor(target));
2545
3061
  exportsCache.set(target, exports2);
2546
3062
  noteRead();
2547
3063
  return exports2;
@@ -2574,13 +3090,14 @@ function allStatements(statements) {
2574
3090
  }
2575
3091
 
2576
3092
  // src/cli.ts
2577
- var usage = "usage: tilua <entry> [--out <file>] [--config <tilua.config.json>] [--noCheck] [--target luau|lua51]";
3093
+ var usage = "usage: tilua <entry> [--out <file>] [--output single|chunks|modules] [--config <tilua.config.json>] [--noCheck] [--target luau|lua51]";
2578
3094
  var args = process.argv.slice(2);
2579
3095
  var entry;
2580
3096
  var out;
2581
3097
  var config;
2582
3098
  var typeCheck = true;
2583
3099
  var buildTarget;
3100
+ var output;
2584
3101
  for (let i = 0; i < args.length; i++) {
2585
3102
  const arg = args[i];
2586
3103
  if (arg === "--out") out = args[++i];
@@ -2593,6 +3110,13 @@ for (let i = 0; i < args.length; i++) {
2593
3110
  process.exit(2);
2594
3111
  }
2595
3112
  buildTarget = value;
3113
+ } else if (arg === "--output") {
3114
+ const value = args[++i];
3115
+ if (value !== "single" && value !== "chunks" && value !== "modules") {
3116
+ console.error(`--output must be 'single', 'chunks' or 'modules', not '${value}'`);
3117
+ process.exit(2);
3118
+ }
3119
+ output = value;
2596
3120
  } else if (arg === "--help" || arg === "-h") {
2597
3121
  console.log(usage);
2598
3122
  process.exit(0);
@@ -2603,18 +3127,32 @@ if (!entry) {
2603
3127
  process.exit(2);
2604
3128
  }
2605
3129
  async function main(from) {
2606
- const result = await bundle({ entry: from, config, typeCheck, target: buildTarget });
2607
- for (const d of result.diagnostics) {
3130
+ const found = resolveConfig(config, (0, import_node_path3.resolve)(from)).config;
3131
+ const mode = output ?? found?.output ?? "single";
3132
+ const entryFile = mode === "modules" ? void 0 : out ?? from.replace(/\.tilua$/, "") + ".luau";
3133
+ const folder = entryFile !== void 0 ? (0, import_node_path3.dirname)(entryFile) : out ?? found?.directory ?? (0, import_node_path3.dirname)(from);
3134
+ const result2 = await bundle({
3135
+ entry: from,
3136
+ config,
3137
+ typeCheck,
3138
+ target: buildTarget,
3139
+ output: mode,
3140
+ outFile: entryFile !== void 0 ? (0, import_node_path3.basename)(entryFile) : void 0
3141
+ });
3142
+ for (const d of result2.diagnostics) {
2608
3143
  console.error(`${(0, import_node_path3.relative)(process.cwd(), d.file)}:${d.line}:${d.column} ${d.message}`);
2609
3144
  }
2610
- if (result.code === void 0) {
3145
+ if (result2.code === void 0) {
2611
3146
  console.error("no bundle written");
2612
3147
  process.exit(1);
2613
3148
  }
2614
- const target = out ?? from.replace(/\.tilua$/, "") + ".luau";
2615
- (0, import_node_fs3.writeFileSync)(target, result.code);
2616
- console.log(`${target}: ${result.modules.length} module${result.modules.length === 1 ? "" : "s"}`);
2617
- process.exit(result.diagnostics.length ? 1 : 0);
3149
+ for (const file of result2.files) {
3150
+ const target = (0, import_node_path3.join)(folder, file.path);
3151
+ (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(target), { recursive: true });
3152
+ (0, import_node_fs4.writeFileSync)(target, file.code);
3153
+ console.log(`${target}: ${file.modules.length} module${file.modules.length === 1 ? "" : "s"}`);
3154
+ }
3155
+ process.exit(result2.diagnostics.length ? 1 : 0);
2618
3156
  }
2619
3157
  main(entry).catch((error) => {
2620
3158
  console.error(error instanceof Error ? error.message : error);