@tilua/compiler 8.1.0 → 9.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -384,6 +384,10 @@ var SharedRuntime = class {
384
384
  return { declare, define };
385
385
  }
386
386
  };
387
+ function moduleArguments(context, name) {
388
+ const loader2 = context.loader?.(name);
389
+ return loader2 ? [string(name), loader2] : [string(name)];
390
+ }
387
391
  function lower(program2, scopes, options = {}) {
388
392
  const result2 = new Lowerer(program2, scopes, options).run();
389
393
  dropUnreachable(result2.statements);
@@ -700,6 +704,7 @@ var Lowerer = class {
700
704
  const plugin3 = lowerings[i].plugin;
701
705
  if (!plugin3.methodCall) continue;
702
706
  const answer = plugin3.methodCall({
707
+ options: lowerings[i].options ?? {},
703
708
  method,
704
709
  receiver,
705
710
  receiverGlobal,
@@ -731,6 +736,7 @@ var Lowerer = class {
731
736
  const plugin3 = lowerings[i].plugin;
732
737
  if (!plugin3.globalCall) continue;
733
738
  const answer = plugin3.globalCall({
739
+ options: lowerings[i].options ?? {},
734
740
  name,
735
741
  at: this.callSite(node.callee),
736
742
  get arguments() {
@@ -750,6 +756,7 @@ var Lowerer = class {
750
756
  if (name === void 0) return void 0;
751
757
  for (let i = lowerings.length - 1; i >= 0; i--) {
752
758
  const answer = lowerings[i].plugin.globalValue?.({
759
+ options: lowerings[i].options ?? {},
753
760
  name,
754
761
  at: this.callSite(node),
755
762
  use: (runtime2) => this.loweringRuntime(i, runtime2)
@@ -1003,12 +1010,12 @@ end`).body.statements, out);
1003
1010
  local2 = this.names.fresh(moduleName(key));
1004
1011
  moduleLocals.set(key, local2);
1005
1012
  locals.push(local2);
1006
- requires.push(assign([identifier(local2)], [call(context.require, [string(key)])]));
1013
+ requires.push(assign([identifier(local2)], [call(context.require, moduleArguments(context, key))]));
1007
1014
  }
1008
1015
  return local2;
1009
1016
  };
1010
1017
  const load = (key) => {
1011
- if (!moduleLocals.has(key)) requires.push(callStatement(call(context.require, [string(key)])));
1018
+ if (!moduleLocals.has(key)) requires.push(callStatement(call(context.require, moduleArguments(context, key))));
1012
1019
  };
1013
1020
  const exportBinding = (node, exported) => {
1014
1021
  const binding = node && this.bindingByDeclaration.get(node);
@@ -1117,20 +1124,22 @@ end`).body.statements, out);
1117
1124
  continue;
1118
1125
  }
1119
1126
  if (declaration.type === "ClassDeclaration") {
1120
- body.push(...this.classDeclaration(declaration, "assign"));
1127
+ body.push(...this.classDeclaration(declaration, "assign").map((s) => this.from(s, declaration)));
1121
1128
  continue;
1122
1129
  }
1123
1130
  switch (declaration.type) {
1131
+ // Stamped with where they were written, as any statement is:
1132
+ // an error in a top-level `const` names its line.
1124
1133
  case "VariableDeclaration":
1125
- body.push(...this.variableDeclaration(declaration, "assign"));
1134
+ body.push(...this.variableDeclaration(declaration, "assign").map((s) => this.from(s, declaration)));
1126
1135
  break;
1127
1136
  case "ExportDefaultStatement": {
1128
1137
  const exported = declaration.declaration;
1129
1138
  if (exported.type === "ClassDeclaration") {
1130
- body.push(...this.classDeclaration(exported, "assign"));
1131
- body.push(assign([member(exportsTable(), "default")], [this.reference(exported.name)]));
1139
+ body.push(...this.classDeclaration(exported, "assign").map((s) => this.from(s, exported)));
1140
+ body.push(this.from(assign([member(exportsTable(), "default")], [this.reference(exported.name)]), declaration));
1132
1141
  } else {
1133
- body.push(assign([member(exportsTable(), "default")], [this.expression(exported)]));
1142
+ body.push(this.from(assign([member(exportsTable(), "default")], [this.expression(exported)]), declaration));
1134
1143
  }
1135
1144
  break;
1136
1145
  }
@@ -1168,7 +1177,7 @@ end`).body.statements, out);
1168
1177
  this.report(source, `Cannot find module '${source.value}'`);
1169
1178
  return nil();
1170
1179
  }
1171
- return call(context.import ?? context.require, [string(key)]);
1180
+ return call(context.import ?? context.require, moduleArguments(context, key));
1172
1181
  }
1173
1182
  isRewritten(node) {
1174
1183
  const binding = this.bindingByDeclaration.get(node);
@@ -1367,9 +1376,9 @@ end`).body.statements, out);
1367
1376
  return this.assignment(node);
1368
1377
  case "CompoundAssignmentStatement": {
1369
1378
  if (node.operator === "??=") return this.nullishAssignment(node);
1370
- const join = node.operator === "+=" ? this.options.types?.joins.get(node) : void 0;
1371
- if (join === "dynamic") return this.dynamicAddition(node);
1372
- const operator = join === "concat" ? "..=" : node.operator;
1379
+ const join2 = node.operator === "+=" ? this.options.types?.joins.get(node) : void 0;
1380
+ if (join2 === "dynamic") return this.dynamicAddition(node);
1381
+ const operator = join2 === "concat" ? "..=" : node.operator;
1373
1382
  if (!this.syntax.has("compoundAssignment")) return this.compoundAssignment51(node, operator);
1374
1383
  return [{
1375
1384
  type: "CompoundAssignmentStatement",
@@ -2055,9 +2064,9 @@ end
2055
2064
  return call(this.helper("instanceof"), [this.keptToOne(left), this.keptToOne(right)]);
2056
2065
  }
2057
2066
  if (node.operator === "+") {
2058
- const join = this.options.types?.joins.get(node);
2059
- if (join === "concat") return binary("..", left, right);
2060
- if (join === "dynamic") return call(this.helper("add"), [this.keptToOne(left), this.keptToOne(right)]);
2067
+ const join2 = this.options.types?.joins.get(node);
2068
+ if (join2 === "concat") return binary("..", left, right);
2069
+ if (join2 === "dynamic") return call(this.helper("add"), [this.keptToOne(left), this.keptToOne(right)]);
2061
2070
  }
2062
2071
  if (!this.syntax.has("floorDivision") && node.operator === "//") return this.floorDivide(left, right);
2063
2072
  return binary(node.operator, left, right);
@@ -2774,7 +2783,7 @@ async function loadLowerings(modules) {
2774
2783
  });
2775
2784
  continue;
2776
2785
  }
2777
- lowerings.push({ plugin: plugin3, from: module.from, library: module.library });
2786
+ lowerings.push({ plugin: plugin3, from: module.from, library: module.library, options: module.options });
2778
2787
  } catch (error) {
2779
2788
  problems.push({
2780
2789
  file: module.file,
@@ -2786,8 +2795,8 @@ async function loadLowerings(modules) {
2786
2795
  }
2787
2796
 
2788
2797
  // src/bundle.ts
2789
- import { readFileSync as readFileSync3, statSync } from "fs";
2790
- import { basename, dirname, extname, posix, relative, resolve as resolve2 } from "path";
2798
+ import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
2799
+ import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve as resolve2 } from "path";
2791
2800
  import {
2792
2801
  parse,
2793
2802
  analyzeScopes,
@@ -2796,6 +2805,9 @@ import {
2796
2805
  resolveModulePath,
2797
2806
  resolveTypeLibraries,
2798
2807
  importExpressions,
2808
+ sourceMapOf,
2809
+ sourceMapTypes,
2810
+ outputPathOf,
2799
2811
  directivesOf,
2800
2812
  applyDirectives,
2801
2813
  UNUSED_EXPECT_ERROR,
@@ -2804,7 +2816,7 @@ import {
2804
2816
  LexError,
2805
2817
  luauString as luauString4
2806
2818
  } from "@tilua/parser";
2807
- import { parse as parseLuau2, parseExpressionFromSource as parseExpressionFromSource2, print } from "luau-parser";
2819
+ import { parse as parseLuau2, parseExpressionFromSource as parseExpressionFromSource2, print as print2 } from "luau-parser";
2808
2820
 
2809
2821
  // src/treeshake.ts
2810
2822
  function walk(node, visit, parent) {
@@ -3072,23 +3084,29 @@ function ownExport(f, name) {
3072
3084
  // src/linemap.ts
3073
3085
  import { luauString as luauString3 } from "@tilua/parser";
3074
3086
  function statementsOf(node) {
3075
- const out = [];
3076
- const visit = (value) => {
3087
+ return statementTree(node).statements;
3088
+ }
3089
+ function statementTree(node) {
3090
+ const statements = [];
3091
+ const parents = [];
3092
+ const visit = (value, parent) => {
3077
3093
  if (!value || typeof value !== "object") return;
3078
- if (Array.isArray(value)) return void value.forEach(visit);
3094
+ if (Array.isArray(value)) return void value.forEach((item) => visit(item, parent));
3079
3095
  const record = value;
3096
+ let inside = parent;
3080
3097
  if (typeof record.type === "string" && record.type.endsWith("Statement")) {
3081
- out.push(value);
3098
+ inside = statements.push(value) - 1;
3099
+ parents.push(parent);
3082
3100
  }
3083
3101
  for (const [key, child] of Object.entries(record)) {
3084
- if (key !== "line" && key !== "column") visit(child);
3102
+ if (key !== "line" && key !== "column") visit(child, inside);
3085
3103
  }
3086
3104
  };
3087
- visit(node);
3088
- return out;
3105
+ visit(node, -1);
3106
+ return { statements, parents };
3089
3107
  }
3090
3108
  function buildLineMap(emitted, printed, origins) {
3091
- const before = statementsOf(emitted);
3109
+ const { statements: before, parents } = statementTree(emitted);
3092
3110
  const after = statementsOf(printed);
3093
3111
  if (before.length !== after.length) return /* @__PURE__ */ new Map();
3094
3112
  const map = /* @__PURE__ */ new Map();
@@ -3099,6 +3117,18 @@ function buildLineMap(emitted, printed, origins) {
3099
3117
  const line = after[i].line.start;
3100
3118
  if (!map.has(line)) map.set(line, origin);
3101
3119
  }
3120
+ const holds = /* @__PURE__ */ new Set();
3121
+ for (let i = 0; i < before.length; i++) {
3122
+ if (!origins.has(before[i])) continue;
3123
+ for (let up = parents[i]; up >= 0 && !holds.has(up); up = parents[up]) holds.add(up);
3124
+ }
3125
+ for (let i = 0; i < before.length; i++) {
3126
+ const origin = origins.get(before[i]);
3127
+ if (!origin || holds.has(i)) continue;
3128
+ for (let line = after[i].line.start + 1; line <= after[i].line.end; line++) {
3129
+ if (!map.has(line)) map.set(line, origin);
3130
+ }
3131
+ }
3102
3132
  return map;
3103
3133
  }
3104
3134
  var VLQ_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@@ -3130,18 +3160,59 @@ function lineMapSource(map, unpack) {
3130
3160
  return `${unpack}({${files.map(luauString3).join(",")}},${luauString3(data)})`;
3131
3161
  }
3132
3162
 
3163
+ // src/output.ts
3164
+ import { print, PrintError } from "luau-parser";
3165
+ var WRITTEN = /* @__PURE__ */ new Set([
3166
+ "compoundAssignment",
3167
+ "continue",
3168
+ "ifExpression",
3169
+ "floorDivision",
3170
+ "attributes",
3171
+ "numberLiterals"
3172
+ ]);
3173
+ function printOptions(syntax, format = {}) {
3174
+ return {
3175
+ syntax: [...syntax].some((s) => WRITTEN.has(s)) ? "luau" : "lua51",
3176
+ compact: format.minify ?? false,
3177
+ ...format.indent !== void 0 && { indent: format.indent }
3178
+ };
3179
+ }
3180
+ function printOutput(node, options) {
3181
+ try {
3182
+ return { code: print(node, options) };
3183
+ } catch (error) {
3184
+ if (!(error instanceof PrintError)) throw error;
3185
+ return {
3186
+ 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"`
3187
+ };
3188
+ }
3189
+ }
3190
+
3133
3191
  // src/bundle.ts
3192
+ var WRITTEN_BY_TILUA = "-- Written by tilua";
3134
3193
  async function bundle(options) {
3135
- const entry = resolve2(options.entry);
3136
3194
  const diagnostics = [];
3195
+ const from = options.entry !== void 0 ? resolve2(options.entry) : resolve2(options.root ?? process.cwd(), "tilua.config.json");
3137
3196
  const inline = options.config !== void 0 && typeof options.config !== "string";
3138
- const { config, problems } = resolveConfig(options.config, entry, options.root ?? (inline ? dirname(entry) : process.cwd()));
3197
+ const { config, problems } = resolveConfig(options.config, from, options.root ?? (inline ? dirname(from) : process.cwd()));
3139
3198
  for (const p of problems) diagnostics.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
3140
- const root = resolve2(options.root ?? config?.directory ?? dirname(entry));
3199
+ const root = resolve2(options.root ?? config?.directory ?? dirname(from));
3141
3200
  const nameOf = (file) => relative(root, file).replace(/\\/g, "/").replace(/\.tilua$/, "");
3201
+ const mode = options.output ?? config?.output ?? "single";
3202
+ const perFile = mode === "files";
3203
+ const rootDir = resolve2(config?.rootDir ?? root);
3204
+ const entry = options.entry !== void 0 ? resolve2(options.entry) : void 0;
3205
+ const anchor = entry ?? config?.path ?? from;
3206
+ const stop = (message, file = anchor) => ({
3207
+ files: [],
3208
+ modules: [],
3209
+ diagnostics: [...diagnostics, { file, message, line: 1, column: 1, category: "config" }]
3210
+ });
3211
+ if (!perFile && entry === void 0) return stop('No entry to build: only "output": "files" builds without one');
3142
3212
  const sources = /* @__PURE__ */ new Map();
3143
3213
  const directives = /* @__PURE__ */ new Map();
3144
- const queue = [entry];
3214
+ const queue = perFile ? tiluaFiles(rootDir) : [entry];
3215
+ if (perFile && !queue.length) return stop(`No .tilua file under ${relative(process.cwd(), rootDir) || "."}`);
3145
3216
  while (queue.length) {
3146
3217
  const file = queue.shift();
3147
3218
  if (sources.has(file)) continue;
@@ -3159,6 +3230,22 @@ async function bundle(options) {
3159
3230
  if (target && !target.endsWith(".d.tilua")) queue.push(target);
3160
3231
  }
3161
3232
  }
3233
+ if (perFile) {
3234
+ const outside = [...sources.keys()].filter((file) => isOutside(rootDir, file));
3235
+ if (outside.length) {
3236
+ return {
3237
+ files: [],
3238
+ modules: [],
3239
+ diagnostics: [...diagnostics, ...outside.map((file) => ({
3240
+ file,
3241
+ message: `This file is imported, but it is outside rootDir (${relative(root, rootDir) || "."}), where every file of "files" output has to be`,
3242
+ line: 1,
3243
+ column: 1,
3244
+ category: "module"
3245
+ }))]
3246
+ };
3247
+ }
3248
+ }
3162
3249
  const analysis = analyzeModules([...sources.values()], config);
3163
3250
  const { lowerings, problems: loweringProblems } = await loadLowerings(analysis.lowerings);
3164
3251
  for (const p of loweringProblems) {
@@ -3169,19 +3256,56 @@ async function bundle(options) {
3169
3256
  const G = names.fresh("G");
3170
3257
  const requireExpression = member(identifier(G), "require");
3171
3258
  const importExpression = member(identifier(G), "import");
3172
- const mode = options.output ?? config?.output ?? "single";
3173
3259
  const byPath = syntaxOf(lowerings).has("requireByPath");
3260
+ if (perFile && !byPath) {
3261
+ 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)`, config?.path ?? anchor);
3262
+ }
3263
+ const outputPath = (file) => relative(rootDir, file).replace(/\\/g, "/").replace(/\.tilua$/, ".luau");
3264
+ const fileOfModule = new Map([...sources.values()].map((s) => [s.name, s.file]));
3265
+ const outDir = resolve2(options.outDir ?? config?.outDir ?? rootDir);
3266
+ const written = (file) => join(outDir, outputPath(file));
3267
+ let host;
3268
+ let runtimeFile = join(outDir, "_tilua.luau");
3269
+ if (perFile) {
3270
+ const outputs = [...sources.keys()].map(written);
3271
+ const taken = new Set(outputs.map(pathKey));
3272
+ for (let n = 2; taken.has(pathKey(runtimeFile)); n++) runtimeFile = join(outDir, `_tilua${n}.luau`);
3273
+ for (let i = lowerings.length - 1; i >= 0 && !host; i--) {
3274
+ const { plugin: plugin3, options: own } = lowerings[i];
3275
+ host = plugin3.files?.({ options: own ?? {}, directory: config?.directory ?? root, outDir, files: outputs, runtime: runtimeFile });
3276
+ }
3277
+ for (const problem of host?.problems ?? []) {
3278
+ diagnostics.push({
3279
+ file: problem.file ?? config?.path ?? anchor,
3280
+ message: problem.message,
3281
+ line: 1,
3282
+ column: 1,
3283
+ category: "config",
3284
+ ...problem.severity === "warning" && { severity: "warning" }
3285
+ });
3286
+ }
3287
+ if (host?.runtime !== void 0) {
3288
+ if (isOutside(outDir, resolve2(host.runtime))) return stop(`The runtime has to go inside outDir, not at ${host.runtime}`);
3289
+ if (taken.has(pathKey(host.runtime))) return stop(`The runtime cannot go at ${host.runtime}: a module's file is there`);
3290
+ runtimeFile = resolve2(host.runtime);
3291
+ }
3292
+ }
3293
+ const placeOf = (file) => host?.place?.(file) ?? placeInFolder(relative(outDir, file));
3294
+ const requireFrom = (from2, to) => pathBetween(placeOf(from2), placeOf(to), host?.root);
3295
+ const isScript = (file) => host?.script?.(file) ?? false;
3174
3296
  const modules = /* @__PURE__ */ new Map();
3175
3297
  const origins = /* @__PURE__ */ new WeakMap();
3176
3298
  const shared = new SharedRuntime(names);
3177
3299
  const shaking = options.treeShaking ?? config?.treeShaking ?? false;
3178
- const mapping = options.lineMap ?? config?.lineMap ?? true;
3300
+ const format = { indent: options.indent ?? config?.indent, minify: options.minify ?? config?.minify ?? false };
3301
+ const printing = printOptions(syntaxOf(lowerings), format);
3302
+ const mapping = !format.minify && (options.lineMap ?? config?.lineMap ?? true);
3179
3303
  const resolveIn = (file) => (specifier) => {
3180
3304
  const found = resolveModulePath(file, specifier, config);
3181
3305
  return found !== void 0 && sources.has(found) ? found : void 0;
3182
3306
  };
3183
- const shaken = shaking ? treeShake([...sources.values()].map((s) => ({ file: s.file, program: s.program, scopes: s.scopes, resolve: resolveIn(s.file) })), entry) : void 0;
3184
- const pending = [entry];
3307
+ const shaken = shaking && entry !== void 0 && !perFile ? treeShake([...sources.values()].map((s) => ({ file: s.file, program: s.program, scopes: s.scopes, resolve: resolveIn(s.file) })), entry) : void 0;
3308
+ const pending = perFile ? [...sources.keys()] : [entry];
3185
3309
  while (pending.length) {
3186
3310
  const file = pending.shift();
3187
3311
  if (modules.has(file)) continue;
@@ -3217,6 +3341,13 @@ async function bundle(options) {
3217
3341
  lines: !mapping ? void 0 : mode === "single" ? `${G}.lines` : `${G}.maps[${luauString4(source.name)}]`,
3218
3342
  fail: mapping ? `${G}.fail` : void 0,
3219
3343
  arrays: `${G}.arrays`,
3344
+ // The file a module is in, required from this one by its
3345
+ // path, written out.
3346
+ loader: !perFile ? void 0 : (name) => {
3347
+ const target = fileOfModule.get(name);
3348
+ if (target === void 0) return void 0;
3349
+ return parseExpressionFromSource2(`function() return require(${luauString4(requireFrom(written(file), written(target)))}) end`);
3350
+ },
3220
3351
  resolve: (specifier, dynamic) => {
3221
3352
  const found = resolveModulePath(file, specifier, config);
3222
3353
  if (!found || found.endsWith(".d.tilua") || !sources.has(found)) return void 0;
@@ -3268,6 +3399,102 @@ async function bundle(options) {
3268
3399
  return { files: [], modules: moduleNames, diagnostics };
3269
3400
  }
3270
3401
  if (shaking) shared.shake(lowered.flatMap((m) => m.statements));
3402
+ const unwritable = (problem) => ({
3403
+ files: [],
3404
+ modules: moduleNames,
3405
+ diagnostics: [...diagnostics, { file: anchor, message: problem, line: 1, column: 1, category: "config" }]
3406
+ });
3407
+ const write = (node) => {
3408
+ const printed2 = printOutput(node, printing);
3409
+ return "problem" in printed2 ? printed2 : printed2.code;
3410
+ };
3411
+ if (perFile) {
3412
+ const imported = [];
3413
+ for (const [file, source] of sources) {
3414
+ for (const { specifier, line, column } of importSites(source.program)) {
3415
+ const target = resolveIn(file)(specifier);
3416
+ if (target === void 0 || !isScript(written(target))) continue;
3417
+ imported.push({
3418
+ file,
3419
+ line,
3420
+ column,
3421
+ category: "module",
3422
+ message: `'${specifier}' is a script the game runs, which nothing can import: move what it shares into a module of its own`
3423
+ });
3424
+ }
3425
+ }
3426
+ if (imported.length) return { files: [], modules: moduleNames, diagnostics: [...diagnostics, ...imported] };
3427
+ const runtimePath = relative(outDir, runtimeFile).replace(/\\/g, "/");
3428
+ const files2 = [];
3429
+ for (const [file, module] of modules) {
3430
+ const path = outputPath(file);
3431
+ const at = written(file);
3432
+ const script = isScript(at);
3433
+ const prelude = host?.prelude?.(at);
3434
+ const body = [
3435
+ ...prelude !== void 0 ? parseLuau2(prelude).body.statements : [],
3436
+ ...parseLuau2(`local ${G} = require(${luauString4(requireFrom(at, runtimeFile))})`).body.statements
3437
+ ];
3438
+ for (let i = 0; i < module.shared.length; i += 100) {
3439
+ const some = module.shared.slice(i, i + 100);
3440
+ body.push(local(some, some.map((name) => member(member(identifier(G), "shared"), name))));
3441
+ }
3442
+ if (module.usesScriptArgs) body.push(...parseLuau2(`local scriptArgs = ${G}.scriptArgs`).body.statements);
3443
+ body.push(assign(
3444
+ [index(member(identifier(G), "modules"), string(module.name))],
3445
+ [moduleTable(module)]
3446
+ ));
3447
+ const value = `${G}.${mapping ? "run" : "require"}(${luauString4(module.name)})`;
3448
+ const returned2 = module.info.exports && !script ? entryReturn(lowerings, module.name, value, () => analysis.exportsOf(file)) : { statements: [], value };
3449
+ if ("problem" in returned2) return stop(returned2.problem.message, file);
3450
+ const ending = parseLuau2(`${returned2.statements.join("\n")}
3451
+ return ${returned2.value}`).body.statements;
3452
+ const chunk = program(body);
3453
+ if (mapping) {
3454
+ const text = write(chunk);
3455
+ if (typeof text !== "string") return unwritable(text.problem);
3456
+ const lines = buildLineMap(chunk, parseLuau2(text), origins);
3457
+ const named = host?.place?.(at) ? `${placeOf(at).join("/")}.luau` : path;
3458
+ body.push(...mapRegistration(G, { path: named, modules: [module] }, lineMapSource(lines, `${G}.lineMap`)));
3459
+ }
3460
+ body.push(...ending);
3461
+ const code = write(chunk);
3462
+ if (typeof code !== "string") return unwritable(code.problem);
3463
+ const from2 = relative(dirname(at), file).replace(/\\/g, "/");
3464
+ files2.push({ path, code: `${code}
3465
+ ${WRITTEN_BY_TILUA} from ${from2}: edit that file, not this one.
3466
+ `, modules: [module.name] });
3467
+ }
3468
+ const runtimeProgram = parseLuau2(runtime(G, {
3469
+ several: true,
3470
+ perFile: true,
3471
+ byPath,
3472
+ files: /* @__PURE__ */ new Map(),
3473
+ imports: lowered.some((m) => m.dynamicImports.size > 0),
3474
+ mapping
3475
+ }));
3476
+ const helpers2 = shared.chunk(new Set(lowered.flatMap((m) => m.shared)));
3477
+ const helperNames = helpers2.declare.flatMap((statement) => statement.type === "LocalStatement" ? statement.names.map((n) => n.name) : []);
3478
+ runtimeProgram.body.statements.unshift(...helpers2.declare);
3479
+ runtimeProgram.body.statements.push(...helpers2.define);
3480
+ if (helperNames.length) {
3481
+ runtimeProgram.body.statements.push(assign(
3482
+ [member(identifier(G), "shared")],
3483
+ [table(helperNames.map((name) => field(name, identifier(name))))]
3484
+ ));
3485
+ }
3486
+ if (lowered.some((m) => m.usesScriptArgs)) {
3487
+ runtimeProgram.body.statements.push(...parseLuau2(`${G}.scriptArgs = {}
3488
+ ${G}.arrays[${G}.scriptArgs] = true`).body.statements);
3489
+ }
3490
+ runtimeProgram.body.statements.push(returns([identifier(G)]));
3491
+ const runtimeCode = write(runtimeProgram);
3492
+ if (typeof runtimeCode !== "string") return unwritable(runtimeCode.problem);
3493
+ files2.push({ path: runtimePath, code: `${runtimeCode}
3494
+ ${WRITTEN_BY_TILUA}: the runtime every file of the project requires.
3495
+ `, modules: [] });
3496
+ return { code: files2[0].code, files: files2, modules: moduleNames, diagnostics };
3497
+ }
3271
3498
  const several = mode !== "single";
3272
3499
  const layout = layoutOf(mode, modules.get(entry), lowered, options.outFile);
3273
3500
  const usesScriptArgs = lowered.some((m) => m.usesScriptArgs);
@@ -3299,17 +3526,24 @@ async function bundle(options) {
3299
3526
  const result2 = names.fresh("result");
3300
3527
  const entryName = luauString4(sources.get(entry).name);
3301
3528
  const returnsExports = modules.get(entry).info.exports;
3302
- const entryStatements = parseLuau2(!mapping ? `${returnsExports ? "return " : ""}${G}.require(${entryName})` : `
3529
+ const returned = returnsExports ? entryReturn(lowerings, sources.get(entry).name, mapping ? result2 : `${G}.require(${entryName})`, () => analysis.exportsOf(entry)) : void 0;
3530
+ if (returned && "problem" in returned) return stop(returned.problem.message);
3531
+ const declarations = returned?.statements.join("\n") ?? "";
3532
+ const entryStatements = parseLuau2(!mapping ? `${declarations}
3533
+ ${returned ? `return ${returned.value}` : `${G}.require(${entryName})`}` : `
3303
3534
  local ${ok}, ${result2} = xpcall(function()
3304
3535
  return ${G}.require(${entryName})
3305
3536
  end, ${G}.fail)
3306
3537
  if not ${ok} then
3307
3538
  error(${result2}, 0)
3308
3539
  end
3309
- ${returnsExports ? `return ${result2}` : ""}
3540
+ ${declarations}
3541
+ ${returned ? `return ${returned.value}` : ""}
3310
3542
  `).body.statements;
3311
3543
  program2.body.statements.push(...entryStatements);
3312
- const map = mapping ? buildLineMap(program2, parseLuau2(print(program2)), origins) : /* @__PURE__ */ new Map();
3544
+ let printed = write(program2);
3545
+ if (typeof printed !== "string") return unwritable(printed.problem);
3546
+ const map = mapping ? buildLineMap(program2, parseLuau2(printed), origins) : /* @__PURE__ */ new Map();
3313
3547
  if (mapping && (map.size || several)) {
3314
3548
  const mapStatements = [assign(
3315
3549
  [member(identifier(G), "lines")],
@@ -3319,7 +3553,9 @@ ${returnsExports ? `return ${result2}` : ""}
3319
3553
  program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...mapStatements);
3320
3554
  }
3321
3555
  program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...helpers.define);
3322
- const files = [{ path: layout[0].path, code: print(program2) + "\n", modules: layout[0].modules.map((m) => m.name) }];
3556
+ printed = write(program2);
3557
+ if (typeof printed !== "string") return unwritable(printed.problem);
3558
+ const files = [{ path: layout[0].path, code: printed + "\n", modules: layout[0].modules.map((m) => m.name) }];
3323
3559
  for (const file of layout.slice(1)) {
3324
3560
  const fileHelpers = shared.chunk(new Set(file.modules.flatMap((m) => m.shared)));
3325
3561
  const body = [...fileHelpers.declare];
@@ -3332,14 +3568,34 @@ ${returnsExports ? `return ${result2}` : ""}
3332
3568
  }
3333
3569
  const chunk = program([returns([functionExpression(functionBody([G], body))])]);
3334
3570
  if (mapping) {
3335
- const lines = buildLineMap(chunk, parseLuau2(print(chunk)), origins);
3571
+ const lines = buildLineMap(chunk, parseLuau2(print2(chunk, printing)), origins);
3336
3572
  body.push(...mapRegistration(G, file, lineMapSource(lines, `${G}.lineMap`)));
3337
3573
  }
3338
3574
  body.push(...fileHelpers.define);
3339
- files.push({ path: file.path, code: print(chunk) + "\n", modules: file.modules.map((m) => m.name) });
3575
+ const code = write(chunk);
3576
+ if (typeof code !== "string") return unwritable(code.problem);
3577
+ files.push({ path: file.path, code: code + "\n", modules: file.modules.map((m) => m.name) });
3340
3578
  }
3341
3579
  return { code: files[0].code, files, modules: moduleNames, diagnostics };
3342
3580
  }
3581
+ function entryReturn(lowerings, module, value, exports) {
3582
+ const syntax = [...syntaxOf(lowerings)];
3583
+ for (let i = lowerings.length - 1; i >= 0; i--) {
3584
+ const { plugin: plugin3, from, options } = lowerings[i];
3585
+ if (!plugin3.entryExports) continue;
3586
+ const answer = plugin3.entryExports({ options: options ?? {}, module, value, exports: exports(), syntax });
3587
+ if (!answer) continue;
3588
+ const statements = [...answer.statements ?? []];
3589
+ try {
3590
+ parseLuau2(`${statements.join("\n")}
3591
+ return ${answer.value}`);
3592
+ } catch (error) {
3593
+ return { problem: { message: `'${from}' answered entryExports with Luau that does not parse: ${error.message}` } };
3594
+ }
3595
+ return { statements, value: answer.value };
3596
+ }
3597
+ return { statements: [], value };
3598
+ }
3343
3599
  function moduleTable(module) {
3344
3600
  const { names: exported, links, stars } = module.info;
3345
3601
  const fields = [];
@@ -3359,10 +3615,6 @@ function layoutOf(mode, entry, modules, outFile) {
3359
3615
  const entryStem = outFile !== void 0 ? basename(outFile, extname(outFile)) : posix.basename(entry.name);
3360
3616
  const others = modules.filter((m) => m !== entry);
3361
3617
  if (mode === "single") return [{ path: `${entryStem}${extension}`, modules: [entry, ...others] }];
3362
- if (mode === "modules") {
3363
- const pathOf = (m) => `${m.name.split("/").map((part) => part === ".." ? "__" : part).join("/")}${extension}`;
3364
- return [entry, ...others].map((m) => ({ path: pathOf(m), modules: [m] }));
3365
- }
3366
3618
  const byName = new Map(modules.map((m) => [m.name, m]));
3367
3619
  const reach = (root) => {
3368
3620
  const seen = /* @__PURE__ */ new Set();
@@ -3398,11 +3650,70 @@ function layoutOf(mode, entry, modules, outFile) {
3398
3650
  return files;
3399
3651
  }
3400
3652
  function loadPath(entryPath, file, byPath) {
3401
- const path = posix.relative(posix.dirname(entryPath), file);
3402
- if (!byPath) return path;
3653
+ return byPath ? requirePath(entryPath, file) : posix.relative(posix.dirname(entryPath), file);
3654
+ }
3655
+ function placeInFolder(path) {
3656
+ const parts = path.replace(/\\/g, "/").replace(/\.luau?$/, "").split("/").filter((part) => part !== "");
3657
+ if (parts.at(-1) === "init") parts.pop();
3658
+ return parts;
3659
+ }
3660
+ function pathBetween(from, to, root) {
3661
+ if (root !== void 0 && from[0] !== to[0]) return `${root}/${to.join("/")}`;
3662
+ if (to.length > from.length && from.every((part, i) => to[i] === part)) return `@self/${to.slice(from.length).join("/")}`;
3663
+ const base = from.slice(0, -1);
3664
+ let shared = 0;
3665
+ while (shared < base.length && shared < to.length - 1 && base[shared] === to[shared]) shared++;
3666
+ const up = base.length - shared;
3667
+ return `${up === 0 ? "./" : "../".repeat(up)}${to.slice(shared).join("/")}`;
3668
+ }
3669
+ function importSites(program2) {
3670
+ const sites = [];
3671
+ for (const statement of program2.body.statements) {
3672
+ if (statement.type === "ImportStatement" || statement.type === "ExportAllStatement" || statement.type === "ExportNamedStatement" && statement.source) {
3673
+ const source = statement.source;
3674
+ sites.push({ specifier: source.value, line: source.line.start, column: source.column.start });
3675
+ }
3676
+ }
3677
+ for (const node of importExpressions(program2)) {
3678
+ if (node.source.type === "StringLiteral") sites.push({ specifier: node.source.value, line: node.source.line.start, column: node.source.column.start });
3679
+ }
3680
+ return sites;
3681
+ }
3682
+ function pathKey(path) {
3683
+ const full = resolve2(path);
3684
+ return process.platform === "win32" ? full.toLowerCase() : full;
3685
+ }
3686
+ function requirePath(from, to) {
3687
+ const path = posix.relative(posix.dirname(from), to);
3403
3688
  const bare = path.slice(0, path.length - posix.extname(path).length);
3404
3689
  return bare.startsWith("../") ? bare : `./${bare}`;
3405
3690
  }
3691
+ function tiluaFiles(folder) {
3692
+ const found = [];
3693
+ const walk2 = (at) => {
3694
+ let entries;
3695
+ try {
3696
+ entries = readdirSync(at, { withFileTypes: true });
3697
+ } catch {
3698
+ return;
3699
+ }
3700
+ for (const item of entries) {
3701
+ if (item.name.startsWith(".")) continue;
3702
+ const path = join(at, item.name);
3703
+ if (item.isDirectory()) {
3704
+ if (item.name !== "node_modules") walk2(path);
3705
+ } else if (item.name.endsWith(".tilua") && !item.name.endsWith(".d.tilua")) {
3706
+ found.push(path);
3707
+ }
3708
+ }
3709
+ };
3710
+ walk2(folder);
3711
+ return found.sort();
3712
+ }
3713
+ function isOutside(folder, file) {
3714
+ const path = relative(folder, file);
3715
+ return path === "" || path.startsWith("..") || isAbsolute(path);
3716
+ }
3406
3717
  function mapRegistration(G, file, map) {
3407
3718
  const extension = posix.extname(file.path);
3408
3719
  const parts = file.path.slice(0, file.path.length - extension.length).split(/[/.]/).filter((part) => part !== "");
@@ -3434,6 +3745,7 @@ function loader(G, byPath) {
3434
3745
  }
3435
3746
  function runtime(G, options) {
3436
3747
  const { several, mapping } = options;
3748
+ const perFile = options.perFile ?? false;
3437
3749
  const files = [...options.files].map(([name, file]) => `[${luauString4(name)}] = ${luauString4(file)}`).join(", ");
3438
3750
  return `
3439
3751
  local ${G}
@@ -3645,32 +3957,60 @@ ${several ? `
3645
3957
  return map[key]
3646
3958
  end })
3647
3959
  end,` : ""}
3648
- records = {},${several ? `
3960
+ records = {},${several && !perFile ? `
3649
3961
  -- The file each module outside this one is in, as \`load\` is handed it.
3650
3962
  files = { ${files} },
3651
- loaded = {},${mapping ? `
3963
+ loaded = {},` : ""}${several && mapping ? `
3652
3964
  -- Every file's line map: by the module, and by the file's name.
3653
3965
  maps = {},
3654
- sources = {},` : ""}
3966
+ sources = {},` : ""}${several && !perFile ? `
3655
3967
  -- Another of the bundle's files: a function that adds its modules here.
3656
3968
  load = function(file)
3657
3969
  if ${G}.loaded[file] then
3658
3970
  return
3659
3971
  end
3660
3972
  ${G}.loaded[file] = true${loader(G, options.byPath)}
3973
+ end,` : ""}${perFile && mapping ? `
3974
+ -- What a file returns: its module's exports. The first file asked for
3975
+ -- runs under \`xpcall\`, so an error nothing caught is reported in the
3976
+ -- project's files; the files it requires in turn are inside that already.
3977
+ running = false,
3978
+ run = function(name)
3979
+ if ${G}.running then
3980
+ return ${G}.require(name)
3981
+ end
3982
+ ${G}.running = true
3983
+ local ok, result = xpcall(${G}.require, ${G}.fail, name)
3984
+ ${G}.running = false
3985
+ if not ok then
3986
+ error(result, 0)
3987
+ end
3988
+ return result
3661
3989
  end,` : ""}${options.imports ? `
3662
3990
  -- \`import("./x")\`: the module's exports, asked for where the code runs
3663
3991
  -- rather than before its module does. \`require\` does the rest \u2014 and
3664
3992
  -- loads the file the module is in, if it is not this one.
3665
- import = function(name)
3666
- return ${G}.require(name)
3993
+ import = function(name${perFile ? ", load" : ""})
3994
+ return ${G}.require(name${perFile ? ", load" : ""})
3667
3995
  end,` : ""}
3668
- require = function(name)
3996
+ require = function(name${perFile ? ", load" : ""})
3669
3997
  local record = ${G}.records[name]
3670
3998
  if record ~= nil then
3671
3999
  return record.exports
3672
4000
  end
3673
- local module = ${G}.modules[name]${several ? `
4001
+ local module = ${G}.modules[name]${perFile ? `
4002
+ -- Its file has not run: requiring it adds the module, and runs it.
4003
+ if module == nil and load ~= nil then
4004
+ load()
4005
+ record = ${G}.records[name]
4006
+ if record ~= nil then
4007
+ return record.exports
4008
+ end
4009
+ module = ${G}.modules[name]
4010
+ end
4011
+ if module == nil then
4012
+ error(("Cannot find module '%s': nothing has loaded the file it is in"):format(name), 2)
4013
+ end` : several ? `
3674
4014
  if module == nil then
3675
4015
  ${G}.load(${G}.files[name])
3676
4016
  module = ${G}.modules[name]
@@ -3758,6 +4098,35 @@ function importedSpecifiers(program2) {
3758
4098
  return program2.body.statements.flatMap((s) => s.type === "ImportStatement" || s.type === "ExportAllStatement" || s.type === "ExportNamedStatement" && s.source ? [s.source.value] : []);
3759
4099
  }
3760
4100
  var libraryCache = /* @__PURE__ */ new Map();
4101
+ var treeCache;
4102
+ function instanceTree(config, libs, noLib) {
4103
+ const tree = sourceMapOf(config);
4104
+ if (!tree) return void 0;
4105
+ if ("problem" in tree) return tree.named ? { problem: { file: config.path, message: tree.problem } } : void 0;
4106
+ const declares = (name) => libs.some((lib) => lib.body.statements.some((s) => s.type === "DeclareStatement" && s.name === name));
4107
+ if (!tree.named && !declares("game")) return void 0;
4108
+ if (treeCache && treeCache.text === tree.text && treeCache.libs.length === libs.length && treeCache.libs.every((lib, i) => lib === libs[i])) {
4109
+ return { types: treeCache.types };
4110
+ }
4111
+ const empty = parse("");
4112
+ const aliases = analyzeTypes(empty, analyzeScopes(empty, { noLib }), { libs, noLib, diagnostics: false }).aliases;
4113
+ const membersOf = (className) => {
4114
+ const type = aliases.get(className);
4115
+ return new Set(type?.kind === "object" ? type.properties.keys() : ["Name", "ClassName", "Parent", "Archivable"]);
4116
+ };
4117
+ const { types, problem } = sourceMapTypes(tree.text, tree.path, { classes: new Set(aliases.keys()), membersOf });
4118
+ if (!types) return tree.named && problem ? { problem: { file: tree.path, message: problem } } : void 0;
4119
+ const scripts = /* @__PURE__ */ new Map();
4120
+ const cached = {
4121
+ program: types.program,
4122
+ scriptFor: (file) => {
4123
+ if (!scripts.has(file)) scripts.set(file, types.scriptFor(file));
4124
+ return scripts.get(file);
4125
+ }
4126
+ };
4127
+ treeCache = { text: tree.text, libs: [...libs], types: cached, scripts };
4128
+ return { types: cached };
4129
+ }
3761
4130
  function parsedLibrary(file) {
3762
4131
  let stamp;
3763
4132
  try {
@@ -3776,11 +4145,15 @@ var moduleAnalyses = /* @__PURE__ */ new Map();
3776
4145
  function analyzeModules(modules, config) {
3777
4146
  const out = [];
3778
4147
  const analyses = /* @__PURE__ */ new Map();
3779
- const libraries = config ? resolveTypeLibraries(config) : { files: [], lowerings: [], problems: [] };
4148
+ const libraries = config ? resolveTypeLibraries(config) : { files: [], lowerings: [], options: [], problems: [] };
3780
4149
  for (const p of libraries.problems) out.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
3781
4150
  const libs = libraries.files.map(parsedLibrary);
3782
4151
  const noLib = config?.noLib ?? false;
3783
4152
  const checked = libs.length > 0 || !noLib;
4153
+ const tree = config ? instanceTree(config, libs, noLib) : void 0;
4154
+ if (tree?.problem) out.push({ file: tree.problem.file, message: tree.problem.message, line: 1, column: 1, category: "config" });
4155
+ if (tree?.types) libs.push(tree.types.program);
4156
+ const scriptOf = (file) => tree?.types && config ? tree.types.scriptFor(outputPathOf(config, file)) : void 0;
3784
4157
  const globals = libs.flatMap((lib) => lib.body.statements.flatMap((s) => s.type === "DeclareStatement" ? [s.name] : []));
3785
4158
  const byFile = new Map(modules.map((m) => [m.file, m]));
3786
4159
  const exportsCache = /* @__PURE__ */ new Map();
@@ -3793,7 +4166,9 @@ function analyzeModules(modules, config) {
3793
4166
  const done = analyzed.get(file);
3794
4167
  if (done) return done;
3795
4168
  const remembered = moduleAnalyses.get(file);
3796
- 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)) {
4169
+ const script = scriptOf(file);
4170
+ const ownLibs = script ? [...libs, script] : libs;
4171
+ if (remembered && remembered.setting === settingFor && ownLibs.length === remembered.libs.length && remembered.libs.every((lib, i) => lib === ownLibs[i]) && remembered.program === program2 && [...remembered.read].every(([read3, stamp]) => stampOf(read3) === stamp)) {
3797
4172
  analyzed.set(file, remembered.result);
3798
4173
  readWhile.set(file, remembered.read);
3799
4174
  return remembered.result;
@@ -3806,7 +4181,7 @@ function analyzeModules(modules, config) {
3806
4181
  }
3807
4182
  const before = partials;
3808
4183
  const types = analyzeTypes(program2, scopes, {
3809
- libs,
4184
+ libs: ownLibs,
3810
4185
  resolveModule: resolverFor(file),
3811
4186
  noLib,
3812
4187
  reportUnknownTypes: checked
@@ -3815,7 +4190,7 @@ function analyzeModules(modules, config) {
3815
4190
  const at = d.node;
3816
4191
  said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type", severity: d.severity });
3817
4192
  }
3818
- const result2 = { types, diagnostics: said };
4193
+ const result2 = { types, diagnostics: said, scopes };
3819
4194
  if (partials === before) {
3820
4195
  analyzed.set(file, result2);
3821
4196
  const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
@@ -3823,7 +4198,7 @@ function analyzeModules(modules, config) {
3823
4198
  if (own !== void 0) read3.set(file, own);
3824
4199
  readWhile.set(file, read3);
3825
4200
  if ([...read3.values()].every((stamp) => stamp !== void 0)) {
3826
- moduleAnalyses.set(file, { setting: settingFor, libs: [...libs], program: program2, read: read3, result: result2 });
4201
+ moduleAnalyses.set(file, { setting: settingFor, libs: [...ownLibs], program: program2, read: read3, result: result2 });
3827
4202
  }
3828
4203
  }
3829
4204
  return result2;
@@ -3872,7 +4247,16 @@ function analyzeModules(modules, config) {
3872
4247
  analyses.set(module.file, types);
3873
4248
  out.push(...diagnostics);
3874
4249
  }
3875
- return { types: analyses, diagnostics: out, lowerings: [...libraries.lowerings] };
4250
+ const exportsOf = (file) => {
4251
+ const cached = exportsCache.get(file);
4252
+ if (cached) return cached;
4253
+ const module = byFile.get(file);
4254
+ const { types, scopes } = analyzeModule(file, module.program);
4255
+ const exports = moduleExports(module.program, scopes, types, resolverFor(file));
4256
+ exportsCache.set(file, exports);
4257
+ return exports;
4258
+ };
4259
+ return { types: analyses, diagnostics: out, lowerings: [...libraries.lowerings], exportsOf };
3876
4260
  }
3877
4261
  function allStatements(statements) {
3878
4262
  const out = [];
@@ -3894,7 +4278,11 @@ function allStatements(statements) {
3894
4278
  export {
3895
4279
  resolveConfig,
3896
4280
  program,
4281
+ syntaxOf,
3897
4282
  lower,
4283
+ printOptions,
4284
+ printOutput,
3898
4285
  loadLowerings,
4286
+ WRITTEN_BY_TILUA,
3899
4287
  bundle
3900
4288
  };