@defold-typescript/transpiler 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { findMainEntryFactoryImports } from "./main-entry-factory-imports";
2
+ export { getProgramDiagnostics } from "./program-diagnostics";
1
3
  export type { TranspileSession } from "./session";
2
4
  export { createTranspileSession } from "./session";
3
5
  export type { TranspileDiagnostic, TranspileProjectInput, TranspileProjectResult, TranspileResult, } from "./transpile";
package/dist/index.js CHANGED
@@ -1,9 +1,5 @@
1
- // src/session.ts
2
- import { readFileSync as readFileSync2 } from "node:fs";
3
- import { createRequire as createRequire2 } from "node:module";
4
- import * as path2 from "node:path";
5
- import * as ts5 from "typescript";
6
- import * as tstl2 from "typescript-to-lua";
1
+ // src/main-entry-factory-imports.ts
2
+ import * as ts3 from "typescript";
7
3
 
8
4
  // src/lifecycle-erasure.ts
9
5
  import * as ts2 from "typescript";
@@ -234,9 +230,12 @@ function initBuilderBody(fn, context) {
234
230
  return [createReturnStatement([context.transformExpression(fn.body)])];
235
231
  }
236
232
  function emitInitMerge(fn, context, property) {
237
- const builder = createFunctionExpression2(createBlock2(initBuilderBody(fn, context)), [], undefined, NodeFlags2.Declaration, fn);
233
+ const authorParam = fn.parameters.find((param) => !isThisParameter2(param) && ts2.isIdentifier(param.name));
234
+ const builderParams = authorParam === undefined ? [] : [context.transformExpression(authorParam.name)];
235
+ const builder = createFunctionExpression2(createBlock2(initBuilderBody(fn, context)), builderParams, undefined, NodeFlags2.Declaration, fn);
238
236
  const builderDecl = createVariableDeclarationStatement2(createIdentifier2("____init"), builder);
239
- const stateDecl = createVariableDeclarationStatement2(createIdentifier2("____s"), createCallExpression2(createIdentifier2("____init"), []));
237
+ const builderArgs = authorParam === undefined ? [] : [createIdentifier2("self")];
238
+ const stateDecl = createVariableDeclarationStatement2(createIdentifier2("____s"), createCallExpression2(createIdentifier2("____init"), builderArgs));
240
239
  const mergeAssignment = createAssignmentStatement(createTableIndexExpression(createIdentifier2("self"), createIdentifier2("____k")), createIdentifier2("____v"));
241
240
  const forIn = createForInStatement(createBlock2([mergeAssignment]), [createIdentifier2("____k"), createIdentifier2("____v")], [createCallExpression2(createIdentifier2("pairs"), [createIdentifier2("____s")])]);
242
241
  const guard = createIfStatement2(createBinaryExpression2(createIdentifier2("____s"), createNilLiteral(), SyntaxKind4.InequalityOperator), createBlock2([forIn]));
@@ -330,8 +329,37 @@ var lifecycleErasurePlugin = {
330
329
  }
331
330
  };
332
331
 
332
+ // src/main-entry-factory-imports.ts
333
+ function findMainEntryFactoryImports(fileName, source) {
334
+ const sourceFile = ts3.createSourceFile(fileName, source, ts3.ScriptTarget.Latest, true);
335
+ const factories = [];
336
+ for (const statement of sourceFile.statements) {
337
+ if (!ts3.isImportDeclaration(statement)) {
338
+ continue;
339
+ }
340
+ const specifier = statement.moduleSpecifier;
341
+ if (!ts3.isStringLiteral(specifier) || specifier.text !== FACTORY_MODULE) {
342
+ continue;
343
+ }
344
+ const bindings = statement.importClause?.namedBindings;
345
+ if (bindings === undefined || !ts3.isNamedImports(bindings)) {
346
+ continue;
347
+ }
348
+ for (const element of bindings.elements) {
349
+ const imported = (element.propertyName ?? element.name).text;
350
+ if (FACTORY_NAMES.has(imported)) {
351
+ factories.push(imported);
352
+ }
353
+ }
354
+ }
355
+ return factories;
356
+ }
357
+ // src/program-diagnostics.ts
358
+ import * as ts6 from "typescript";
359
+ import * as tstl from "typescript-to-lua";
360
+
333
361
  // src/message-guard-lowering.ts
334
- import * as ts3 from "typescript";
362
+ import * as ts4 from "typescript";
335
363
  import {
336
364
  createBinaryExpression as createBinaryExpression3,
337
365
  createCallExpression as createCallExpression3,
@@ -345,7 +373,7 @@ function resolvesToGuardExport(callee, checker) {
345
373
  if (symbol === undefined) {
346
374
  return false;
347
375
  }
348
- if (symbol.flags & ts3.SymbolFlags.Alias) {
376
+ if (symbol.flags & ts4.SymbolFlags.Alias) {
349
377
  symbol = checker.getAliasedSymbol(symbol);
350
378
  }
351
379
  if (symbol.getName() !== GUARD_NAME) {
@@ -359,7 +387,7 @@ function resolvesToGuardExport(callee, checker) {
359
387
  }
360
388
  var messageGuardLoweringPlugin = {
361
389
  visitors: {
362
- [ts3.SyntaxKind.CallExpression]: (node, context) => {
390
+ [ts4.SyntaxKind.CallExpression]: (node, context) => {
363
391
  const [messageIdArg, , expectedArg] = node.arguments;
364
392
  if (node.arguments.length === 3 && messageIdArg !== undefined && expectedArg !== undefined && resolvesToGuardExport(node.expression, context.checker)) {
365
393
  const messageId = context.transformExpression(messageIdArg);
@@ -371,24 +399,117 @@ var messageGuardLoweringPlugin = {
371
399
  }
372
400
  };
373
401
 
402
+ // src/timers-lowering.ts
403
+ import * as ts5 from "typescript";
404
+
405
+ // src/timers-runtime.ts
406
+ var TIMERS_MODULE_SPECIFIER = "@defold-typescript/types/timers";
407
+ var TIMERS_REQUIRE_NAME = "defold_typescript_timers";
408
+ var TIMERS_RUNTIME = `local ____lualib = require("lualib_bundle")
409
+ local __TS__Promise = ____lualib.__TS__Promise
410
+ local __TS__New = ____lualib.__TS__New
411
+
412
+ local function setTimeout(callback, delayMs)
413
+ return timer.delay(delayMs / 1000, false, function()
414
+ callback()
415
+ end)
416
+ end
417
+
418
+ local function setInterval(callback, delayMs)
419
+ return timer.delay(delayMs / 1000, true, function()
420
+ callback()
421
+ end)
422
+ end
423
+
424
+ local function clearTimeout(handle)
425
+ timer.cancel(handle)
426
+ end
427
+
428
+ local function clearInterval(handle)
429
+ timer.cancel(handle)
430
+ end
431
+
432
+ local function wait(seconds)
433
+ return __TS__New(__TS__Promise, function(_, resolve)
434
+ timer.delay(seconds, false, function()
435
+ resolve()
436
+ end)
437
+ end)
438
+ end
439
+
440
+ return {
441
+ setTimeout = setTimeout,
442
+ setInterval = setInterval,
443
+ clearTimeout = clearTimeout,
444
+ clearInterval = clearInterval,
445
+ wait = wait,
446
+ }
447
+ `;
448
+
449
+ // src/timers-lowering.ts
450
+ function isTimersSpecifier(node) {
451
+ return ts5.isStringLiteral(node) && node.text === TIMERS_MODULE_SPECIFIER;
452
+ }
453
+ function importsTimersModule(sourceFile) {
454
+ return sourceFile.statements.some((statement) => ts5.isImportDeclaration(statement) && isTimersSpecifier(statement.moduleSpecifier));
455
+ }
456
+ var TIMERS_REQUIRE_PATH = `@NoResolution:${TIMERS_REQUIRE_NAME}`;
457
+ var timersLoweringPlugin = {
458
+ visitors: {
459
+ [ts5.SyntaxKind.ImportDeclaration]: (node, context) => {
460
+ if (!isTimersSpecifier(node.moduleSpecifier)) {
461
+ return context.superTransformStatements(node);
462
+ }
463
+ const rewritten = ts5.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts5.factory.createStringLiteral(TIMERS_REQUIRE_PATH), node.attributes);
464
+ return context.superTransformStatements(rewritten);
465
+ }
466
+ }
467
+ };
468
+
469
+ // src/program-diagnostics.ts
470
+ var LUA_PLUGINS = [
471
+ lifecycleErasurePlugin,
472
+ messageGuardLoweringPlugin,
473
+ messageDispatchLoweringPlugin,
474
+ timersLoweringPlugin
475
+ ];
476
+ var noopWriteFile = () => {};
477
+ function isAmbient(fileName) {
478
+ return /[\\/]node_modules[\\/]/.test(fileName) || /(^|[\\/])lib\.[^\\/]*\.d\.ts$/.test(fileName);
479
+ }
480
+ function getProgramDiagnostics(program, sourceFile) {
481
+ const { diagnostics } = tstl.getProgramTranspileResult(ts6.sys, noopWriteFile, {
482
+ program,
483
+ plugins: LUA_PLUGINS,
484
+ ...sourceFile ? { sourceFiles: [sourceFile] } : {}
485
+ });
486
+ return diagnostics.filter((d) => d.file !== undefined && !isAmbient(d.file.fileName));
487
+ }
488
+ // src/session.ts
489
+ import { readFileSync as readFileSync2 } from "node:fs";
490
+ import { createRequire as createRequire2 } from "node:module";
491
+ import * as path2 from "node:path";
492
+ import * as ts8 from "typescript";
493
+ import * as tstl3 from "typescript-to-lua";
494
+
374
495
  // src/transpile.ts
375
496
  import { readdirSync, readFileSync } from "node:fs";
376
497
  import { createRequire } from "node:module";
377
498
  import * as path from "node:path";
378
- import * as tstl from "typescript-to-lua";
499
+ import * as tstl2 from "typescript-to-lua";
379
500
 
380
501
  // src/go-property-direct-call.ts
381
- import * as ts4 from "typescript";
502
+ import * as ts7 from "typescript";
382
503
  var GO_PROPERTY_DIRECT_CALL_MESSAGE = "`go.property` called directly is deprecated; declare the property in `defineScript({ properties })` so it types onto `self` (the transpiler emits the registration for you).";
383
504
  function findDirectGoPropertyCalls(sourceFile) {
384
505
  const calls = [];
385
506
  const visit = (node) => {
386
- if (ts4.isCallExpression(node) && ts4.isPropertyAccessExpression(node.expression) && node.expression.name.text === "property" && ts4.isIdentifier(node.expression.expression) && node.expression.expression.text === "go") {
507
+ if (ts7.isCallExpression(node) && ts7.isPropertyAccessExpression(node.expression) && node.expression.name.text === "property" && ts7.isIdentifier(node.expression.expression) && node.expression.expression.text === "go") {
387
508
  calls.push(node);
388
509
  }
389
- ts4.forEachChild(node, visit);
510
+ ts7.forEachChild(node, visit);
390
511
  };
391
- ts4.forEachChild(sourceFile, visit);
512
+ ts7.forEachChild(sourceFile, visit);
392
513
  return calls;
393
514
  }
394
515
 
@@ -405,6 +526,29 @@ var TSTL_LANG_EXT_ROOT = path.dirname(requireFromHere.resolve("@typescript-to-lu
405
526
  function readAmbient(rel) {
406
527
  return readFileSync(path.join(TYPES_PKG_ROOT, rel), "utf8");
407
528
  }
529
+ var LUA_TYPES_ROOT = path.dirname(createRequire(path.join(TYPES_PKG_ROOT, "package.json")).resolve("lua-types/package.json"));
530
+ var LUA_TYPES_ENTRY_FILES = ["5.1.d.ts", "special/jit-only.d.ts"];
531
+ var REFERENCE_PATH_RE = /\/\/\/\s*<reference\s+path="([^"]+)"\s*\/>/g;
532
+ function collectLuaTypesClosure() {
533
+ const files = {};
534
+ const visit = (rel) => {
535
+ const key = `node_modules/lua-types/${rel}`;
536
+ if (key in files)
537
+ return;
538
+ const source = readFileSync(path.join(LUA_TYPES_ROOT, rel), "utf8");
539
+ files[key] = source;
540
+ const dir = path.posix.dirname(rel);
541
+ for (const match of source.matchAll(REFERENCE_PATH_RE)) {
542
+ const ref = match[1];
543
+ if (!ref)
544
+ continue;
545
+ visit(path.posix.normalize(path.posix.join(dir, ref)));
546
+ }
547
+ };
548
+ for (const entry of LUA_TYPES_ENTRY_FILES)
549
+ visit(entry);
550
+ return files;
551
+ }
408
552
  function buildAmbientFiles() {
409
553
  const files = {
410
554
  "node_modules/@typescript-to-lua/language-extensions/index.d.ts": readFileSync(path.join(TSTL_LANG_EXT_ROOT, "index.d.ts"), "utf8"),
@@ -415,6 +559,7 @@ function buildAmbientFiles() {
415
559
  "node_modules/@defold-typescript/types/src/message-guard.d.ts": readAmbient("src/message-guard.d.ts"),
416
560
  "node_modules/@defold-typescript/types/src/message-dispatch.d.ts": readAmbient("src/message-dispatch.d.ts"),
417
561
  "node_modules/@defold-typescript/types/src/lifecycle.ts": readAmbient("src/lifecycle.ts"),
562
+ "node_modules/@defold-typescript/types/src/timers.d.ts": readAmbient("src/timers.d.ts"),
418
563
  "node_modules/@defold-typescript/types/index.ts": [
419
564
  'export { defineGuiScript, defineRenderScript, defineScript } from "./src/lifecycle";',
420
565
  'export type { GuiScriptHooks, InputAction, InputTouch, RenderScriptHooks, ScriptHooks, ScriptProperties, ScriptProperty } from "./src/lifecycle";',
@@ -432,6 +577,7 @@ export type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";
432
577
  export type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";
433
578
  `
434
579
  };
580
+ Object.assign(files, collectLuaTypesClosure());
435
581
  for (const entry of readdirSync(path.join(TYPES_PKG_ROOT, "generated"))) {
436
582
  if (entry.endsWith(".d.ts")) {
437
583
  files[`node_modules/@defold-typescript/types/generated/${entry}`] = readAmbient(`generated/${entry}`);
@@ -440,10 +586,19 @@ export type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";
440
586
  return files;
441
587
  }
442
588
  var AMBIENT_FILES = buildAmbientFiles();
589
+ var LUALIB_BUNDLE_NAME = "lualib_bundle.lua";
590
+ function isLualibBundle(file) {
591
+ return file.sourceFiles.length === 0 && file.outPath.endsWith(LUALIB_BUNDLE_NAME);
592
+ }
443
593
  function collectOutputs(transpiledFiles, diagnostics, userKeys) {
444
594
  const lua = {};
445
595
  const sourceMaps = {};
596
+ let lualib;
446
597
  for (const file of transpiledFiles) {
598
+ if (isLualibBundle(file) && typeof file.lua === "string") {
599
+ lualib = file.lua;
600
+ continue;
601
+ }
447
602
  const userSource = file.sourceFiles.find((s) => userKeys.has(s.fileName));
448
603
  if (!userSource || typeof file.lua !== "string") {
449
604
  continue;
@@ -459,6 +614,7 @@ function collectOutputs(transpiledFiles, diagnostics, userKeys) {
459
614
  return fileName !== undefined && userKeys.has(fileName) ? { file: fileName, message } : { message };
460
615
  });
461
616
  const scanned = new Set;
617
+ let timersImported = false;
462
618
  for (const file of transpiledFiles) {
463
619
  for (const sourceFile of file.sourceFiles) {
464
620
  if (!userKeys.has(sourceFile.fileName) || scanned.has(sourceFile.fileName)) {
@@ -472,22 +628,32 @@ function collectOutputs(transpiledFiles, diagnostics, userKeys) {
472
628
  category: "warning"
473
629
  });
474
630
  }
631
+ if (importsTimersModule(sourceFile)) {
632
+ timersImported = true;
633
+ }
475
634
  }
476
635
  }
477
- return { lua, sourceMaps, diagnostics: collectedDiagnostics };
636
+ return {
637
+ lua,
638
+ sourceMaps,
639
+ diagnostics: collectedDiagnostics,
640
+ ...lualib !== undefined ? { lualib } : {},
641
+ ...timersImported ? { timersRuntime: TIMERS_RUNTIME } : {}
642
+ };
478
643
  }
479
644
  function transpileProject(input) {
480
645
  const userKeys = new Set(Object.keys(input.files));
481
646
  const merged = { ...AMBIENT_FILES, ...input.files };
482
- const result = tstl.transpileVirtualProject(merged, {
483
- luaTarget: tstl.LuaTarget.Lua54,
647
+ const result = tstl2.transpileVirtualProject(merged, {
648
+ luaTarget: tstl2.LuaTarget.Lua51,
484
649
  sourceMap: true,
485
650
  skipLibCheck: true,
486
651
  noImplicitSelf: true,
487
652
  luaPlugins: [
488
653
  { plugin: lifecycleErasurePlugin },
489
654
  { plugin: messageGuardLoweringPlugin },
490
- { plugin: messageDispatchLoweringPlugin }
655
+ { plugin: messageDispatchLoweringPlugin },
656
+ { plugin: timersLoweringPlugin }
491
657
  ]
492
658
  });
493
659
  return collectOutputs(result.transpiledFiles, result.diagnostics, userKeys);
@@ -504,14 +670,15 @@ function transpile(source) {
504
670
  // src/session.ts
505
671
  var requireFromHere2 = createRequire2(import.meta.url);
506
672
  var COMPILER_OPTIONS = {
507
- luaTarget: tstl2.LuaTarget.Lua54,
673
+ luaTarget: tstl3.LuaTarget.Lua51,
508
674
  sourceMap: true,
509
675
  skipLibCheck: true,
510
676
  noImplicitSelf: true,
511
677
  luaPlugins: [
512
678
  { plugin: lifecycleErasurePlugin },
513
679
  { plugin: messageGuardLoweringPlugin },
514
- { plugin: messageDispatchLoweringPlugin }
680
+ { plugin: messageDispatchLoweringPlugin },
681
+ { plugin: timersLoweringPlugin }
515
682
  ]
516
683
  };
517
684
  function normalizeSlashes(p) {
@@ -552,7 +719,7 @@ function createTranspileSession() {
552
719
  if (cached) {
553
720
  return cached;
554
721
  }
555
- const created = ts5.createSourceFile(fileName, content, ts5.ScriptTarget.Latest, false);
722
+ const created = ts8.createSourceFile(fileName, content, ts8.ScriptTarget.Latest, false);
556
723
  sourceFileCache.set(normalized, created);
557
724
  return created;
558
725
  }
@@ -571,7 +738,7 @@ function createTranspileSession() {
571
738
  return cached;
572
739
  }
573
740
  const fileContent = readFileSync2(filePath, "utf8");
574
- const created = ts5.createSourceFile(filePath, fileContent, ts5.ScriptTarget.Latest, false);
741
+ const created = ts8.createSourceFile(filePath, fileContent, ts8.ScriptTarget.Latest, false);
575
742
  libCache.set(fileName, created);
576
743
  return created;
577
744
  }
@@ -584,10 +751,10 @@ function createTranspileSession() {
584
751
  return AMBIENT_FILES[normalized];
585
752
  }
586
753
  const host = {
587
- fileExists: (fileName) => mergedContent(normalizeSlashes(fileName)) !== undefined || ts5.sys.fileExists(fileName),
754
+ fileExists: (fileName) => mergedContent(normalizeSlashes(fileName)) !== undefined || ts8.sys.fileExists(fileName),
588
755
  getCanonicalFileName: (fileName) => fileName,
589
756
  getCurrentDirectory: () => "",
590
- getDefaultLibFileName: ts5.getDefaultLibFileName,
757
+ getDefaultLibFileName: ts8.getDefaultLibFileName,
591
758
  readFile: () => "",
592
759
  getNewLine: () => `
593
760
  `,
@@ -608,15 +775,15 @@ function createTranspileSession() {
608
775
  }
609
776
  const userKeys = new Set(userFiles.keys());
610
777
  const rootNames = [...Object.keys(AMBIENT_FILES).map(normalizeSlashes), ...userKeys];
611
- program = ts5.createProgram(rootNames, COMPILER_OPTIONS, host, program);
612
- const preEmitDiagnostics = ts5.getPreEmitDiagnostics(program);
778
+ program = ts8.createProgram(rootNames, COMPILER_OPTIONS, host, program);
779
+ const preEmitDiagnostics = ts8.getPreEmitDiagnostics(program);
613
780
  const collector = createOutputCollector();
614
- const { diagnostics: transpileDiagnostics } = new tstl2.Transpiler().emit({
781
+ const { diagnostics: transpileDiagnostics } = new tstl3.Transpiler().emit({
615
782
  program,
616
783
  writeFile: collector.writeFile
617
784
  });
618
785
  const diagnostics = [
619
- ...ts5.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics])
786
+ ...ts8.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics])
620
787
  ];
621
788
  return collectOutputs(collector.files, diagnostics, userKeys);
622
789
  }
@@ -628,5 +795,7 @@ function createTranspileSession() {
628
795
  export {
629
796
  transpileProject,
630
797
  transpile,
798
+ getProgramDiagnostics,
799
+ findMainEntryFactoryImports,
631
800
  createTranspileSession
632
801
  };
@@ -1,4 +1,6 @@
1
1
  import * as ts from "typescript";
2
2
  import { type Plugin } from "typescript-to-lua";
3
+ export declare const FACTORY_MODULE = "@defold-typescript/types";
4
+ export declare const FACTORY_NAMES: Set<string>;
3
5
  export declare function isFactoryOnlyImport(node: ts.ImportDeclaration): boolean;
4
6
  export declare const lifecycleErasurePlugin: Plugin;
@@ -0,0 +1 @@
1
+ export declare function findMainEntryFactoryImports(fileName: string, source: string): string[];
@@ -0,0 +1,2 @@
1
+ import * as ts from "typescript";
2
+ export declare function getProgramDiagnostics(program: ts.Program, sourceFile?: ts.SourceFile): ts.Diagnostic[];
@@ -0,0 +1,4 @@
1
+ import * as ts from "typescript";
2
+ import type { Plugin } from "typescript-to-lua";
3
+ export declare function importsTimersModule(sourceFile: ts.SourceFile): boolean;
4
+ export declare const timersLoweringPlugin: Plugin;
@@ -0,0 +1,3 @@
1
+ export declare const TIMERS_MODULE_SPECIFIER = "@defold-typescript/types/timers";
2
+ export declare const TIMERS_REQUIRE_NAME = "defold_typescript_timers";
3
+ export declare const TIMERS_RUNTIME = "local ____lualib = require(\"lualib_bundle\")\nlocal __TS__Promise = ____lualib.__TS__Promise\nlocal __TS__New = ____lualib.__TS__New\n\nlocal function setTimeout(callback, delayMs)\n return timer.delay(delayMs / 1000, false, function()\n callback()\n end)\nend\n\nlocal function setInterval(callback, delayMs)\n return timer.delay(delayMs / 1000, true, function()\n callback()\n end)\nend\n\nlocal function clearTimeout(handle)\n timer.cancel(handle)\nend\n\nlocal function clearInterval(handle)\n timer.cancel(handle)\nend\n\nlocal function wait(seconds)\n return __TS__New(__TS__Promise, function(_, resolve)\n timer.delay(seconds, false, function()\n resolve()\n end)\n end)\nend\n\nreturn {\n setTimeout = setTimeout,\n setInterval = setInterval,\n clearTimeout = clearTimeout,\n clearInterval = clearInterval,\n wait = wait,\n}\n";
@@ -16,9 +16,12 @@ export interface TranspileProjectResult {
16
16
  readonly lua: Readonly<Record<string, string>>;
17
17
  readonly sourceMaps: Readonly<Record<string, string>>;
18
18
  readonly diagnostics: readonly TranspileDiagnostic[];
19
+ readonly lualib?: string;
20
+ readonly timersRuntime?: string;
19
21
  }
20
22
  export declare const AMBIENT_FILES: Readonly<Record<string, string>>;
21
23
  interface CollectableFile {
24
+ readonly outPath: string;
22
25
  readonly sourceFiles: readonly ts.SourceFile[];
23
26
  readonly lua?: string;
24
27
  readonly luaSourceMap?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/transpiler",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "TypeScript-to-Lua build pipeline tuned for Defold's runtime.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "test": "bun test"
36
36
  },
37
37
  "dependencies": {
38
- "@defold-typescript/types": "0.7.0",
38
+ "@defold-typescript/types": "0.8.1",
39
39
  "@typescript-to-lua/language-extensions": "1.19.0",
40
40
  "typescript-to-lua": "^1.36.0"
41
41
  }
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { findMainEntryFactoryImports } from "./main-entry-factory-imports";
2
+ export { getProgramDiagnostics } from "./program-diagnostics";
1
3
  export type { TranspileSession } from "./session";
2
4
  export { createTranspileSession } from "./session";
3
5
  export type {
@@ -23,8 +23,8 @@ import {
23
23
  } from "typescript-to-lua";
24
24
  import { resolvesToDispatchExport } from "./message-dispatch-lowering";
25
25
 
26
- const FACTORY_MODULE = "@defold-typescript/types";
27
- const FACTORY_NAMES = new Set(["defineScript", "defineGuiScript", "defineRenderScript"]);
26
+ export const FACTORY_MODULE = "@defold-typescript/types";
27
+ export const FACTORY_NAMES = new Set(["defineScript", "defineGuiScript", "defineRenderScript"]);
28
28
  // A walled source imports its factory from the matching kind subpath
29
29
  // (`@defold-typescript/types/gui-script`) to avoid pulling the cross-kind
30
30
  // `declare global` namespaces; those imports erase exactly like the bare one.
@@ -155,18 +155,31 @@ function emitInitMerge(
155
155
  context: TransformationContext,
156
156
  property: ts.ObjectLiteralElementLike,
157
157
  ): Statement[] {
158
+ // init-self: Defold has already populated `self` with the declared property
159
+ // values when `init` runs, so an `init(self)` body must resolve `self` to the
160
+ // engine table — not a nil global. Bind the builder's parameter to the
161
+ // author's first non-`this:` param (which may be named anything) and pass the
162
+ // engine `self` at the call. A param-less `init` keeps the zero-arg builder.
163
+ const authorParam = fn.parameters.find(
164
+ (param) => !isThisParameter(param) && ts.isIdentifier(param.name),
165
+ );
166
+ const builderParams: Identifier[] =
167
+ authorParam === undefined
168
+ ? []
169
+ : [context.transformExpression(authorParam.name as ts.Identifier) as Identifier];
158
170
  const builder = createFunctionExpression(
159
171
  createBlock(initBuilderBody(fn, context)),
160
- [],
172
+ builderParams,
161
173
  undefined,
162
174
  NodeFlags.Declaration,
163
175
  fn,
164
176
  );
165
177
  const builderDecl = createVariableDeclarationStatement(createIdentifier("____init"), builder);
166
178
 
179
+ const builderArgs = authorParam === undefined ? [] : [createIdentifier("self")];
167
180
  const stateDecl = createVariableDeclarationStatement(
168
181
  createIdentifier("____s"),
169
- createCallExpression(createIdentifier("____init"), []),
182
+ createCallExpression(createIdentifier("____init"), builderArgs),
170
183
  );
171
184
  const mergeAssignment = createAssignmentStatement(
172
185
  createTableIndexExpression(createIdentifier("self"), createIdentifier("____k")),
@@ -0,0 +1,31 @@
1
+ import * as ts from "typescript";
2
+ import { FACTORY_MODULE, FACTORY_NAMES } from "./lifecycle-erasure";
3
+
4
+ // A walled source narrows only its ambient `types`; importing a factory from the
5
+ // bare main entry (`@defold-typescript/types`) still pulls the cross-kind
6
+ // `declare global` namespaces and silently re-defeats the wall. The sanctioned
7
+ // `@defold-typescript/types/<kind>` subpath imports are never flagged.
8
+ export function findMainEntryFactoryImports(fileName: string, source: string): string[] {
9
+ const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
10
+ const factories: string[] = [];
11
+ for (const statement of sourceFile.statements) {
12
+ if (!ts.isImportDeclaration(statement)) {
13
+ continue;
14
+ }
15
+ const specifier = statement.moduleSpecifier;
16
+ if (!ts.isStringLiteral(specifier) || specifier.text !== FACTORY_MODULE) {
17
+ continue;
18
+ }
19
+ const bindings = statement.importClause?.namedBindings;
20
+ if (bindings === undefined || !ts.isNamedImports(bindings)) {
21
+ continue;
22
+ }
23
+ for (const element of bindings.elements) {
24
+ const imported = (element.propertyName ?? element.name).text;
25
+ if (FACTORY_NAMES.has(imported)) {
26
+ factories.push(imported);
27
+ }
28
+ }
29
+ }
30
+ return factories;
31
+ }
@@ -0,0 +1,37 @@
1
+ import * as ts from "typescript";
2
+ import * as tstl from "typescript-to-lua";
3
+ import { lifecycleErasurePlugin } from "./lifecycle-erasure";
4
+ import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
5
+ import { messageGuardLoweringPlugin } from "./message-guard-lowering";
6
+ import { timersLoweringPlugin } from "./timers-lowering";
7
+
8
+ // The same four lua plugins the build path lists (transpile.ts / session.ts),
9
+ // so the editor and the build agree on what is unsupported.
10
+ const LUA_PLUGINS: tstl.Plugin[] = [
11
+ lifecycleErasurePlugin,
12
+ messageGuardLoweringPlugin,
13
+ messageDispatchLoweringPlugin,
14
+ timersLoweringPlugin,
15
+ ];
16
+
17
+ const noopWriteFile: ts.WriteFileCallback = () => {};
18
+
19
+ function isAmbient(fileName: string): boolean {
20
+ return /[\\/]node_modules[\\/]/.test(fileName) || /(^|[\\/])lib\.[^\\/]*\.d\.ts$/.test(fileName);
21
+ }
22
+
23
+ // Run the build path's TSTL diagnostic pass against an already-open `ts.Program`
24
+ // (e.g. the editor's), returning the raw, span-bearing diagnostics so a squiggle
25
+ // can land on the offending source. Restricts to `sourceFile` when given and
26
+ // drops diagnostics on lib/`node_modules` ambients.
27
+ export function getProgramDiagnostics(
28
+ program: ts.Program,
29
+ sourceFile?: ts.SourceFile,
30
+ ): ts.Diagnostic[] {
31
+ const { diagnostics } = tstl.getProgramTranspileResult(ts.sys, noopWriteFile, {
32
+ program,
33
+ plugins: LUA_PLUGINS,
34
+ ...(sourceFile ? { sourceFiles: [sourceFile] } : {}),
35
+ });
36
+ return diagnostics.filter((d) => d.file !== undefined && !isAmbient(d.file.fileName));
37
+ }
package/src/session.ts CHANGED
@@ -6,6 +6,7 @@ import * as tstl from "typescript-to-lua";
6
6
  import { lifecycleErasurePlugin } from "./lifecycle-erasure";
7
7
  import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
8
8
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
9
+ import { timersLoweringPlugin } from "./timers-lowering";
9
10
  import { AMBIENT_FILES, collectOutputs, type TranspileProjectResult } from "./transpile";
10
11
 
11
12
  export interface TranspileSession {
@@ -16,7 +17,7 @@ export interface TranspileSession {
16
17
  const requireFromHere = createRequire(import.meta.url);
17
18
 
18
19
  const COMPILER_OPTIONS: tstl.CompilerOptions = {
19
- luaTarget: tstl.LuaTarget.Lua54,
20
+ luaTarget: tstl.LuaTarget.Lua51,
20
21
  sourceMap: true,
21
22
  // Don't cross-check the seeded ambient .d.ts surface against itself; only user
22
23
  // files matter (mirrors transpileProject and the editor's skipLibCheck).
@@ -28,6 +29,7 @@ const COMPILER_OPTIONS: tstl.CompilerOptions = {
28
29
  { plugin: lifecycleErasurePlugin },
29
30
  { plugin: messageGuardLoweringPlugin },
30
31
  { plugin: messageDispatchLoweringPlugin },
32
+ { plugin: timersLoweringPlugin },
31
33
  ],
32
34
  };
33
35
 
@@ -0,0 +1,43 @@
1
+ import * as ts from "typescript";
2
+ import type { Plugin } from "typescript-to-lua";
3
+ import { TIMERS_MODULE_SPECIFIER, TIMERS_REQUIRE_NAME } from "./timers-runtime";
4
+
5
+ function isTimersSpecifier(node: ts.Expression): node is ts.StringLiteral {
6
+ return ts.isStringLiteral(node) && node.text === TIMERS_MODULE_SPECIFIER;
7
+ }
8
+
9
+ // True when a user source imports the polyfill module, so `collectOutputs` can
10
+ // surface the runtime pay-for-use. The original specifier is read here (the
11
+ // lowering rewrites a fresh node for emit, not the source AST).
12
+ export function importsTimersModule(sourceFile: ts.SourceFile): boolean {
13
+ return sourceFile.statements.some(
14
+ (statement) =>
15
+ ts.isImportDeclaration(statement) && isTimersSpecifier(statement.moduleSpecifier),
16
+ );
17
+ }
18
+
19
+ // Rewrite the dotted package specifier to the flat `require` name so TSTL emits
20
+ // `require("defold_typescript_timers")` instead of the unresolvable default.
21
+ // The `@NoResolution:` marker tells TSTL's resolver to leave the path alone (the
22
+ // runtime module has no source file in the program); the resolver strips the
23
+ // marker, leaving the bare flat require. The import clause is the original node,
24
+ // so the checker/resolver still bind the named imports.
25
+ const TIMERS_REQUIRE_PATH = `@NoResolution:${TIMERS_REQUIRE_NAME}`;
26
+
27
+ export const timersLoweringPlugin: Plugin = {
28
+ visitors: {
29
+ [ts.SyntaxKind.ImportDeclaration]: (node, context) => {
30
+ if (!isTimersSpecifier(node.moduleSpecifier)) {
31
+ return context.superTransformStatements(node);
32
+ }
33
+ const rewritten = ts.factory.updateImportDeclaration(
34
+ node,
35
+ node.modifiers,
36
+ node.importClause,
37
+ ts.factory.createStringLiteral(TIMERS_REQUIRE_PATH),
38
+ node.attributes,
39
+ );
40
+ return context.superTransformStatements(rewritten);
41
+ },
42
+ },
43
+ };
@@ -0,0 +1,52 @@
1
+ // The importable polyfill specifier (matched by the type checker and the
2
+ // lowering pass) and the flat Lua require name the import is rewritten to. The
3
+ // CLI writes the runtime to `<require name>.lua` at the output root, mirroring
4
+ // `lualib_bundle`, so `require("defold_typescript_timers")` resolves in Defold.
5
+ export const TIMERS_MODULE_SPECIFIER = "@defold-typescript/types/timers";
6
+ export const TIMERS_REQUIRE_NAME = "defold_typescript_timers";
7
+
8
+ // Hand-authored runtime Lua. Unlike `defineScript` (which `lifecycle-erasure`
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).
13
+ export const TIMERS_RUNTIME = `local ____lualib = require("lualib_bundle")
14
+ local __TS__Promise = ____lualib.__TS__Promise
15
+ local __TS__New = ____lualib.__TS__New
16
+
17
+ local function setTimeout(callback, delayMs)
18
+ return timer.delay(delayMs / 1000, false, function()
19
+ callback()
20
+ end)
21
+ end
22
+
23
+ local function setInterval(callback, delayMs)
24
+ return timer.delay(delayMs / 1000, true, function()
25
+ callback()
26
+ end)
27
+ end
28
+
29
+ local function clearTimeout(handle)
30
+ timer.cancel(handle)
31
+ end
32
+
33
+ local function clearInterval(handle)
34
+ timer.cancel(handle)
35
+ end
36
+
37
+ local function wait(seconds)
38
+ return __TS__New(__TS__Promise, function(_, resolve)
39
+ timer.delay(seconds, false, function()
40
+ resolve()
41
+ end)
42
+ end)
43
+ end
44
+
45
+ return {
46
+ setTimeout = setTimeout,
47
+ setInterval = setInterval,
48
+ clearTimeout = clearTimeout,
49
+ clearInterval = clearInterval,
50
+ wait = wait,
51
+ }
52
+ `;
package/src/transpile.ts CHANGED
@@ -10,6 +10,8 @@ import {
10
10
  import { lifecycleErasurePlugin } from "./lifecycle-erasure";
11
11
  import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
12
12
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
13
+ import { importsTimersModule, timersLoweringPlugin } from "./timers-lowering";
14
+ import { TIMERS_RUNTIME } from "./timers-runtime";
13
15
 
14
16
  export interface TranspileResult {
15
17
  readonly lua: string;
@@ -34,6 +36,17 @@ export interface TranspileProjectResult {
34
36
  readonly lua: Readonly<Record<string, string>>;
35
37
  readonly sourceMaps: Readonly<Record<string, string>>;
36
38
  readonly diagnostics: readonly TranspileDiagnostic[];
39
+ // TSTL synthesizes this bundle (no user source) whenever a lualib feature
40
+ // (`Object.keys`, spread, `__TS__TypeOf`, ...) is used; the emitted
41
+ // `require("lualib_bundle")` only resolves in Defold if the CLI writes it to
42
+ // the output root. Absent when no feature pulls it in.
43
+ readonly lualib?: string;
44
+ // Hand-authored runtime Lua for the `@defold-typescript/types/timers`
45
+ // polyfills (`setTimeout`/`wait`/...). Unlike `defineScript`, the import is
46
+ // not erased — it lowers to `require("defold_typescript_timers")`, so the CLI
47
+ // must write this to the output root. Present only when a user file imports
48
+ // the module (pay-for-use).
49
+ readonly timersRuntime?: string;
37
50
  }
38
51
 
39
52
  function flattenDiagnosticMessage(
@@ -57,6 +70,36 @@ function readAmbient(rel: string): string {
57
70
  return readFileSync(path.join(TYPES_PKG_ROOT, rel), "utf8");
58
71
  }
59
72
 
73
+ // lua-types is a dependency of @defold-typescript/types, not the transpiler, so
74
+ // resolve it from the types package root where it is installed.
75
+ const LUA_TYPES_ROOT = path.dirname(
76
+ createRequire(path.join(TYPES_PKG_ROOT, "package.json")).resolve("lua-types/package.json"),
77
+ );
78
+
79
+ // Follow the triple-slash `path` reference graph from the 5.1 + LuaJIT entry
80
+ // files so the virtual program sees exactly the Lua 5.1 surface (single-arg
81
+ // `randomseed`) and never the conflicting 5.2–5.5 global declarations.
82
+ const LUA_TYPES_ENTRY_FILES = ["5.1.d.ts", "special/jit-only.d.ts"];
83
+ const REFERENCE_PATH_RE = /\/\/\/\s*<reference\s+path="([^"]+)"\s*\/>/g;
84
+
85
+ function collectLuaTypesClosure(): Record<string, string> {
86
+ const files: Record<string, string> = {};
87
+ const visit = (rel: string): void => {
88
+ const key = `node_modules/lua-types/${rel}`;
89
+ if (key in files) return;
90
+ const source = readFileSync(path.join(LUA_TYPES_ROOT, rel), "utf8");
91
+ files[key] = source;
92
+ const dir = path.posix.dirname(rel);
93
+ for (const match of source.matchAll(REFERENCE_PATH_RE)) {
94
+ const ref = match[1];
95
+ if (!ref) continue;
96
+ visit(path.posix.normalize(path.posix.join(dir, ref)));
97
+ }
98
+ };
99
+ for (const entry of LUA_TYPES_ENTRY_FILES) visit(entry);
100
+ return files;
101
+ }
102
+
60
103
  function buildAmbientFiles(): Record<string, string> {
61
104
  const files: Record<string, string> = {
62
105
  "node_modules/@typescript-to-lua/language-extensions/index.d.ts": readFileSync(
@@ -76,6 +119,10 @@ function buildAmbientFiles(): Record<string, string> {
76
119
  "src/message-dispatch.d.ts",
77
120
  ),
78
121
  "node_modules/@defold-typescript/types/src/lifecycle.ts": readAmbient("src/lifecycle.ts"),
122
+ // Ambient `declare module "@defold-typescript/types/timers"` so user code
123
+ // type-resolves the polyfill import in the virtual program; the lowering
124
+ // rewrites the specifier to a flat require at emit.
125
+ "node_modules/@defold-typescript/types/src/timers.d.ts": readAmbient("src/timers.d.ts"),
79
126
  // Mirror the consumer-facing re-exports of the real package index so a user
80
127
  // file can `import { defineScript }` and `import type { Hash, Vector3 }`.
81
128
  "node_modules/@defold-typescript/types/index.ts": [
@@ -97,6 +144,9 @@ function buildAmbientFiles(): Record<string, string> {
97
144
  "node_modules/@defold-typescript/types/render-script.d.ts":
98
145
  'export { defineRenderScript } from "./src/lifecycle.js";\nexport type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";\n',
99
146
  };
147
+ // Seed the Lua 5.1 standard library (math/os/string/table/coroutine + base
148
+ // globals) so user code can call e.g. `math.randomseed(os.time())`.
149
+ Object.assign(files, collectLuaTypesClosure());
100
150
  // Seed every generated namespace so real multi-namespace user code (sprite,
101
151
  // physics, label, ...) resolves — not just the historical vmath/msg/go subset.
102
152
  for (const entry of readdirSync(path.join(TYPES_PKG_ROOT, "generated"))) {
@@ -112,11 +162,18 @@ function buildAmbientFiles(): Record<string, string> {
112
162
  export const AMBIENT_FILES: Readonly<Record<string, string>> = buildAmbientFiles();
113
163
 
114
164
  interface CollectableFile {
165
+ readonly outPath: string;
115
166
  readonly sourceFiles: readonly ts.SourceFile[];
116
167
  readonly lua?: string;
117
168
  readonly luaSourceMap?: string;
118
169
  }
119
170
 
171
+ const LUALIB_BUNDLE_NAME = "lualib_bundle.lua";
172
+
173
+ function isLualibBundle(file: CollectableFile): boolean {
174
+ return file.sourceFiles.length === 0 && file.outPath.endsWith(LUALIB_BUNDLE_NAME);
175
+ }
176
+
120
177
  export function collectOutputs(
121
178
  transpiledFiles: readonly CollectableFile[],
122
179
  diagnostics: readonly ts.Diagnostic[],
@@ -124,7 +181,12 @@ export function collectOutputs(
124
181
  ): TranspileProjectResult {
125
182
  const lua: Record<string, string> = {};
126
183
  const sourceMaps: Record<string, string> = {};
184
+ let lualib: string | undefined;
127
185
  for (const file of transpiledFiles) {
186
+ if (isLualibBundle(file) && typeof file.lua === "string") {
187
+ lualib = file.lua;
188
+ continue;
189
+ }
128
190
  const userSource = file.sourceFiles.find((s) => userKeys.has(s.fileName));
129
191
  if (!userSource || typeof file.lua !== "string") {
130
192
  continue;
@@ -144,9 +206,11 @@ export function collectOutputs(
144
206
  });
145
207
 
146
208
  // Advisory scan of the user TypeScript AST for the deprecated direct
147
- // `go.property` call. Run here (shared by transpileProject and the watch
148
- // session) so both paths report it identically.
209
+ // `go.property` call, plus detection of the timers polyfill import. Run here
210
+ // (shared by transpileProject and the watch session) so both paths report it
211
+ // identically.
149
212
  const scanned = new Set<string>();
213
+ let timersImported = false;
150
214
  for (const file of transpiledFiles) {
151
215
  for (const sourceFile of file.sourceFiles) {
152
216
  if (!userKeys.has(sourceFile.fileName) || scanned.has(sourceFile.fileName)) {
@@ -160,10 +224,19 @@ export function collectOutputs(
160
224
  category: "warning",
161
225
  });
162
226
  }
227
+ if (importsTimersModule(sourceFile)) {
228
+ timersImported = true;
229
+ }
163
230
  }
164
231
  }
165
232
 
166
- return { lua, sourceMaps, diagnostics: collectedDiagnostics };
233
+ return {
234
+ lua,
235
+ sourceMaps,
236
+ diagnostics: collectedDiagnostics,
237
+ ...(lualib !== undefined ? { lualib } : {}),
238
+ ...(timersImported ? { timersRuntime: TIMERS_RUNTIME } : {}),
239
+ };
167
240
  }
168
241
 
169
242
  export function transpileProject(input: TranspileProjectInput): TranspileProjectResult {
@@ -171,7 +244,7 @@ export function transpileProject(input: TranspileProjectInput): TranspileProject
171
244
  const merged: Record<string, string> = { ...AMBIENT_FILES, ...input.files };
172
245
 
173
246
  const result = tstl.transpileVirtualProject(merged, {
174
- luaTarget: tstl.LuaTarget.Lua54,
247
+ luaTarget: tstl.LuaTarget.Lua51,
175
248
  sourceMap: true,
176
249
  // Don't cross-check the seeded ambient .d.ts surface against itself; we only
177
250
  // care about diagnostics on user files (mirrors the editor's skipLibCheck).
@@ -183,6 +256,7 @@ export function transpileProject(input: TranspileProjectInput): TranspileProject
183
256
  { plugin: lifecycleErasurePlugin },
184
257
  { plugin: messageGuardLoweringPlugin },
185
258
  { plugin: messageDispatchLoweringPlugin },
259
+ { plugin: timersLoweringPlugin },
186
260
  ],
187
261
  });
188
262