@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 +2 -0
- package/dist/index.js +199 -30
- package/dist/lifecycle-erasure.d.ts +2 -0
- package/dist/main-entry-factory-imports.d.ts +1 -0
- package/dist/program-diagnostics.d.ts +2 -0
- package/dist/timers-lowering.d.ts +4 -0
- package/dist/timers-runtime.d.ts +3 -0
- package/dist/transpile.d.ts +3 -0
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/lifecycle-erasure.ts +17 -4
- package/src/main-entry-factory-imports.ts +31 -0
- package/src/program-diagnostics.ts +37 -0
- package/src/session.ts +3 -1
- package/src/timers-lowering.ts +43 -0
- package/src/timers-runtime.ts +52 -0
- package/src/transpile.ts +78 -4
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/
|
|
2
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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 &
|
|
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
|
-
[
|
|
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
|
|
499
|
+
import * as tstl2 from "typescript-to-lua";
|
|
379
500
|
|
|
380
501
|
// src/go-property-direct-call.ts
|
|
381
|
-
import * as
|
|
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 (
|
|
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
|
-
|
|
510
|
+
ts7.forEachChild(node, visit);
|
|
390
511
|
};
|
|
391
|
-
|
|
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 {
|
|
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 =
|
|
483
|
-
luaTarget:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 ||
|
|
754
|
+
fileExists: (fileName) => mergedContent(normalizeSlashes(fileName)) !== undefined || ts8.sys.fileExists(fileName),
|
|
588
755
|
getCanonicalFileName: (fileName) => fileName,
|
|
589
756
|
getCurrentDirectory: () => "",
|
|
590
|
-
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 =
|
|
612
|
-
const preEmitDiagnostics =
|
|
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
|
|
781
|
+
const { diagnostics: transpileDiagnostics } = new tstl3.Transpiler().emit({
|
|
615
782
|
program,
|
|
616
783
|
writeFile: collector.writeFile
|
|
617
784
|
});
|
|
618
785
|
const diagnostics = [
|
|
619
|
-
...
|
|
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,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";
|
package/dist/transpile.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
package/src/lifecycle-erasure.ts
CHANGED
|
@@ -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.
|
|
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
|
|
148
|
-
// session) so both paths report it
|
|
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 {
|
|
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.
|
|
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
|
|