@tilua/compiler 8.0.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,
@@ -2804,7 +2813,7 @@ import {
2804
2813
  LexError,
2805
2814
  luauString as luauString4
2806
2815
  } from "@tilua/parser";
2807
- import { parse as parseLuau2, parseExpressionFromSource as parseExpressionFromSource2, print } from "luau-parser";
2816
+ import { parse as parseLuau2, parseExpressionFromSource as parseExpressionFromSource2, print as print2 } from "luau-parser";
2808
2817
 
2809
2818
  // src/treeshake.ts
2810
2819
  function walk(node, visit, parent) {
@@ -3072,23 +3081,29 @@ function ownExport(f, name) {
3072
3081
  // src/linemap.ts
3073
3082
  import { luauString as luauString3 } from "@tilua/parser";
3074
3083
  function statementsOf(node) {
3075
- const out = [];
3076
- const visit = (value) => {
3084
+ return statementTree(node).statements;
3085
+ }
3086
+ function statementTree(node) {
3087
+ const statements = [];
3088
+ const parents = [];
3089
+ const visit = (value, parent) => {
3077
3090
  if (!value || typeof value !== "object") return;
3078
- if (Array.isArray(value)) return void value.forEach(visit);
3091
+ if (Array.isArray(value)) return void value.forEach((item) => visit(item, parent));
3079
3092
  const record = value;
3093
+ let inside = parent;
3080
3094
  if (typeof record.type === "string" && record.type.endsWith("Statement")) {
3081
- out.push(value);
3095
+ inside = statements.push(value) - 1;
3096
+ parents.push(parent);
3082
3097
  }
3083
3098
  for (const [key, child] of Object.entries(record)) {
3084
- if (key !== "line" && key !== "column") visit(child);
3099
+ if (key !== "line" && key !== "column") visit(child, inside);
3085
3100
  }
3086
3101
  };
3087
- visit(node);
3088
- return out;
3102
+ visit(node, -1);
3103
+ return { statements, parents };
3089
3104
  }
3090
3105
  function buildLineMap(emitted, printed, origins) {
3091
- const before = statementsOf(emitted);
3106
+ const { statements: before, parents } = statementTree(emitted);
3092
3107
  const after = statementsOf(printed);
3093
3108
  if (before.length !== after.length) return /* @__PURE__ */ new Map();
3094
3109
  const map = /* @__PURE__ */ new Map();
@@ -3099,26 +3114,102 @@ function buildLineMap(emitted, printed, origins) {
3099
3114
  const line = after[i].line.start;
3100
3115
  if (!map.has(line)) map.set(line, origin);
3101
3116
  }
3117
+ const holds = /* @__PURE__ */ new Set();
3118
+ for (let i = 0; i < before.length; i++) {
3119
+ if (!origins.has(before[i])) continue;
3120
+ for (let up = parents[i]; up >= 0 && !holds.has(up); up = parents[up]) holds.add(up);
3121
+ }
3122
+ for (let i = 0; i < before.length; i++) {
3123
+ const origin = origins.get(before[i]);
3124
+ if (!origin || holds.has(i)) continue;
3125
+ for (let line = after[i].line.start + 1; line <= after[i].line.end; line++) {
3126
+ if (!map.has(line)) map.set(line, origin);
3127
+ }
3128
+ }
3102
3129
  return map;
3103
3130
  }
3104
- function lineMapSource(map) {
3131
+ var VLQ_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3132
+ function vlq(n) {
3133
+ let value = n < 0 ? -n * 2 + 1 : n * 2;
3134
+ let out = "";
3135
+ do {
3136
+ const digit = value % 32;
3137
+ value = Math.floor(value / 32);
3138
+ out += VLQ_DIGITS[value > 0 ? digit + 32 : digit];
3139
+ } while (value > 0);
3140
+ return out;
3141
+ }
3142
+ function lineMapSource(map, unpack) {
3105
3143
  const entries = [...map.entries()].sort((a, b) => a[0] - b[0]);
3106
- const quoted = (s) => luauString3(s.replace(/\\/g, "/"));
3107
- return `{${entries.map(([line, o]) => `[${line}]={${quoted(o.file)},${o.line}}`).join(",")}}`;
3144
+ const files = [];
3145
+ const indexes = /* @__PURE__ */ new Map();
3146
+ let data = "";
3147
+ let line = 0, file = 0, source = 0;
3148
+ for (const [at, origin] of entries) {
3149
+ const name = origin.file.replace(/\\/g, "/");
3150
+ if (!indexes.has(name)) indexes.set(name, files.push(name));
3151
+ const index2 = indexes.get(name);
3152
+ data += vlq(at - line) + vlq(index2 - file) + vlq(origin.line - source);
3153
+ line = at;
3154
+ file = index2;
3155
+ source = origin.line;
3156
+ }
3157
+ return `${unpack}({${files.map(luauString3).join(",")}},${luauString3(data)})`;
3158
+ }
3159
+
3160
+ // src/output.ts
3161
+ import { print, PrintError } from "luau-parser";
3162
+ var WRITTEN = /* @__PURE__ */ new Set([
3163
+ "compoundAssignment",
3164
+ "continue",
3165
+ "ifExpression",
3166
+ "floorDivision",
3167
+ "attributes",
3168
+ "numberLiterals"
3169
+ ]);
3170
+ function printOptions(syntax, format = {}) {
3171
+ return {
3172
+ syntax: [...syntax].some((s) => WRITTEN.has(s)) ? "luau" : "lua51",
3173
+ compact: format.minify ?? false,
3174
+ ...format.indent !== void 0 && { indent: format.indent }
3175
+ };
3176
+ }
3177
+ function printOutput(node, options) {
3178
+ try {
3179
+ return { code: print(node, options) };
3180
+ } catch (error) {
3181
+ if (!(error instanceof PrintError)) throw error;
3182
+ return {
3183
+ 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"`
3184
+ };
3185
+ }
3108
3186
  }
3109
3187
 
3110
3188
  // src/bundle.ts
3189
+ var WRITTEN_BY_TILUA = "-- Written by tilua";
3111
3190
  async function bundle(options) {
3112
- const entry = resolve2(options.entry);
3113
3191
  const diagnostics = [];
3192
+ const from = options.entry !== void 0 ? resolve2(options.entry) : resolve2(options.root ?? process.cwd(), "tilua.config.json");
3114
3193
  const inline = options.config !== void 0 && typeof options.config !== "string";
3115
- const { config, problems } = resolveConfig(options.config, entry, options.root ?? (inline ? dirname(entry) : process.cwd()));
3194
+ const { config, problems } = resolveConfig(options.config, from, options.root ?? (inline ? dirname(from) : process.cwd()));
3116
3195
  for (const p of problems) diagnostics.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
3117
- const root = resolve2(options.root ?? config?.directory ?? dirname(entry));
3196
+ const root = resolve2(options.root ?? config?.directory ?? dirname(from));
3118
3197
  const nameOf = (file) => relative(root, file).replace(/\\/g, "/").replace(/\.tilua$/, "");
3198
+ const mode = options.output ?? config?.output ?? "single";
3199
+ const perFile = mode === "files";
3200
+ const rootDir = resolve2(config?.rootDir ?? root);
3201
+ const entry = options.entry !== void 0 ? resolve2(options.entry) : void 0;
3202
+ const anchor = entry ?? config?.path ?? from;
3203
+ const stop = (message, file = anchor) => ({
3204
+ files: [],
3205
+ modules: [],
3206
+ diagnostics: [...diagnostics, { file, message, line: 1, column: 1, category: "config" }]
3207
+ });
3208
+ if (!perFile && entry === void 0) return stop('No entry to build: only "output": "files" builds without one');
3119
3209
  const sources = /* @__PURE__ */ new Map();
3120
3210
  const directives = /* @__PURE__ */ new Map();
3121
- const queue = [entry];
3211
+ const queue = perFile ? tiluaFiles(rootDir) : [entry];
3212
+ if (perFile && !queue.length) return stop(`No .tilua file under ${relative(process.cwd(), rootDir) || "."}`);
3122
3213
  while (queue.length) {
3123
3214
  const file = queue.shift();
3124
3215
  if (sources.has(file)) continue;
@@ -3136,6 +3227,22 @@ async function bundle(options) {
3136
3227
  if (target && !target.endsWith(".d.tilua")) queue.push(target);
3137
3228
  }
3138
3229
  }
3230
+ if (perFile) {
3231
+ const outside = [...sources.keys()].filter((file) => isOutside(rootDir, file));
3232
+ if (outside.length) {
3233
+ return {
3234
+ files: [],
3235
+ modules: [],
3236
+ diagnostics: [...diagnostics, ...outside.map((file) => ({
3237
+ file,
3238
+ message: `This file is imported, but it is outside rootDir (${relative(root, rootDir) || "."}), where every file of "files" output has to be`,
3239
+ line: 1,
3240
+ column: 1,
3241
+ category: "module"
3242
+ }))]
3243
+ };
3244
+ }
3245
+ }
3139
3246
  const analysis = analyzeModules([...sources.values()], config);
3140
3247
  const { lowerings, problems: loweringProblems } = await loadLowerings(analysis.lowerings);
3141
3248
  for (const p of loweringProblems) {
@@ -3146,18 +3253,56 @@ async function bundle(options) {
3146
3253
  const G = names.fresh("G");
3147
3254
  const requireExpression = member(identifier(G), "require");
3148
3255
  const importExpression = member(identifier(G), "import");
3149
- const mode = options.output ?? config?.output ?? "single";
3150
3256
  const byPath = syntaxOf(lowerings).has("requireByPath");
3257
+ if (perFile && !byPath) {
3258
+ 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);
3259
+ }
3260
+ const outputPath = (file) => relative(rootDir, file).replace(/\\/g, "/").replace(/\.tilua$/, ".luau");
3261
+ const fileOfModule = new Map([...sources.values()].map((s) => [s.name, s.file]));
3262
+ const outDir = resolve2(options.outDir ?? config?.outDir ?? rootDir);
3263
+ const written = (file) => join(outDir, outputPath(file));
3264
+ let host;
3265
+ let runtimeFile = join(outDir, "_tilua.luau");
3266
+ if (perFile) {
3267
+ const outputs = [...sources.keys()].map(written);
3268
+ const taken = new Set(outputs.map(pathKey));
3269
+ for (let n = 2; taken.has(pathKey(runtimeFile)); n++) runtimeFile = join(outDir, `_tilua${n}.luau`);
3270
+ for (let i = lowerings.length - 1; i >= 0 && !host; i--) {
3271
+ const { plugin: plugin3, options: own } = lowerings[i];
3272
+ host = plugin3.files?.({ options: own ?? {}, directory: config?.directory ?? root, outDir, files: outputs, runtime: runtimeFile });
3273
+ }
3274
+ for (const problem of host?.problems ?? []) {
3275
+ diagnostics.push({
3276
+ file: problem.file ?? config?.path ?? anchor,
3277
+ message: problem.message,
3278
+ line: 1,
3279
+ column: 1,
3280
+ category: "config",
3281
+ ...problem.severity === "warning" && { severity: "warning" }
3282
+ });
3283
+ }
3284
+ if (host?.runtime !== void 0) {
3285
+ if (isOutside(outDir, resolve2(host.runtime))) return stop(`The runtime has to go inside outDir, not at ${host.runtime}`);
3286
+ if (taken.has(pathKey(host.runtime))) return stop(`The runtime cannot go at ${host.runtime}: a module's file is there`);
3287
+ runtimeFile = resolve2(host.runtime);
3288
+ }
3289
+ }
3290
+ const placeOf = (file) => host?.place?.(file) ?? placeInFolder(relative(outDir, file));
3291
+ const requireFrom = (from2, to) => pathBetween(placeOf(from2), placeOf(to), host?.root);
3292
+ const isScript = (file) => host?.script?.(file) ?? false;
3151
3293
  const modules = /* @__PURE__ */ new Map();
3152
3294
  const origins = /* @__PURE__ */ new WeakMap();
3153
3295
  const shared = new SharedRuntime(names);
3154
3296
  const shaking = options.treeShaking ?? config?.treeShaking ?? false;
3297
+ const format = { indent: options.indent ?? config?.indent, minify: options.minify ?? config?.minify ?? false };
3298
+ const printing = printOptions(syntaxOf(lowerings), format);
3299
+ const mapping = !format.minify && (options.lineMap ?? config?.lineMap ?? true);
3155
3300
  const resolveIn = (file) => (specifier) => {
3156
3301
  const found = resolveModulePath(file, specifier, config);
3157
3302
  return found !== void 0 && sources.has(found) ? found : void 0;
3158
3303
  };
3159
- const shaken = shaking ? treeShake([...sources.values()].map((s) => ({ file: s.file, program: s.program, scopes: s.scopes, resolve: resolveIn(s.file) })), entry) : void 0;
3160
- const pending = [entry];
3304
+ 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;
3305
+ const pending = perFile ? [...sources.keys()] : [entry];
3161
3306
  while (pending.length) {
3162
3307
  const file = pending.shift();
3163
3308
  if (modules.has(file)) continue;
@@ -3190,9 +3335,16 @@ async function bundle(options) {
3190
3335
  import: importExpression,
3191
3336
  // One file has one line map. Several have one each, and a
3192
3337
  // module reads the one of the file it is in.
3193
- lines: mode === "single" ? `${G}.lines` : `${G}.maps[${luauString4(source.name)}]`,
3194
- fail: `${G}.fail`,
3338
+ lines: !mapping ? void 0 : mode === "single" ? `${G}.lines` : `${G}.maps[${luauString4(source.name)}]`,
3339
+ fail: mapping ? `${G}.fail` : void 0,
3195
3340
  arrays: `${G}.arrays`,
3341
+ // The file a module is in, required from this one by its
3342
+ // path, written out.
3343
+ loader: !perFile ? void 0 : (name) => {
3344
+ const target = fileOfModule.get(name);
3345
+ if (target === void 0) return void 0;
3346
+ return parseExpressionFromSource2(`function() return require(${luauString4(requireFrom(written(file), written(target)))}) end`);
3347
+ },
3196
3348
  resolve: (specifier, dynamic) => {
3197
3349
  const found = resolveModulePath(file, specifier, config);
3198
3350
  if (!found || found.endsWith(".d.tilua") || !sources.has(found)) return void 0;
@@ -3244,6 +3396,102 @@ async function bundle(options) {
3244
3396
  return { files: [], modules: moduleNames, diagnostics };
3245
3397
  }
3246
3398
  if (shaking) shared.shake(lowered.flatMap((m) => m.statements));
3399
+ const unwritable = (problem) => ({
3400
+ files: [],
3401
+ modules: moduleNames,
3402
+ diagnostics: [...diagnostics, { file: anchor, message: problem, line: 1, column: 1, category: "config" }]
3403
+ });
3404
+ const write = (node) => {
3405
+ const printed2 = printOutput(node, printing);
3406
+ return "problem" in printed2 ? printed2 : printed2.code;
3407
+ };
3408
+ if (perFile) {
3409
+ const imported = [];
3410
+ for (const [file, source] of sources) {
3411
+ for (const { specifier, line, column } of importSites(source.program)) {
3412
+ const target = resolveIn(file)(specifier);
3413
+ if (target === void 0 || !isScript(written(target))) continue;
3414
+ imported.push({
3415
+ file,
3416
+ line,
3417
+ column,
3418
+ category: "module",
3419
+ message: `'${specifier}' is a script the game runs, which nothing can import: move what it shares into a module of its own`
3420
+ });
3421
+ }
3422
+ }
3423
+ if (imported.length) return { files: [], modules: moduleNames, diagnostics: [...diagnostics, ...imported] };
3424
+ const runtimePath = relative(outDir, runtimeFile).replace(/\\/g, "/");
3425
+ const files2 = [];
3426
+ for (const [file, module] of modules) {
3427
+ const path = outputPath(file);
3428
+ const at = written(file);
3429
+ const script = isScript(at);
3430
+ const prelude = host?.prelude?.(at);
3431
+ const body = [
3432
+ ...prelude !== void 0 ? parseLuau2(prelude).body.statements : [],
3433
+ ...parseLuau2(`local ${G} = require(${luauString4(requireFrom(at, runtimeFile))})`).body.statements
3434
+ ];
3435
+ for (let i = 0; i < module.shared.length; i += 100) {
3436
+ const some = module.shared.slice(i, i + 100);
3437
+ body.push(local(some, some.map((name) => member(member(identifier(G), "shared"), name))));
3438
+ }
3439
+ if (module.usesScriptArgs) body.push(...parseLuau2(`local scriptArgs = ${G}.scriptArgs`).body.statements);
3440
+ body.push(assign(
3441
+ [index(member(identifier(G), "modules"), string(module.name))],
3442
+ [moduleTable(module)]
3443
+ ));
3444
+ const value = `${G}.${mapping ? "run" : "require"}(${luauString4(module.name)})`;
3445
+ const returned2 = module.info.exports && !script ? entryReturn(lowerings, module.name, value, () => analysis.exportsOf(file)) : { statements: [], value };
3446
+ if ("problem" in returned2) return stop(returned2.problem.message, file);
3447
+ const ending = parseLuau2(`${returned2.statements.join("\n")}
3448
+ return ${returned2.value}`).body.statements;
3449
+ const chunk = program(body);
3450
+ if (mapping) {
3451
+ const text = write(chunk);
3452
+ if (typeof text !== "string") return unwritable(text.problem);
3453
+ const lines = buildLineMap(chunk, parseLuau2(text), origins);
3454
+ const named = host?.place?.(at) ? `${placeOf(at).join("/")}.luau` : path;
3455
+ body.push(...mapRegistration(G, { path: named, modules: [module] }, lineMapSource(lines, `${G}.lineMap`)));
3456
+ }
3457
+ body.push(...ending);
3458
+ const code = write(chunk);
3459
+ if (typeof code !== "string") return unwritable(code.problem);
3460
+ const from2 = relative(dirname(at), file).replace(/\\/g, "/");
3461
+ files2.push({ path, code: `${code}
3462
+ ${WRITTEN_BY_TILUA} from ${from2}: edit that file, not this one.
3463
+ `, modules: [module.name] });
3464
+ }
3465
+ const runtimeProgram = parseLuau2(runtime(G, {
3466
+ several: true,
3467
+ perFile: true,
3468
+ byPath,
3469
+ files: /* @__PURE__ */ new Map(),
3470
+ imports: lowered.some((m) => m.dynamicImports.size > 0),
3471
+ mapping
3472
+ }));
3473
+ const helpers2 = shared.chunk(new Set(lowered.flatMap((m) => m.shared)));
3474
+ const helperNames = helpers2.declare.flatMap((statement) => statement.type === "LocalStatement" ? statement.names.map((n) => n.name) : []);
3475
+ runtimeProgram.body.statements.unshift(...helpers2.declare);
3476
+ runtimeProgram.body.statements.push(...helpers2.define);
3477
+ if (helperNames.length) {
3478
+ runtimeProgram.body.statements.push(assign(
3479
+ [member(identifier(G), "shared")],
3480
+ [table(helperNames.map((name) => field(name, identifier(name))))]
3481
+ ));
3482
+ }
3483
+ if (lowered.some((m) => m.usesScriptArgs)) {
3484
+ runtimeProgram.body.statements.push(...parseLuau2(`${G}.scriptArgs = {}
3485
+ ${G}.arrays[${G}.scriptArgs] = true`).body.statements);
3486
+ }
3487
+ runtimeProgram.body.statements.push(returns([identifier(G)]));
3488
+ const runtimeCode = write(runtimeProgram);
3489
+ if (typeof runtimeCode !== "string") return unwritable(runtimeCode.problem);
3490
+ files2.push({ path: runtimePath, code: `${runtimeCode}
3491
+ ${WRITTEN_BY_TILUA}: the runtime every file of the project requires.
3492
+ `, modules: [] });
3493
+ return { code: files2[0].code, files: files2, modules: moduleNames, diagnostics };
3494
+ }
3247
3495
  const several = mode !== "single";
3248
3496
  const layout = layoutOf(mode, modules.get(entry), lowered, options.outFile);
3249
3497
  const usesScriptArgs = lowered.some((m) => m.usesScriptArgs);
@@ -3255,7 +3503,8 @@ async function bundle(options) {
3255
3503
  several,
3256
3504
  byPath,
3257
3505
  files: elsewhere,
3258
- imports: lowered.some((m) => m.dynamicImports.size > 0)
3506
+ imports: lowered.some((m) => m.dynamicImports.size > 0),
3507
+ mapping
3259
3508
  }));
3260
3509
  const assignment = program2.body.statements[1];
3261
3510
  if (usesScriptArgs) {
@@ -3272,27 +3521,38 @@ async function bundle(options) {
3272
3521
  program2.body.statements.unshift(...helpers.declare);
3273
3522
  const ok = names.fresh("ok");
3274
3523
  const result2 = names.fresh("result");
3275
- const entryStatements = parseLuau2(`
3524
+ const entryName = luauString4(sources.get(entry).name);
3525
+ const returnsExports = modules.get(entry).info.exports;
3526
+ const returned = returnsExports ? entryReturn(lowerings, sources.get(entry).name, mapping ? result2 : `${G}.require(${entryName})`, () => analysis.exportsOf(entry)) : void 0;
3527
+ if (returned && "problem" in returned) return stop(returned.problem.message);
3528
+ const declarations = returned?.statements.join("\n") ?? "";
3529
+ const entryStatements = parseLuau2(!mapping ? `${declarations}
3530
+ ${returned ? `return ${returned.value}` : `${G}.require(${entryName})`}` : `
3276
3531
  local ${ok}, ${result2} = xpcall(function()
3277
- return ${G}.require(${luauString4(sources.get(entry).name)})
3532
+ return ${G}.require(${entryName})
3278
3533
  end, ${G}.fail)
3279
3534
  if not ${ok} then
3280
3535
  error(${result2}, 0)
3281
3536
  end
3282
- ${modules.get(entry).info.exports ? `return ${result2}` : ""}
3537
+ ${declarations}
3538
+ ${returned ? `return ${returned.value}` : ""}
3283
3539
  `).body.statements;
3284
3540
  program2.body.statements.push(...entryStatements);
3285
- const map = buildLineMap(program2, parseLuau2(print(program2)), origins);
3286
- if (map.size || several) {
3541
+ let printed = write(program2);
3542
+ if (typeof printed !== "string") return unwritable(printed.problem);
3543
+ const map = mapping ? buildLineMap(program2, parseLuau2(printed), origins) : /* @__PURE__ */ new Map();
3544
+ if (mapping && (map.size || several)) {
3287
3545
  const mapStatements = [assign(
3288
3546
  [member(identifier(G), "lines")],
3289
- [parseExpressionFromSource2(lineMapSource(map))]
3547
+ [parseExpressionFromSource2(lineMapSource(map, `${G}.lineMap`))]
3290
3548
  )];
3291
3549
  if (several) mapStatements.push(...mapRegistration(G, layout[0], `${G}.lines`));
3292
3550
  program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...mapStatements);
3293
3551
  }
3294
3552
  program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...helpers.define);
3295
- const files = [{ path: layout[0].path, code: print(program2) + "\n", modules: layout[0].modules.map((m) => m.name) }];
3553
+ printed = write(program2);
3554
+ if (typeof printed !== "string") return unwritable(printed.problem);
3555
+ const files = [{ path: layout[0].path, code: printed + "\n", modules: layout[0].modules.map((m) => m.name) }];
3296
3556
  for (const file of layout.slice(1)) {
3297
3557
  const fileHelpers = shared.chunk(new Set(file.modules.flatMap((m) => m.shared)));
3298
3558
  const body = [...fileHelpers.declare];
@@ -3304,12 +3564,35 @@ ${modules.get(entry).info.exports ? `return ${result2}` : ""}
3304
3564
  ));
3305
3565
  }
3306
3566
  const chunk = program([returns([functionExpression(functionBody([G], body))])]);
3307
- const lines = buildLineMap(chunk, parseLuau2(print(chunk)), origins);
3308
- body.push(...mapRegistration(G, file, lineMapSource(lines)), ...fileHelpers.define);
3309
- files.push({ path: file.path, code: print(chunk) + "\n", modules: file.modules.map((m) => m.name) });
3567
+ if (mapping) {
3568
+ const lines = buildLineMap(chunk, parseLuau2(print2(chunk, printing)), origins);
3569
+ body.push(...mapRegistration(G, file, lineMapSource(lines, `${G}.lineMap`)));
3570
+ }
3571
+ body.push(...fileHelpers.define);
3572
+ const code = write(chunk);
3573
+ if (typeof code !== "string") return unwritable(code.problem);
3574
+ files.push({ path: file.path, code: code + "\n", modules: file.modules.map((m) => m.name) });
3310
3575
  }
3311
3576
  return { code: files[0].code, files, modules: moduleNames, diagnostics };
3312
3577
  }
3578
+ function entryReturn(lowerings, module, value, exports) {
3579
+ const syntax = [...syntaxOf(lowerings)];
3580
+ for (let i = lowerings.length - 1; i >= 0; i--) {
3581
+ const { plugin: plugin3, from, options } = lowerings[i];
3582
+ if (!plugin3.entryExports) continue;
3583
+ const answer = plugin3.entryExports({ options: options ?? {}, module, value, exports: exports(), syntax });
3584
+ if (!answer) continue;
3585
+ const statements = [...answer.statements ?? []];
3586
+ try {
3587
+ parseLuau2(`${statements.join("\n")}
3588
+ return ${answer.value}`);
3589
+ } catch (error) {
3590
+ return { problem: { message: `'${from}' answered entryExports with Luau that does not parse: ${error.message}` } };
3591
+ }
3592
+ return { statements, value: answer.value };
3593
+ }
3594
+ return { statements: [], value };
3595
+ }
3313
3596
  function moduleTable(module) {
3314
3597
  const { names: exported, links, stars } = module.info;
3315
3598
  const fields = [];
@@ -3329,10 +3612,6 @@ function layoutOf(mode, entry, modules, outFile) {
3329
3612
  const entryStem = outFile !== void 0 ? basename(outFile, extname(outFile)) : posix.basename(entry.name);
3330
3613
  const others = modules.filter((m) => m !== entry);
3331
3614
  if (mode === "single") return [{ path: `${entryStem}${extension}`, modules: [entry, ...others] }];
3332
- if (mode === "modules") {
3333
- const pathOf = (m) => `${m.name.split("/").map((part) => part === ".." ? "__" : part).join("/")}${extension}`;
3334
- return [entry, ...others].map((m) => ({ path: pathOf(m), modules: [m] }));
3335
- }
3336
3615
  const byName = new Map(modules.map((m) => [m.name, m]));
3337
3616
  const reach = (root) => {
3338
3617
  const seen = /* @__PURE__ */ new Set();
@@ -3368,11 +3647,70 @@ function layoutOf(mode, entry, modules, outFile) {
3368
3647
  return files;
3369
3648
  }
3370
3649
  function loadPath(entryPath, file, byPath) {
3371
- const path = posix.relative(posix.dirname(entryPath), file);
3372
- if (!byPath) return path;
3650
+ return byPath ? requirePath(entryPath, file) : posix.relative(posix.dirname(entryPath), file);
3651
+ }
3652
+ function placeInFolder(path) {
3653
+ const parts = path.replace(/\\/g, "/").replace(/\.luau?$/, "").split("/").filter((part) => part !== "");
3654
+ if (parts.at(-1) === "init") parts.pop();
3655
+ return parts;
3656
+ }
3657
+ function pathBetween(from, to, root) {
3658
+ if (root !== void 0 && from[0] !== to[0]) return `${root}/${to.join("/")}`;
3659
+ if (to.length > from.length && from.every((part, i) => to[i] === part)) return `@self/${to.slice(from.length).join("/")}`;
3660
+ const base = from.slice(0, -1);
3661
+ let shared = 0;
3662
+ while (shared < base.length && shared < to.length - 1 && base[shared] === to[shared]) shared++;
3663
+ const up = base.length - shared;
3664
+ return `${up === 0 ? "./" : "../".repeat(up)}${to.slice(shared).join("/")}`;
3665
+ }
3666
+ function importSites(program2) {
3667
+ const sites = [];
3668
+ for (const statement of program2.body.statements) {
3669
+ if (statement.type === "ImportStatement" || statement.type === "ExportAllStatement" || statement.type === "ExportNamedStatement" && statement.source) {
3670
+ const source = statement.source;
3671
+ sites.push({ specifier: source.value, line: source.line.start, column: source.column.start });
3672
+ }
3673
+ }
3674
+ for (const node of importExpressions(program2)) {
3675
+ if (node.source.type === "StringLiteral") sites.push({ specifier: node.source.value, line: node.source.line.start, column: node.source.column.start });
3676
+ }
3677
+ return sites;
3678
+ }
3679
+ function pathKey(path) {
3680
+ const full = resolve2(path);
3681
+ return process.platform === "win32" ? full.toLowerCase() : full;
3682
+ }
3683
+ function requirePath(from, to) {
3684
+ const path = posix.relative(posix.dirname(from), to);
3373
3685
  const bare = path.slice(0, path.length - posix.extname(path).length);
3374
3686
  return bare.startsWith("../") ? bare : `./${bare}`;
3375
3687
  }
3688
+ function tiluaFiles(folder) {
3689
+ const found = [];
3690
+ const walk2 = (at) => {
3691
+ let entries;
3692
+ try {
3693
+ entries = readdirSync(at, { withFileTypes: true });
3694
+ } catch {
3695
+ return;
3696
+ }
3697
+ for (const item of entries) {
3698
+ if (item.name.startsWith(".")) continue;
3699
+ const path = join(at, item.name);
3700
+ if (item.isDirectory()) {
3701
+ if (item.name !== "node_modules") walk2(path);
3702
+ } else if (item.name.endsWith(".tilua") && !item.name.endsWith(".d.tilua")) {
3703
+ found.push(path);
3704
+ }
3705
+ }
3706
+ };
3707
+ walk2(folder);
3708
+ return found.sort();
3709
+ }
3710
+ function isOutside(folder, file) {
3711
+ const path = relative(folder, file);
3712
+ return path === "" || path.startsWith("..") || isAbsolute(path);
3713
+ }
3376
3714
  function mapRegistration(G, file, map) {
3377
3715
  const extension = posix.extname(file.path);
3378
3716
  const parts = file.path.slice(0, file.path.length - extension.length).split(/[/.]/).filter((part) => part !== "");
@@ -3403,7 +3741,8 @@ function loader(G, byPath) {
3403
3741
  require(file)(${G})`;
3404
3742
  }
3405
3743
  function runtime(G, options) {
3406
- const { several } = options;
3744
+ const { several, mapping } = options;
3745
+ const perFile = options.perFile ?? false;
3407
3746
  const files = [...options.files].map(([name, file]) => `[${luauString4(name)}] = ${luauString4(file)}`).join(", ");
3408
3747
  return `
3409
3748
  local ${G}
@@ -3411,7 +3750,7 @@ ${G} = {
3411
3750
  modules = {},
3412
3751
  -- Every array the program made, so \`typeof\` can tell one from an object.
3413
3752
  -- The keys are held weakly: being written down keeps no array alive.
3414
- arrays = setmetatable({}, { __mode = "k" }),
3753
+ arrays = setmetatable({}, { __mode = "k" }),${mapping ? `
3415
3754
  -- Filled in just before the entry runs: bundle line -> { file, line }.
3416
3755
  lines = nil,
3417
3756
  -- What an error nothing caught says, in the project's files: the bundle's
@@ -3579,32 +3918,96 @@ ${several ? `
3579
3918
  end
3580
3919
  return mapped .. "\\n" .. table.concat(frames, "\\n")
3581
3920
  end,
3582
- records = {},${several ? `
3921
+ -- A line map as it is written: the files it names, and base-64 VLQ
3922
+ -- numbers, three to a line \u2014 how far down the bundle, which file, how far
3923
+ -- down that file, each counted from the line before. It is read out the
3924
+ -- first time a line is looked up, which is when something went wrong.
3925
+ lineMap = function(files, data)
3926
+ local map = {}
3927
+ return setmetatable(map, { __index = function(_, key)
3928
+ setmetatable(map, nil)
3929
+ local digits = {}
3930
+ for i = 1, 64 do
3931
+ digits[("${VLQ_DIGITS}"):byte(i)] = i - 1
3932
+ end
3933
+ local at = 1
3934
+ local function read()
3935
+ local value, scale = 0, 1
3936
+ repeat
3937
+ local digit = digits[data:byte(at)]
3938
+ at = at + 1
3939
+ value = value + digit % 32 * scale
3940
+ scale = scale * 32
3941
+ until digit < 32
3942
+ if value % 2 == 1 then
3943
+ return -math.floor(value / 2)
3944
+ end
3945
+ return math.floor(value / 2)
3946
+ end
3947
+ local line, file, source = 0, 0, 0
3948
+ while at <= #data do
3949
+ line = line + read()
3950
+ file = file + read()
3951
+ source = source + read()
3952
+ map[line] = { files[file], source }
3953
+ end
3954
+ return map[key]
3955
+ end })
3956
+ end,` : ""}
3957
+ records = {},${several && !perFile ? `
3583
3958
  -- The file each module outside this one is in, as \`load\` is handed it.
3584
3959
  files = { ${files} },
3585
- loaded = {},
3960
+ loaded = {},` : ""}${several && mapping ? `
3586
3961
  -- Every file's line map: by the module, and by the file's name.
3587
3962
  maps = {},
3588
- sources = {},
3963
+ sources = {},` : ""}${several && !perFile ? `
3589
3964
  -- Another of the bundle's files: a function that adds its modules here.
3590
3965
  load = function(file)
3591
3966
  if ${G}.loaded[file] then
3592
3967
  return
3593
3968
  end
3594
3969
  ${G}.loaded[file] = true${loader(G, options.byPath)}
3970
+ end,` : ""}${perFile && mapping ? `
3971
+ -- What a file returns: its module's exports. The first file asked for
3972
+ -- runs under \`xpcall\`, so an error nothing caught is reported in the
3973
+ -- project's files; the files it requires in turn are inside that already.
3974
+ running = false,
3975
+ run = function(name)
3976
+ if ${G}.running then
3977
+ return ${G}.require(name)
3978
+ end
3979
+ ${G}.running = true
3980
+ local ok, result = xpcall(${G}.require, ${G}.fail, name)
3981
+ ${G}.running = false
3982
+ if not ok then
3983
+ error(result, 0)
3984
+ end
3985
+ return result
3595
3986
  end,` : ""}${options.imports ? `
3596
3987
  -- \`import("./x")\`: the module's exports, asked for where the code runs
3597
3988
  -- rather than before its module does. \`require\` does the rest \u2014 and
3598
3989
  -- loads the file the module is in, if it is not this one.
3599
- import = function(name)
3600
- return ${G}.require(name)
3990
+ import = function(name${perFile ? ", load" : ""})
3991
+ return ${G}.require(name${perFile ? ", load" : ""})
3601
3992
  end,` : ""}
3602
- require = function(name)
3993
+ require = function(name${perFile ? ", load" : ""})
3603
3994
  local record = ${G}.records[name]
3604
3995
  if record ~= nil then
3605
3996
  return record.exports
3606
3997
  end
3607
- local module = ${G}.modules[name]${several ? `
3998
+ local module = ${G}.modules[name]${perFile ? `
3999
+ -- Its file has not run: requiring it adds the module, and runs it.
4000
+ if module == nil and load ~= nil then
4001
+ load()
4002
+ record = ${G}.records[name]
4003
+ if record ~= nil then
4004
+ return record.exports
4005
+ end
4006
+ module = ${G}.modules[name]
4007
+ end
4008
+ if module == nil then
4009
+ error(("Cannot find module '%s': nothing has loaded the file it is in"):format(name), 2)
4010
+ end` : several ? `
3608
4011
  if module == nil then
3609
4012
  ${G}.load(${G}.files[name])
3610
4013
  module = ${G}.modules[name]
@@ -3710,7 +4113,7 @@ var moduleAnalyses = /* @__PURE__ */ new Map();
3710
4113
  function analyzeModules(modules, config) {
3711
4114
  const out = [];
3712
4115
  const analyses = /* @__PURE__ */ new Map();
3713
- const libraries = config ? resolveTypeLibraries(config) : { files: [], lowerings: [], problems: [] };
4116
+ const libraries = config ? resolveTypeLibraries(config) : { files: [], lowerings: [], options: [], problems: [] };
3714
4117
  for (const p of libraries.problems) out.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
3715
4118
  const libs = libraries.files.map(parsedLibrary);
3716
4119
  const noLib = config?.noLib ?? false;
@@ -3749,7 +4152,7 @@ function analyzeModules(modules, config) {
3749
4152
  const at = d.node;
3750
4153
  said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type", severity: d.severity });
3751
4154
  }
3752
- const result2 = { types, diagnostics: said };
4155
+ const result2 = { types, diagnostics: said, scopes };
3753
4156
  if (partials === before) {
3754
4157
  analyzed.set(file, result2);
3755
4158
  const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
@@ -3806,7 +4209,16 @@ function analyzeModules(modules, config) {
3806
4209
  analyses.set(module.file, types);
3807
4210
  out.push(...diagnostics);
3808
4211
  }
3809
- return { types: analyses, diagnostics: out, lowerings: [...libraries.lowerings] };
4212
+ const exportsOf = (file) => {
4213
+ const cached = exportsCache.get(file);
4214
+ if (cached) return cached;
4215
+ const module = byFile.get(file);
4216
+ const { types, scopes } = analyzeModule(file, module.program);
4217
+ const exports = moduleExports(module.program, scopes, types, resolverFor(file));
4218
+ exportsCache.set(file, exports);
4219
+ return exports;
4220
+ };
4221
+ return { types: analyses, diagnostics: out, lowerings: [...libraries.lowerings], exportsOf };
3810
4222
  }
3811
4223
  function allStatements(statements) {
3812
4224
  const out = [];
@@ -3828,7 +4240,11 @@ function allStatements(statements) {
3828
4240
  export {
3829
4241
  resolveConfig,
3830
4242
  program,
4243
+ syntaxOf,
3831
4244
  lower,
4245
+ printOptions,
4246
+ printOutput,
3832
4247
  loadLowerings,
4248
+ WRITTEN_BY_TILUA,
3833
4249
  bundle
3834
4250
  };