@defold-typescript/transpiler 0.34.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.
package/src/index.ts CHANGED
@@ -1,9 +1,17 @@
1
+ export type {
2
+ ClosureSite,
3
+ ClosureViolation,
4
+ ClosureViolationKind,
5
+ CompanionClosure,
6
+ } from "./companion-closure";
7
+ export { COMPANION_INTERNAL_FIELD, computeCompanionClosure } from "./companion-closure";
1
8
  export type { ComponentAnimationIndex } from "./component-animation-index";
2
9
  export {
3
10
  buildComponentAnimationIndex,
4
11
  componentIdOfSameObjectAddress,
5
12
  } from "./component-animation-index";
6
13
  export { buildConfigKeyIndex, readGameProjectSetting } from "./config-key-index";
14
+ export { findEmittedRequires, LUALIB_REQUIRE_NAME } from "./emitted-requires";
7
15
  export type { GuiFlipbookIndex } from "./gui-flipbook-index";
8
16
  export { buildGuiFlipbookIndex } from "./gui-flipbook-index";
9
17
  export type { GuiNodeIndex } from "./gui-node-index";
@@ -34,11 +42,13 @@ export {
34
42
  computeOutputRel,
35
43
  DEFAULT_INCLUDE,
36
44
  parseBuildConfig,
45
+ requirePathForRel,
37
46
  SCRIPT_SUFFIX_BY_KIND,
38
47
  stripIncludeBase,
39
48
  } from "./output-paths";
40
49
  export { getProgramDiagnostics } from "./program-diagnostics";
41
50
  export { isDefignoredPath, SCAFFOLDED_DEFIGNORE_LINES } from "./project-resources";
51
+ export { rewriteEmittedRequires } from "./require-rewrite";
42
52
  export { buildSceneAddressDeclaration } from "./scene-address-declaration";
43
53
  export type { SceneCollectionRoles, SceneCollectionRolesInput } from "./scene-collection-roles";
44
54
  export { buildSceneCollectionRoles } from "./scene-collection-roles";
@@ -75,6 +85,7 @@ export type { SceneMessage } from "./scene-text-format";
75
85
  export { parseSceneTextFormat, SceneTextFormatError } from "./scene-text-format";
76
86
  export type { TranspileSession } from "./session";
77
87
  export { createTranspileSession } from "./session";
88
+ export { TIMERS_REQUIRE_NAME } from "./timers-runtime";
78
89
  export type {
79
90
  TranspileDiagnostic,
80
91
  TranspileProjectInput,
@@ -35,7 +35,7 @@ const FACTORY_SPECIFIERS = new Set([
35
35
  `${FACTORY_MODULE}/render-script`,
36
36
  ]);
37
37
 
38
- function resolvesToFactoryExport(callee: ts.Expression, checker: ts.TypeChecker): boolean {
38
+ export function resolvesToFactoryExport(callee: ts.Expression, checker: ts.TypeChecker): boolean {
39
39
  let symbol = checker.getSymbolAtLocation(callee);
40
40
  if (symbol === undefined) {
41
41
  return false;
Binary file
@@ -60,6 +60,26 @@ function relUnderOutDir(rel: string, config: BuildConfig): string {
60
60
  return path.posix.join(outDir, relUnderBase);
61
61
  }
62
62
 
63
+ /**
64
+ * The `require` path Lua reaches a rel by, mirroring the segment math TSTL
65
+ * applies when it emits one: the extension comes off the last segment, then
66
+ * every remaining `.` becomes `_` *within* a segment, because Lua reads a dot as
67
+ * a path separator and `src/foo.bar.lua` would otherwise be unreachable as
68
+ * `require("src.foo.bar")`. Both sides of the resolution check key through this,
69
+ * so a require and the output meant to satisfy it cannot be spelled differently.
70
+ */
71
+ export function requirePathForRel(rel: string): string {
72
+ const segments = rel
73
+ .split("/")
74
+ .filter((segment) => segment !== "" && segment !== "." && segment !== "..");
75
+ const last = segments.length - 1;
76
+ const lastSegment = segments[last];
77
+ if (lastSegment !== undefined) {
78
+ segments[last] = lastSegment.replace(/\.[^.]*$/, "");
79
+ }
80
+ return segments.map((segment) => segment.replace(/\./g, "_")).join(".");
81
+ }
82
+
63
83
  export function computeOutputRel(rel: string, config: BuildConfig, kind: SourceOutputKind): string {
64
84
  const baseRel = relUnderOutDir(rel, config);
65
85
  if (kind === "module") {
@@ -0,0 +1,36 @@
1
+ import { scanEmittedRequires } from "./lua-require-scan";
2
+
3
+ function quote(value: string): string {
4
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
5
+ }
6
+
7
+ /**
8
+ * Repoint every code-position `require` whose module path `rewrites` maps, and
9
+ * leave the chunk byte-identical everywhere else.
10
+ *
11
+ * Built in one pass over the spans the shared scanner reports rather than by
12
+ * iterated replacement: a chunk is rewritten against the map it was scanned
13
+ * with, so a mapped value that is itself a key is never re-entered, and two
14
+ * paths sharing a prefix cannot bleed into each other. The generated runtimes
15
+ * are rewritten like any other require — unlike the resolution check, which
16
+ * exempts them, the rewriter has to move them when the build writes them under
17
+ * an `outDir`.
18
+ */
19
+ export function rewriteEmittedRequires(lua: string, rewrites: ReadonlyMap<string, string>): string {
20
+ if (rewrites.size === 0) {
21
+ return lua;
22
+ }
23
+
24
+ let out = "";
25
+ let cursor = 0;
26
+ for (const { path, start, end } of scanEmittedRequires(lua)) {
27
+ const mapped = rewrites.get(path);
28
+ if (mapped === undefined) {
29
+ continue;
30
+ }
31
+ out += lua.slice(cursor, start) + quote(mapped);
32
+ cursor = end;
33
+ }
34
+
35
+ return cursor === 0 ? lua : out + lua.slice(cursor);
36
+ }
package/src/session.ts CHANGED
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
3
3
  import * as path from "node:path";
4
4
  import * as ts from "typescript";
5
5
  import * as tstl from "typescript-to-lua";
6
+ import { type CompanionEmitter, createCompanionEmitPlugin } from "./companion-emit";
6
7
  import { editorScriptErasurePlugin } from "./editor-script-erasure";
7
8
  import { lifecycleErasurePlugin } from "./lifecycle-erasure";
8
9
  import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
@@ -18,28 +19,34 @@ export interface TranspileSession {
18
19
 
19
20
  const requireFromHere = createRequire(import.meta.url);
20
21
 
21
- const COMPILER_OPTIONS: tstl.CompilerOptions = {
22
- luaTarget: tstl.LuaTarget.Lua51,
23
- sourceMap: true,
24
- // Already-resolved CompilerOptions, so the value is the lib file name, not the
25
- // tsconfig spelling "ES2022". Drops the default `lib.dom`, whose `declare var
26
- // window` shadows Defold's `window` namespace. Keep in lockstep with transpile.ts.
27
- lib: ["lib.es2022.d.ts"],
28
- // Don't cross-check the seeded ambient .d.ts surface against itself; only user
29
- // files matter (mirrors transpileProject and the editor's skipLibCheck).
30
- skipLibCheck: true,
31
- // Defold scripts are not OO: free helper functions never receive a context,
32
- // so suppress TSTL's implicit `self` parameter and the `_G` call-site filler.
33
- noImplicitSelf: true,
34
- luaPlugins: [
35
- { plugin: lifecycleErasurePlugin },
36
- { plugin: editorScriptErasurePlugin },
37
- { plugin: messageGuardLoweringPlugin },
38
- { plugin: windowEventGuardLoweringPlugin },
39
- { plugin: messageDispatchLoweringPlugin },
40
- { plugin: timersLoweringPlugin },
41
- ],
42
- };
22
+ // A function rather than a const because the companion emitter carries the
23
+ // chunks of one transpile: a shared instance would hand an editor rebuild the
24
+ // companions of the rebuild before it. Keep in lockstep with transpile.ts.
25
+ function compilerOptions(companionEmit: CompanionEmitter): tstl.CompilerOptions {
26
+ return {
27
+ luaTarget: tstl.LuaTarget.Lua51,
28
+ sourceMap: true,
29
+ // Already-resolved CompilerOptions, so the value is the lib file name, not the
30
+ // tsconfig spelling "ES2022". Drops the default `lib.dom`, whose `declare var
31
+ // window` shadows Defold's `window` namespace.
32
+ lib: ["lib.es2022.d.ts"],
33
+ // Don't cross-check the seeded ambient .d.ts surface against itself; only user
34
+ // files matter (mirrors transpileProject and the editor's skipLibCheck).
35
+ skipLibCheck: true,
36
+ // Defold scripts are not OO: free helper functions never receive a context,
37
+ // so suppress TSTL's implicit `self` parameter and the `_G` call-site filler.
38
+ noImplicitSelf: true,
39
+ luaPlugins: [
40
+ { plugin: lifecycleErasurePlugin },
41
+ { plugin: editorScriptErasurePlugin },
42
+ { plugin: messageGuardLoweringPlugin },
43
+ { plugin: windowEventGuardLoweringPlugin },
44
+ { plugin: messageDispatchLoweringPlugin },
45
+ { plugin: timersLoweringPlugin },
46
+ { plugin: companionEmit.plugin },
47
+ ],
48
+ };
49
+ }
43
50
 
44
51
  function normalizeSlashes(p: string): string {
45
52
  return p.replace(/\\/g, "/");
@@ -151,7 +158,8 @@ export function createTranspileSession(): TranspileSession {
151
158
  const userKeys = new Set(userFiles.keys());
152
159
  const rootNames = [...Object.keys(AMBIENT_FILES).map(normalizeSlashes), ...userKeys];
153
160
 
154
- program = ts.createProgram(rootNames, COMPILER_OPTIONS, host, program);
161
+ const companionEmit = createCompanionEmitPlugin();
162
+ program = ts.createProgram(rootNames, compilerOptions(companionEmit), host, program);
155
163
 
156
164
  const preEmitDiagnostics = ts.getPreEmitDiagnostics(program);
157
165
  const collector = createOutputCollector();
@@ -163,7 +171,7 @@ export function createTranspileSession(): TranspileSession {
163
171
  ...ts.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics]),
164
172
  ];
165
173
 
166
- return collectOutputs(collector.files, diagnostics, userKeys);
174
+ return collectOutputs(collector.files, diagnostics, userKeys, companionEmit.companions);
167
175
  }
168
176
 
169
177
  return {
@@ -1,5 +1,5 @@
1
1
  import * as ts from "typescript";
2
- import type { Plugin } from "typescript-to-lua";
2
+ import { LuaLibFeature, type Plugin } from "typescript-to-lua";
3
3
  import { TIMERS_MODULE_SPECIFIER, TIMERS_REQUIRE_NAME } from "./timers-runtime";
4
4
 
5
5
  function isTimersSpecifier(node: ts.Expression): node is ts.StringLiteral {
@@ -30,6 +30,15 @@ export const timersLoweringPlugin: Plugin = {
30
30
  if (!isTimersSpecifier(node.moduleSpecifier)) {
31
31
  return context.superTransformStatements(node);
32
32
  }
33
+ // This chunk reads neither helper, but the hand-authored runtime the
34
+ // rewritten require loads does, and TSTL synthesizes `lualib_bundle.lua`
35
+ // only for features registered here — it never sees the runtime source.
36
+ // Registering both (rather than only `Promise`, which recursively pulls
37
+ // `New`) also keeps the emitted prelude complete under
38
+ // `luaLibImport: "require-minimal"`, where the bundle is rebuilt from a
39
+ // scan of the emitted chunks' `local X = ____lualib.X` lines.
40
+ context.usedLuaLibFeatures.add(LuaLibFeature.Promise);
41
+ context.usedLuaLibFeatures.add(LuaLibFeature.New);
33
42
  const rewritten = ts.factory.updateImportDeclaration(
34
43
  node,
35
44
  node.modifiers,
@@ -7,9 +7,10 @@ export const TIMERS_REQUIRE_NAME = "defold_typescript_timers";
7
7
 
8
8
  // Hand-authored runtime Lua. Unlike `defineScript` (which `lifecycle-erasure`
9
9
  // removes), these wrappers have real behavior, so the Lua must reach the Defold
10
- // project. `__TS__Promise`/`__TS__New` come from the lualib bundle the build
11
- // already writes alongside this file; `wait` resolves its promise from inside
12
- // the `timer.delay` callback, the only thing that advances it (no event loop).
10
+ // project. `__TS__Promise`/`__TS__New` come from the lualib bundle that exists
11
+ // because the import lowering registers those two features when it rewrites the
12
+ // polyfill specifier; `wait` resolves its promise from inside the `timer.delay`
13
+ // callback, the only thing that advances it (no event loop).
13
14
  export const TIMERS_RUNTIME = `local ____lualib = require("lualib_bundle")
14
15
  local __TS__Promise = ____lualib.__TS__Promise
15
16
  local __TS__New = ____lualib.__TS__New
package/src/transpile.ts CHANGED
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
3
3
  import * as path from "node:path";
4
4
  import type * as ts from "typescript";
5
5
  import * as tstl from "typescript-to-lua";
6
+ import { createCompanionEmitPlugin } from "./companion-emit";
6
7
  import { editorScriptErasurePlugin } from "./editor-script-erasure";
7
8
  import {
8
9
  findDirectGoPropertyCalls,
@@ -43,10 +44,17 @@ export interface TranspileProjectResult {
43
44
  readonly lua: Readonly<Record<string, string>>;
44
45
  readonly sourceMaps: Readonly<Record<string, string>>;
45
46
  readonly diagnostics: readonly TranspileDiagnostic[];
46
- // TSTL synthesizes this bundle (no user source) whenever a lualib feature
47
- // (`Object.keys`, spread, `__TS__TypeOf`, ...) is used; the emitted
48
- // `require("lualib_bundle")` only resolves in Defold if the CLI writes it to
49
- // the output root. Absent when no feature pulls it in.
47
+ // The companion module a script-kind source with runtime value exports emits
48
+ // alongside its script, keyed by the same source rel as `lua`. The script's
49
+ // own chunk requires it at the module-kind output path, so the CLI must write
50
+ // it there. Absent for a source that emits a single chunk.
51
+ readonly companions?: Readonly<Record<string, string>>;
52
+ // TSTL synthesizes this bundle (no user source) whenever a lualib feature is
53
+ // registered — either by a user source using one (`Object.keys`, spread,
54
+ // `__TS__TypeOf`, ...) or by the timers import lowering, whose generated
55
+ // runtime requires the bundle. The emitted `require("lualib_bundle")` only
56
+ // resolves in Defold if the CLI writes it to the output root. Absent when
57
+ // neither trigger fires.
50
58
  readonly lualib?: string;
51
59
  // Hand-authored runtime Lua for the `@defold-typescript/types/timers`
52
60
  // polyfills (`setTimeout`/`wait`/...). Unlike `defineScript`, the import is
@@ -215,9 +223,11 @@ export function collectOutputs(
215
223
  transpiledFiles: readonly CollectableFile[],
216
224
  diagnostics: readonly ts.Diagnostic[],
217
225
  userKeys: ReadonlySet<string>,
226
+ emittedCompanions: ReadonlyMap<string, string> = new Map(),
218
227
  ): TranspileProjectResult {
219
228
  const lua: Record<string, string> = {};
220
229
  const sourceMaps: Record<string, string> = {};
230
+ const companions: Record<string, string> = {};
221
231
  let lualib: string | undefined;
222
232
  for (const file of transpiledFiles) {
223
233
  if (isLualibBundle(file) && typeof file.lua === "string") {
@@ -232,6 +242,10 @@ export function collectOutputs(
232
242
  if (typeof file.luaSourceMap === "string") {
233
243
  sourceMaps[userSource.fileName] = file.luaSourceMap;
234
244
  }
245
+ const companion = emittedCompanions.get(userSource.fileName);
246
+ if (companion !== undefined) {
247
+ companions[userSource.fileName] = companion;
248
+ }
235
249
  }
236
250
 
237
251
  const collectedDiagnostics: TranspileDiagnostic[] = diagnostics.map((d) => {
@@ -276,6 +290,7 @@ export function collectOutputs(
276
290
  lua,
277
291
  sourceMaps,
278
292
  diagnostics: collectedDiagnostics,
293
+ ...(Object.keys(companions).length > 0 ? { companions } : {}),
279
294
  ...(lualib !== undefined ? { lualib } : {}),
280
295
  ...(timersImported ? { timersRuntime: TIMERS_RUNTIME } : {}),
281
296
  };
@@ -284,6 +299,7 @@ export function collectOutputs(
284
299
  export function transpileProject(input: TranspileProjectInput): TranspileProjectResult {
285
300
  const userKeys = new Set(Object.keys(input.files));
286
301
  const merged: Record<string, string> = { ...AMBIENT_FILES, ...input.files };
302
+ const companionEmit = createCompanionEmitPlugin();
287
303
 
288
304
  const result = tstl.transpileVirtualProject(merged, {
289
305
  luaTarget: tstl.LuaTarget.Lua51,
@@ -305,10 +321,16 @@ export function transpileProject(input: TranspileProjectInput): TranspileProject
305
321
  { plugin: windowEventGuardLoweringPlugin },
306
322
  { plugin: messageDispatchLoweringPlugin },
307
323
  { plugin: timersLoweringPlugin },
324
+ { plugin: companionEmit.plugin },
308
325
  ],
309
326
  });
310
327
 
311
- return collectOutputs(result.transpiledFiles, result.diagnostics, userKeys);
328
+ return collectOutputs(
329
+ result.transpiledFiles,
330
+ result.diagnostics,
331
+ userKeys,
332
+ companionEmit.companions,
333
+ );
312
334
  }
313
335
 
314
336
  export function transpile(source: string): TranspileResult {