@tilua/compiler 8.1.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,6 +3114,18 @@ 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
3131
  var VLQ_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@@ -3130,18 +3157,59 @@ function lineMapSource(map, unpack) {
3130
3157
  return `${unpack}({${files.map(luauString3).join(",")}},${luauString3(data)})`;
3131
3158
  }
3132
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
+ }
3186
+ }
3187
+
3133
3188
  // src/bundle.ts
3189
+ var WRITTEN_BY_TILUA = "-- Written by tilua";
3134
3190
  async function bundle(options) {
3135
- const entry = resolve2(options.entry);
3136
3191
  const diagnostics = [];
3192
+ const from = options.entry !== void 0 ? resolve2(options.entry) : resolve2(options.root ?? process.cwd(), "tilua.config.json");
3137
3193
  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()));
3194
+ const { config, problems } = resolveConfig(options.config, from, options.root ?? (inline ? dirname(from) : process.cwd()));
3139
3195
  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));
3196
+ const root = resolve2(options.root ?? config?.directory ?? dirname(from));
3141
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');
3142
3209
  const sources = /* @__PURE__ */ new Map();
3143
3210
  const directives = /* @__PURE__ */ new Map();
3144
- 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) || "."}`);
3145
3213
  while (queue.length) {
3146
3214
  const file = queue.shift();
3147
3215
  if (sources.has(file)) continue;
@@ -3159,6 +3227,22 @@ async function bundle(options) {
3159
3227
  if (target && !target.endsWith(".d.tilua")) queue.push(target);
3160
3228
  }
3161
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
+ }
3162
3246
  const analysis = analyzeModules([...sources.values()], config);
3163
3247
  const { lowerings, problems: loweringProblems } = await loadLowerings(analysis.lowerings);
3164
3248
  for (const p of loweringProblems) {
@@ -3169,19 +3253,56 @@ async function bundle(options) {
3169
3253
  const G = names.fresh("G");
3170
3254
  const requireExpression = member(identifier(G), "require");
3171
3255
  const importExpression = member(identifier(G), "import");
3172
- const mode = options.output ?? config?.output ?? "single";
3173
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;
3174
3293
  const modules = /* @__PURE__ */ new Map();
3175
3294
  const origins = /* @__PURE__ */ new WeakMap();
3176
3295
  const shared = new SharedRuntime(names);
3177
3296
  const shaking = options.treeShaking ?? config?.treeShaking ?? false;
3178
- const mapping = options.lineMap ?? config?.lineMap ?? true;
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);
3179
3300
  const resolveIn = (file) => (specifier) => {
3180
3301
  const found = resolveModulePath(file, specifier, config);
3181
3302
  return found !== void 0 && sources.has(found) ? found : void 0;
3182
3303
  };
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];
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];
3185
3306
  while (pending.length) {
3186
3307
  const file = pending.shift();
3187
3308
  if (modules.has(file)) continue;
@@ -3217,6 +3338,13 @@ async function bundle(options) {
3217
3338
  lines: !mapping ? void 0 : mode === "single" ? `${G}.lines` : `${G}.maps[${luauString4(source.name)}]`,
3218
3339
  fail: mapping ? `${G}.fail` : void 0,
3219
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
+ },
3220
3348
  resolve: (specifier, dynamic) => {
3221
3349
  const found = resolveModulePath(file, specifier, config);
3222
3350
  if (!found || found.endsWith(".d.tilua") || !sources.has(found)) return void 0;
@@ -3268,6 +3396,102 @@ async function bundle(options) {
3268
3396
  return { files: [], modules: moduleNames, diagnostics };
3269
3397
  }
3270
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
+ }
3271
3495
  const several = mode !== "single";
3272
3496
  const layout = layoutOf(mode, modules.get(entry), lowered, options.outFile);
3273
3497
  const usesScriptArgs = lowered.some((m) => m.usesScriptArgs);
@@ -3299,17 +3523,24 @@ async function bundle(options) {
3299
3523
  const result2 = names.fresh("result");
3300
3524
  const entryName = luauString4(sources.get(entry).name);
3301
3525
  const returnsExports = modules.get(entry).info.exports;
3302
- const entryStatements = parseLuau2(!mapping ? `${returnsExports ? "return " : ""}${G}.require(${entryName})` : `
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})`}` : `
3303
3531
  local ${ok}, ${result2} = xpcall(function()
3304
3532
  return ${G}.require(${entryName})
3305
3533
  end, ${G}.fail)
3306
3534
  if not ${ok} then
3307
3535
  error(${result2}, 0)
3308
3536
  end
3309
- ${returnsExports ? `return ${result2}` : ""}
3537
+ ${declarations}
3538
+ ${returned ? `return ${returned.value}` : ""}
3310
3539
  `).body.statements;
3311
3540
  program2.body.statements.push(...entryStatements);
3312
- const map = mapping ? buildLineMap(program2, parseLuau2(print(program2)), origins) : /* @__PURE__ */ new Map();
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();
3313
3544
  if (mapping && (map.size || several)) {
3314
3545
  const mapStatements = [assign(
3315
3546
  [member(identifier(G), "lines")],
@@ -3319,7 +3550,9 @@ ${returnsExports ? `return ${result2}` : ""}
3319
3550
  program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...mapStatements);
3320
3551
  }
3321
3552
  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) }];
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) }];
3323
3556
  for (const file of layout.slice(1)) {
3324
3557
  const fileHelpers = shared.chunk(new Set(file.modules.flatMap((m) => m.shared)));
3325
3558
  const body = [...fileHelpers.declare];
@@ -3332,14 +3565,34 @@ ${returnsExports ? `return ${result2}` : ""}
3332
3565
  }
3333
3566
  const chunk = program([returns([functionExpression(functionBody([G], body))])]);
3334
3567
  if (mapping) {
3335
- const lines = buildLineMap(chunk, parseLuau2(print(chunk)), origins);
3568
+ const lines = buildLineMap(chunk, parseLuau2(print2(chunk, printing)), origins);
3336
3569
  body.push(...mapRegistration(G, file, lineMapSource(lines, `${G}.lineMap`)));
3337
3570
  }
3338
3571
  body.push(...fileHelpers.define);
3339
- files.push({ path: file.path, code: print(chunk) + "\n", modules: file.modules.map((m) => m.name) });
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) });
3340
3575
  }
3341
3576
  return { code: files[0].code, files, modules: moduleNames, diagnostics };
3342
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
+ }
3343
3596
  function moduleTable(module) {
3344
3597
  const { names: exported, links, stars } = module.info;
3345
3598
  const fields = [];
@@ -3359,10 +3612,6 @@ function layoutOf(mode, entry, modules, outFile) {
3359
3612
  const entryStem = outFile !== void 0 ? basename(outFile, extname(outFile)) : posix.basename(entry.name);
3360
3613
  const others = modules.filter((m) => m !== entry);
3361
3614
  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
3615
  const byName = new Map(modules.map((m) => [m.name, m]));
3367
3616
  const reach = (root) => {
3368
3617
  const seen = /* @__PURE__ */ new Set();
@@ -3398,11 +3647,70 @@ function layoutOf(mode, entry, modules, outFile) {
3398
3647
  return files;
3399
3648
  }
3400
3649
  function loadPath(entryPath, file, byPath) {
3401
- const path = posix.relative(posix.dirname(entryPath), file);
3402
- 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);
3403
3685
  const bare = path.slice(0, path.length - posix.extname(path).length);
3404
3686
  return bare.startsWith("../") ? bare : `./${bare}`;
3405
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
+ }
3406
3714
  function mapRegistration(G, file, map) {
3407
3715
  const extension = posix.extname(file.path);
3408
3716
  const parts = file.path.slice(0, file.path.length - extension.length).split(/[/.]/).filter((part) => part !== "");
@@ -3434,6 +3742,7 @@ function loader(G, byPath) {
3434
3742
  }
3435
3743
  function runtime(G, options) {
3436
3744
  const { several, mapping } = options;
3745
+ const perFile = options.perFile ?? false;
3437
3746
  const files = [...options.files].map(([name, file]) => `[${luauString4(name)}] = ${luauString4(file)}`).join(", ");
3438
3747
  return `
3439
3748
  local ${G}
@@ -3645,32 +3954,60 @@ ${several ? `
3645
3954
  return map[key]
3646
3955
  end })
3647
3956
  end,` : ""}
3648
- records = {},${several ? `
3957
+ records = {},${several && !perFile ? `
3649
3958
  -- The file each module outside this one is in, as \`load\` is handed it.
3650
3959
  files = { ${files} },
3651
- loaded = {},${mapping ? `
3960
+ loaded = {},` : ""}${several && mapping ? `
3652
3961
  -- Every file's line map: by the module, and by the file's name.
3653
3962
  maps = {},
3654
- sources = {},` : ""}
3963
+ sources = {},` : ""}${several && !perFile ? `
3655
3964
  -- Another of the bundle's files: a function that adds its modules here.
3656
3965
  load = function(file)
3657
3966
  if ${G}.loaded[file] then
3658
3967
  return
3659
3968
  end
3660
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
3661
3986
  end,` : ""}${options.imports ? `
3662
3987
  -- \`import("./x")\`: the module's exports, asked for where the code runs
3663
3988
  -- rather than before its module does. \`require\` does the rest \u2014 and
3664
3989
  -- loads the file the module is in, if it is not this one.
3665
- import = function(name)
3666
- return ${G}.require(name)
3990
+ import = function(name${perFile ? ", load" : ""})
3991
+ return ${G}.require(name${perFile ? ", load" : ""})
3667
3992
  end,` : ""}
3668
- require = function(name)
3993
+ require = function(name${perFile ? ", load" : ""})
3669
3994
  local record = ${G}.records[name]
3670
3995
  if record ~= nil then
3671
3996
  return record.exports
3672
3997
  end
3673
- 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 ? `
3674
4011
  if module == nil then
3675
4012
  ${G}.load(${G}.files[name])
3676
4013
  module = ${G}.modules[name]
@@ -3776,7 +4113,7 @@ var moduleAnalyses = /* @__PURE__ */ new Map();
3776
4113
  function analyzeModules(modules, config) {
3777
4114
  const out = [];
3778
4115
  const analyses = /* @__PURE__ */ new Map();
3779
- const libraries = config ? resolveTypeLibraries(config) : { files: [], lowerings: [], problems: [] };
4116
+ const libraries = config ? resolveTypeLibraries(config) : { files: [], lowerings: [], options: [], problems: [] };
3780
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" });
3781
4118
  const libs = libraries.files.map(parsedLibrary);
3782
4119
  const noLib = config?.noLib ?? false;
@@ -3815,7 +4152,7 @@ function analyzeModules(modules, config) {
3815
4152
  const at = d.node;
3816
4153
  said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type", severity: d.severity });
3817
4154
  }
3818
- const result2 = { types, diagnostics: said };
4155
+ const result2 = { types, diagnostics: said, scopes };
3819
4156
  if (partials === before) {
3820
4157
  analyzed.set(file, result2);
3821
4158
  const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
@@ -3872,7 +4209,16 @@ function analyzeModules(modules, config) {
3872
4209
  analyses.set(module.file, types);
3873
4210
  out.push(...diagnostics);
3874
4211
  }
3875
- 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 };
3876
4222
  }
3877
4223
  function allStatements(statements) {
3878
4224
  const out = [];
@@ -3894,7 +4240,11 @@ function allStatements(statements) {
3894
4240
  export {
3895
4241
  resolveConfig,
3896
4242
  program,
4243
+ syntaxOf,
3897
4244
  lower,
4245
+ printOptions,
4246
+ printOutput,
3898
4247
  loadLowerings,
4248
+ WRITTEN_BY_TILUA,
3899
4249
  bundle
3900
4250
  };