@defold-typescript/transpiler 0.35.0 → 0.36.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.
@@ -0,0 +1,660 @@
1
+ import * as ts from "typescript";
2
+ import {
3
+ createAssignmentStatement,
4
+ createCallExpression,
5
+ createFile,
6
+ createIdentifier,
7
+ createReturnStatement,
8
+ createStringLiteral,
9
+ createTableExpression,
10
+ createTableFieldExpression,
11
+ createTableIndexExpression,
12
+ createVariableDeclarationStatement,
13
+ isBlock,
14
+ isCallExpression,
15
+ isDoStatement,
16
+ isForInStatement,
17
+ isForStatement,
18
+ isFunctionDefinition,
19
+ isFunctionExpression,
20
+ isIdentifier,
21
+ isMethodCallExpression,
22
+ isRepeatStatement,
23
+ isReturnStatement,
24
+ isStringLiteral,
25
+ isTableExpression,
26
+ isVariableDeclarationStatement,
27
+ LuaPrinter,
28
+ type Node,
29
+ type Plugin,
30
+ type ProcessedFile,
31
+ type Statement,
32
+ type TransformationContext,
33
+ } from "typescript-to-lua";
34
+ import { COMPANION_INTERNAL_FIELD, computeCompanionClosure } from "./companion-closure";
35
+ import { resolvesToFactoryExport } from "./lifecycle-erasure";
36
+ import { requirePathForRel } from "./output-paths";
37
+
38
+ /** The table TSTL opens every module chunk with and returns from it. */
39
+ const EXPORTS_NAME = "____exports";
40
+
41
+ function exportsIdentifier() {
42
+ return createIdentifier(EXPORTS_NAME);
43
+ }
44
+
45
+ /**
46
+ * Whether the source owns Defold lifecycle hooks, which is what makes its output
47
+ * a component resource rather than a requirable module. Only the three runtime
48
+ * kinds split: an editor script's chunk returns its own hooks table instead of
49
+ * `____exports`, so the halves a split would produce are not the halves below.
50
+ */
51
+ function isRuntimeScriptSource(sourceFile: ts.SourceFile, checker: ts.TypeChecker): boolean {
52
+ return sourceFile.statements.some((statement) => {
53
+ const expression = ts.isExpressionStatement(statement)
54
+ ? statement.expression
55
+ : ts.isExportAssignment(statement) && !statement.isExportEquals
56
+ ? statement.expression
57
+ : undefined;
58
+ return (
59
+ expression !== undefined &&
60
+ ts.isCallExpression(expression) &&
61
+ resolvesToFactoryExport(expression.expression, checker)
62
+ );
63
+ });
64
+ }
65
+
66
+ interface CompanionPlan {
67
+ /** Full start of each top-level statement the companion takes over. */
68
+ readonly closureStarts: ReadonlySet<number>;
69
+ /** Members the script reaches by bare name, which the companion must expose. */
70
+ readonly internals: readonly string[];
71
+ /** Import bindings the companion needs its own `require` of. */
72
+ readonly importBindings: readonly string[];
73
+ /** The module path the script's `require` names. */
74
+ readonly requirePath: string;
75
+ }
76
+
77
+ function planFor(sourceFile: ts.SourceFile, checker: ts.TypeChecker): CompanionPlan | undefined {
78
+ if (sourceFile.isDeclarationFile || !isRuntimeScriptSource(sourceFile, checker)) {
79
+ return undefined;
80
+ }
81
+ const closure = computeCompanionClosure(sourceFile, checker);
82
+ // A source the previous slice rejects is reported by the build, not split: the
83
+ // shapes it names are exactly the ones a companion would silently break.
84
+ if (closure.statements.length === 0 || closure.violations.length > 0) {
85
+ return undefined;
86
+ }
87
+ return {
88
+ closureStarts: new Set(closure.statements.map((statement) => statement.pos)),
89
+ internals: closure.internals,
90
+ importBindings: closure.importBindings,
91
+ requirePath: requirePathForRel(sourceFile.fileName),
92
+ };
93
+ }
94
+
95
+ function isLuaNode(value: unknown): value is Node {
96
+ return typeof value === "object" && value !== null && typeof (value as Node).kind === "number";
97
+ }
98
+
99
+ function forEachLuaNode(root: Node, visit: (node: Node) => void): void {
100
+ visit(root);
101
+ for (const value of Object.values(root)) {
102
+ if (Array.isArray(value)) {
103
+ for (const element of value) {
104
+ if (isLuaNode(element)) {
105
+ forEachLuaNode(element, visit);
106
+ }
107
+ }
108
+ } else if (isLuaNode(value)) {
109
+ forEachLuaNode(value, visit);
110
+ }
111
+ }
112
+ }
113
+
114
+ function declaredNames(statement: Statement): readonly string[] {
115
+ return isVariableDeclarationStatement(statement) ? statement.left.map((name) => name.text) : [];
116
+ }
117
+
118
+ /**
119
+ * The names these statements read but do not bind, under Lua's own scoping.
120
+ *
121
+ * A flat identifier walk cannot answer this: a parameter, a nested `local` and a
122
+ * loop variable are all spelled as plain identifiers, so every name introduced
123
+ * inside a moved function would read as a free reference to whatever the script
124
+ * happens to bind under the same spelling.
125
+ *
126
+ * The statements' own top level is one scope, pre-seeded with everything it
127
+ * declares before the walk starts. That makes it order-insensitive, unlike the
128
+ * nested scopes, which is what keeps TSTL's hoisted `local a, b` forward
129
+ * references reading as bound.
130
+ */
131
+ function freeNames(statements: readonly Statement[]): Set<string> {
132
+ const free = new Set<string>();
133
+ const scopes: Set<string>[] = [new Set()];
134
+
135
+ function declare(name: string): void {
136
+ scopes[scopes.length - 1]?.add(name);
137
+ }
138
+
139
+ function isBound(name: string): boolean {
140
+ return scopes.some((scope) => scope.has(name));
141
+ }
142
+
143
+ function inScope(run: () => void): void {
144
+ scopes.push(new Set());
145
+ run();
146
+ scopes.pop();
147
+ }
148
+
149
+ function visitEach(nodes: readonly Node[] | undefined): void {
150
+ for (const node of nodes ?? []) {
151
+ visit(node);
152
+ }
153
+ }
154
+
155
+ function visit(node: Node | undefined): void {
156
+ if (node === undefined) {
157
+ return;
158
+ }
159
+ if (isIdentifier(node)) {
160
+ if (!isBound(node.text)) {
161
+ free.add(node.text);
162
+ }
163
+ return;
164
+ }
165
+ // `right` before `left`, so `local x = x` reads the outer `x` as Lua does.
166
+ if (isVariableDeclarationStatement(node)) {
167
+ // Except the one shape the printer emits as `local function name(...)`
168
+ // "to allow recursion" — there the name is bound inside its own body.
169
+ // `FunctionDefinition` narrows only `right`, so mirror the printer, which
170
+ // binds `left[0]` and prints no other name in this shape.
171
+ if (isFunctionDefinition(node)) {
172
+ const [name] = node.left;
173
+ if (name !== undefined) {
174
+ declare(name.text);
175
+ }
176
+ visit(node.right[0]);
177
+ return;
178
+ }
179
+ visitEach(node.right);
180
+ for (const name of node.left) {
181
+ declare(name.text);
182
+ }
183
+ return;
184
+ }
185
+ if (isFunctionExpression(node)) {
186
+ inScope(() => {
187
+ for (const param of node.params ?? []) {
188
+ declare(param.text);
189
+ }
190
+ visit(node.body);
191
+ });
192
+ return;
193
+ }
194
+ if (isForStatement(node)) {
195
+ visit(node.controlVariableInitializer);
196
+ visit(node.limitExpression);
197
+ visit(node.stepExpression);
198
+ inScope(() => {
199
+ declare(node.controlVariable.text);
200
+ visit(node.body);
201
+ });
202
+ return;
203
+ }
204
+ if (isForInStatement(node)) {
205
+ visitEach(node.expressions);
206
+ inScope(() => {
207
+ for (const name of node.names) {
208
+ declare(name.text);
209
+ }
210
+ visit(node.body);
211
+ });
212
+ return;
213
+ }
214
+ // `repeat … until` is the one loop whose condition sees the body's locals.
215
+ if (isRepeatStatement(node)) {
216
+ inScope(() => {
217
+ visitEach(node.body.statements);
218
+ visit(node.condition);
219
+ });
220
+ return;
221
+ }
222
+ if (isBlock(node) || isDoStatement(node)) {
223
+ inScope(() => visitEach(node.statements));
224
+ return;
225
+ }
226
+ // A method name is not a variable read; a table index spelled as an
227
+ // identifier is.
228
+ if (isMethodCallExpression(node)) {
229
+ visit(node.prefixExpression);
230
+ visitEach(node.params);
231
+ return;
232
+ }
233
+ for (const value of Object.values(node)) {
234
+ if (Array.isArray(value)) {
235
+ for (const element of value) {
236
+ if (isLuaNode(element)) {
237
+ visit(element);
238
+ }
239
+ }
240
+ } else if (isLuaNode(value)) {
241
+ visit(value);
242
+ }
243
+ }
244
+ }
245
+
246
+ for (const statement of statements) {
247
+ for (const name of declaredNames(statement)) {
248
+ declare(name);
249
+ }
250
+ }
251
+ for (const statement of statements) {
252
+ visit(statement);
253
+ }
254
+ return free;
255
+ }
256
+
257
+ /**
258
+ * Where each module specifier the source imports from lands as a Lua require.
259
+ *
260
+ * TSTL rewrites its own requires by string substitution over the printed code,
261
+ * in a pass that runs *after* `afterPrint` — so the script's copy is rewritten
262
+ * and the companion's, printed here, never is. Resolving through the checker and
263
+ * `requirePathForRel` reaches the same spelling by the same segment math the
264
+ * build's resolution check reads. A specifier that resolves to no emitted source
265
+ * — a declaration file, an unresolved module — is left as the source wrote it.
266
+ */
267
+ function resolvedRequirePaths(
268
+ sourceFile: ts.SourceFile,
269
+ checker: ts.TypeChecker,
270
+ ): Map<string, string> {
271
+ const paths = new Map<string, string>();
272
+ for (const statement of sourceFile.statements) {
273
+ const specifier =
274
+ ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)
275
+ ? statement.moduleSpecifier
276
+ : undefined;
277
+ if (specifier === undefined || !ts.isStringLiteral(specifier)) {
278
+ continue;
279
+ }
280
+ const target = checker
281
+ .getSymbolAtLocation(specifier)
282
+ ?.declarations?.find((declaration): declaration is ts.SourceFile =>
283
+ ts.isSourceFile(declaration),
284
+ );
285
+ if (target === undefined || target.isDeclarationFile) {
286
+ continue;
287
+ }
288
+ paths.set(specifier.text, requirePathForRel(target.fileName));
289
+ }
290
+ return paths;
291
+ }
292
+
293
+ /**
294
+ * Rewrites every `require` a moved statement carries, at whatever depth — a
295
+ * re-export's is nested in the `do … end` block TSTL emits for it.
296
+ *
297
+ * In place, which is safe for exactly these statements: `split` sends an owned
298
+ * statement to the companion alone, so no printer ever sees the node again.
299
+ */
300
+ function resolveMovedRequires(
301
+ statements: readonly Statement[],
302
+ paths: ReadonlyMap<string, string>,
303
+ ) {
304
+ for (const statement of statements) {
305
+ forEachLuaNode(statement, (node) => {
306
+ if (!isCallExpression(node) || !isIdentifier(node.expression)) {
307
+ return;
308
+ }
309
+ if (node.expression.text !== "require" || node.params.length !== 1) {
310
+ return;
311
+ }
312
+ const [argument] = node.params;
313
+ if (argument === undefined || !isStringLiteral(argument)) {
314
+ return;
315
+ }
316
+ const resolved = paths.get(argument.value);
317
+ if (resolved !== undefined) {
318
+ argument.value = resolved;
319
+ }
320
+ });
321
+ }
322
+ }
323
+
324
+ // Rebuilt rather than mutated in place: the script keeps the very same node, and
325
+ // its require is still spelled the way TSTL's later pass expects to find it.
326
+ function withResolvedRequire(statement: Statement, paths: ReadonlyMap<string, string>): Statement {
327
+ if (!isVariableDeclarationStatement(statement) || statement.left.length !== 1) {
328
+ return statement;
329
+ }
330
+ const name = statement.left[0];
331
+ const [value] = statement.right ?? [];
332
+ if (name === undefined || value === undefined || !isCallExpression(value)) {
333
+ return statement;
334
+ }
335
+ if (!isIdentifier(value.expression) || value.expression.text !== "require") {
336
+ return statement;
337
+ }
338
+ const [argument] = value.params;
339
+ if (argument === undefined || !isStringLiteral(argument)) {
340
+ return statement;
341
+ }
342
+ const resolved = paths.get(argument.value);
343
+ if (resolved === undefined) {
344
+ return statement;
345
+ }
346
+ return createVariableDeclarationStatement(
347
+ createIdentifier(name.text),
348
+ createCallExpression(createIdentifier("require"), [createStringLiteral(resolved)]),
349
+ );
350
+ }
351
+
352
+ /**
353
+ * The script statements binding names the companion reads but does not bind —
354
+ * TSTL's hoisted `require` prelude — to copy, not move, into the companion.
355
+ *
356
+ * Copying is the only option and also the right one: an import's Lua comes from
357
+ * TSTL's own hoisting rather than from the visitor, so there is no statement to
358
+ * move, and Lua's module cache makes the second `require` the same table, so a
359
+ * mutable import keeps its identity across the split. The walk is transitive
360
+ * because `local x = ____lib.x` is useless without `local ____lib = require(…)`.
361
+ */
362
+ function copiedPrelude(halves: SplitHalves, importBindings: readonly string[]): Statement[] {
363
+ const needed = new Set(importBindings);
364
+ // `importBindings` is what the companion's import needs actually come from.
365
+ // This second seed is a net over the scope model: today it contributes only
366
+ // names no script declaration binds, so it copies nothing, and a future gap
367
+ // in `freeNames` surfaces as an over-copy rather than an unbound global.
368
+ for (const name of freeNames(halves.companion)) {
369
+ needed.add(name);
370
+ }
371
+
372
+ const alreadyCarried = new Set(halves.companion);
373
+ const candidates = halves.script.filter(
374
+ (statement) => isVariableDeclarationStatement(statement) && !alreadyCarried.has(statement),
375
+ );
376
+ const copied = new Set<Statement>();
377
+ let grew = true;
378
+ while (grew) {
379
+ grew = false;
380
+ for (const statement of candidates) {
381
+ if (copied.has(statement) || !declaredNames(statement).some((name) => needed.has(name))) {
382
+ continue;
383
+ }
384
+ copied.add(statement);
385
+ // The statement's own free names, so copying `local warm = ____boot.warm`
386
+ // pulls `____boot` behind it and not `warm`.
387
+ for (const name of freeNames([statement])) {
388
+ needed.add(name);
389
+ }
390
+ grew = true;
391
+ }
392
+ }
393
+ return halves.script.filter((statement) => copied.has(statement));
394
+ }
395
+
396
+ function namesIdentifier(statement: Statement, names: ReadonlySet<string>): boolean {
397
+ let found = false;
398
+ forEachLuaNode(statement, (node) => {
399
+ if (isIdentifier(node) && names.has(node.text)) {
400
+ found = true;
401
+ }
402
+ });
403
+ return found;
404
+ }
405
+
406
+ function isExportsTableInit(statement: Statement): boolean {
407
+ if (!isVariableDeclarationStatement(statement) || statement.left.length !== 1) {
408
+ return false;
409
+ }
410
+ const [single] = statement.right ?? [];
411
+ return (
412
+ statement.left[0]?.text === EXPORTS_NAME && single !== undefined && isTableExpression(single)
413
+ );
414
+ }
415
+
416
+ function isExportsReturn(statement: Statement): boolean {
417
+ if (!isReturnStatement(statement) || statement.expressions.length !== 1) {
418
+ return false;
419
+ }
420
+ const [single] = statement.expressions;
421
+ return single !== undefined && isIdentifier(single) && single.text === EXPORTS_NAME;
422
+ }
423
+
424
+ // A bare `local a, b` with no initializer is the declaration TSTL's hoisting
425
+ // prepends for forward-referenced names. It belongs to neither half, so both
426
+ // halves take a copy: without it the half that assigns one of those names would
427
+ // write a global instead.
428
+ function isHoistedNameDeclaration(statement: Statement): boolean {
429
+ return isVariableDeclarationStatement(statement) && statement.right === undefined;
430
+ }
431
+
432
+ function internalsField(name: string) {
433
+ return createTableIndexExpression(
434
+ createTableIndexExpression(exportsIdentifier(), createStringLiteral(COMPANION_INTERNAL_FIELD)),
435
+ createStringLiteral(name),
436
+ );
437
+ }
438
+
439
+ interface SplitHalves {
440
+ readonly script: Statement[];
441
+ readonly companion: Statement[];
442
+ }
443
+
444
+ /**
445
+ * Where the companion's `require` goes in the script: after the leading run of
446
+ * plain declarations, which is where TSTL's hoisting has already put every other
447
+ * module's `require`, so a runtime import written above the closure still loads
448
+ * first. The run stops at the first statement that reads the companion, and at
449
+ * the first non-declaration — a lifecycle hook closes over the internals it
450
+ * reads, so its declaration must precede the hook's.
451
+ */
452
+ function requireInsertionPoint(script: readonly Statement[], reads: ReadonlySet<string>): number {
453
+ let index = 0;
454
+ while (index < script.length) {
455
+ const statement = script[index];
456
+ if (statement === undefined || !isVariableDeclarationStatement(statement)) {
457
+ break;
458
+ }
459
+ if (namesIdentifier(statement, reads)) {
460
+ break;
461
+ }
462
+ index++;
463
+ }
464
+ return index;
465
+ }
466
+
467
+ function split(statements: readonly Statement[], owned: ReadonlySet<Statement>): SplitHalves {
468
+ const script: Statement[] = [];
469
+ const companion: Statement[] = [];
470
+ for (const statement of statements) {
471
+ if (isExportsTableInit(statement) || isExportsReturn(statement)) {
472
+ continue;
473
+ }
474
+ if (owned.has(statement)) {
475
+ companion.push(statement);
476
+ continue;
477
+ }
478
+ if (isHoistedNameDeclaration(statement)) {
479
+ companion.push(statement);
480
+ }
481
+ script.push(statement);
482
+ }
483
+ return { script, companion };
484
+ }
485
+
486
+ export interface CompanionEmitter {
487
+ readonly plugin: Plugin;
488
+ /**
489
+ * Companion Lua by source file name, for the sources that produced one. Read
490
+ * after a transpile; a source absent from it emits a single chunk.
491
+ */
492
+ readonly companions: ReadonlyMap<string, string>;
493
+ }
494
+
495
+ /**
496
+ * Splits a script-kind source that exports runtime values into two chunks: a
497
+ * companion module holding the dependency closure of those exports, and the
498
+ * script, which requires it rather than redeclaring it. One definition then
499
+ * serves the script's own hooks and every importer, which is what makes a
500
+ * mutable export the same table on both sides.
501
+ *
502
+ * A factory rather than a shared constant: the captured chunks belong to one
503
+ * transpile, and a module-level plugin would leak them between the editor
504
+ * session's rebuilds.
505
+ */
506
+ export function createCompanionEmitPlugin(): CompanionEmitter {
507
+ const plans = new Map<string, CompanionPlan | undefined>();
508
+ const owned = new Map<string, Set<Statement>>();
509
+ const companions = new Map<string, string>();
510
+
511
+ function planOf(sourceFile: ts.SourceFile, checker: ts.TypeChecker): CompanionPlan | undefined {
512
+ const key = sourceFile.fileName;
513
+ if (!plans.has(key)) {
514
+ plans.set(key, planFor(sourceFile, checker));
515
+ }
516
+ return plans.get(key);
517
+ }
518
+
519
+ function capture(node: ts.Statement, context: TransformationContext): Statement[] {
520
+ const statements = context.superTransformStatements(node);
521
+ const sourceFile = context.sourceFile;
522
+ const plan = planOf(sourceFile, context.checker);
523
+ if (plan === undefined || !plan.closureStarts.has(node.pos)) {
524
+ return statements;
525
+ }
526
+ let bucket = owned.get(sourceFile.fileName);
527
+ if (bucket === undefined) {
528
+ bucket = new Set();
529
+ owned.set(sourceFile.fileName, bucket);
530
+ }
531
+ for (const statement of statements) {
532
+ bucket.add(statement);
533
+ }
534
+ return statements;
535
+ }
536
+
537
+ const visitors: Plugin["visitors"] = {
538
+ [ts.SyntaxKind.VariableStatement]: capture,
539
+ [ts.SyntaxKind.FunctionDeclaration]: capture,
540
+ [ts.SyntaxKind.ClassDeclaration]: capture,
541
+ [ts.SyntaxKind.EnumDeclaration]: capture,
542
+ // Not ImportDeclaration: `superTransformStatements` returns nothing for one,
543
+ // because TSTL emits its `require` bindings through its own hoisting. The
544
+ // companion takes a copy of that prelude instead — see `copiedPrelude`.
545
+ [ts.SyntaxKind.ExportDeclaration]: capture,
546
+ };
547
+
548
+ let configuredNoResolvePaths: readonly string[] = [];
549
+
550
+ const plugin: Plugin = {
551
+ visitors,
552
+ beforeTransform(_program, options) {
553
+ configuredNoResolvePaths = options.noResolvePaths ?? [];
554
+ },
555
+ afterPrint(program, options, emitHost, result) {
556
+ const emitted: string[] = [];
557
+ for (const file of result) {
558
+ const sourceFile = file.sourceFiles?.[0];
559
+ if (sourceFile === undefined || file.luaAst === undefined) {
560
+ continue;
561
+ }
562
+ const plan = plans.get(sourceFile.fileName);
563
+ const bucket = owned.get(sourceFile.fileName);
564
+ if (plan === undefined || bucket === undefined || bucket.size === 0) {
565
+ continue;
566
+ }
567
+ emitCompanion(program, emitHost, file, sourceFile, plan, bucket, companions);
568
+ emitted.push(plan.requirePath);
569
+ }
570
+ // The companion is written by the CLI, not by TSTL's emit plan, so TSTL's
571
+ // resolver would report the script's own require as unresolvable and
572
+ // rewrite it to a fallback path. Exempting exactly the paths just emitted
573
+ // leaves the require spelled the way `requirePathForRel` spells it, which
574
+ // is the spelling the build's resolution check reads.
575
+ if (emitted.length > 0) {
576
+ options.noResolvePaths = [...configuredNoResolvePaths, ...emitted];
577
+ }
578
+ },
579
+ };
580
+
581
+ return { plugin, companions };
582
+ }
583
+
584
+ function emitCompanion(
585
+ program: ts.Program,
586
+ emitHost: Parameters<NonNullable<Plugin["afterPrint"]>>[2],
587
+ file: ProcessedFile,
588
+ sourceFile: ts.SourceFile,
589
+ plan: CompanionPlan,
590
+ owned: ReadonlySet<Statement>,
591
+ companions: Map<string, string>,
592
+ ): void {
593
+ const luaAst = file.luaAst;
594
+ if (luaAst === undefined) {
595
+ return;
596
+ }
597
+ const halves = split(luaAst.statements, owned);
598
+ const requirePaths = resolvedRequirePaths(sourceFile, program.getTypeChecker());
599
+ // A require that moved is no longer in the script for TSTL's later pass to
600
+ // find, so the companion is the only place left to spell it correctly.
601
+ resolveMovedRequires(halves.companion, requirePaths);
602
+
603
+ const closureTable = createTableExpression(
604
+ plan.internals.map((name) =>
605
+ createTableFieldExpression(createIdentifier(name), createStringLiteral(name)),
606
+ ),
607
+ );
608
+ const companionStatements: Statement[] = [
609
+ createVariableDeclarationStatement(exportsIdentifier(), createTableExpression()),
610
+ ...copiedPrelude(halves, plan.importBindings).map((statement) =>
611
+ withResolvedRequire(statement, requirePaths),
612
+ ),
613
+ ...halves.companion,
614
+ ...(plan.internals.length > 0
615
+ ? [
616
+ createAssignmentStatement(
617
+ createTableIndexExpression(
618
+ exportsIdentifier(),
619
+ createStringLiteral(COMPANION_INTERNAL_FIELD),
620
+ ),
621
+ closureTable,
622
+ ),
623
+ ]
624
+ : []),
625
+ createReturnStatement([exportsIdentifier()]),
626
+ ];
627
+
628
+ const reads = new Set([EXPORTS_NAME, ...plan.internals]);
629
+ const insertAt = requireInsertionPoint(halves.script, reads);
630
+ const scriptStatements: Statement[] = [
631
+ ...halves.script.slice(0, insertAt),
632
+ createVariableDeclarationStatement(
633
+ exportsIdentifier(),
634
+ createCallExpression(createIdentifier("require"), [createStringLiteral(plan.requirePath)]),
635
+ ),
636
+ ...plan.internals.map((name) =>
637
+ createVariableDeclarationStatement(createIdentifier(name), internalsField(name)),
638
+ ),
639
+ ...halves.script.slice(insertAt),
640
+ createReturnStatement([exportsIdentifier()]),
641
+ ];
642
+
643
+ // Both halves carry the whole feature set rather than a per-half estimate: an
644
+ // underestimate leaves a half calling a nil global, and the cost of an
645
+ // overestimate is an unused local.
646
+ const features = new Set(luaAst.luaLibFeatures);
647
+ const printer = new LuaPrinter(emitHost, program, sourceFile.fileName);
648
+ companions.set(
649
+ sourceFile.fileName,
650
+ printer.print(createFile(companionStatements, new Set(features), luaAst.trivia, sourceFile))
651
+ .code,
652
+ );
653
+
654
+ const scriptFile = createFile(scriptStatements, features, luaAst.trivia, sourceFile);
655
+ const printed = new LuaPrinter(emitHost, program, sourceFile.fileName).print(scriptFile);
656
+ file.luaAst = scriptFile;
657
+ file.code = printed.code;
658
+ file.sourceMap = printed.sourceMap;
659
+ file.sourceMapNode = printed.sourceMapNode;
660
+ }
@@ -0,0 +1,31 @@
1
+ import { scanEmittedRequires } from "./lua-require-scan";
2
+ import { TIMERS_REQUIRE_NAME } from "./timers-runtime";
3
+
4
+ export const LUALIB_REQUIRE_NAME = "lualib_bundle";
5
+
6
+ // Written unconditionally by both build paths and backed by no TypeScript
7
+ // source, so a require of either can never be unresolvable.
8
+ const GENERATED_RUNTIME_REQUIRES: ReadonlySet<string> = new Set([
9
+ LUALIB_REQUIRE_NAME,
10
+ TIMERS_REQUIRE_NAME,
11
+ ]);
12
+
13
+ /**
14
+ * The distinct string-literal module paths an emitted Lua chunk requires, in
15
+ * source order. The two generated runtimes are skipped, as is any require whose
16
+ * argument is not a string literal — neither can name a source the build owns.
17
+ */
18
+ export function findEmittedRequires(lua: string): string[] {
19
+ const found: string[] = [];
20
+ const seen = new Set<string>();
21
+
22
+ for (const { path } of scanEmittedRequires(lua)) {
23
+ if (GENERATED_RUNTIME_REQUIRES.has(path) || seen.has(path)) {
24
+ continue;
25
+ }
26
+ seen.add(path);
27
+ found.push(path);
28
+ }
29
+
30
+ return found;
31
+ }