@defold-typescript/transpiler 0.51.0 → 0.53.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/dist/companion-closure.d.ts +7 -0
- package/dist/debug-bootstrap.d.ts +21 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +488 -242
- package/dist/lifecycle-erasure.d.ts +1 -0
- package/dist/transpile.d.ts +3 -0
- package/package.json +5 -2
- package/src/companion-closure.ts +28 -6
- package/src/companion-emit.ts +35 -4
- package/src/debug-bootstrap.ts +88 -0
- package/src/hsm-builtin.ts +2 -2
- package/src/index.ts +8 -0
- package/src/lifecycle-erasure.ts +233 -8
- package/src/timers-runtime.ts +2 -2
- package/src/transpile.ts +48 -12
|
@@ -3,6 +3,7 @@ import { type Plugin } from "typescript-to-lua";
|
|
|
3
3
|
export declare const FACTORY_MODULE = "@defold-typescript/types";
|
|
4
4
|
export declare const FACTORY_NAMES: Set<string>;
|
|
5
5
|
export declare function resolvesToFactoryExport(callee: ts.Expression, checker: ts.TypeChecker): boolean;
|
|
6
|
+
export declare const RESOURCE_PROPERTY_MARKERS: ReadonlySet<string>;
|
|
6
7
|
export interface PropertyMember {
|
|
7
8
|
readonly name: string;
|
|
8
9
|
readonly initializer: ts.Expression;
|
package/dist/transpile.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface TranspileProjectResult {
|
|
|
22
22
|
readonly lualib?: string;
|
|
23
23
|
readonly timersRuntime?: string;
|
|
24
24
|
readonly hsmModules?: Readonly<Record<string, string>>;
|
|
25
|
+
readonly hsmSourceMaps?: Readonly<Record<string, string>>;
|
|
25
26
|
}
|
|
26
27
|
export declare const AMBIENT_FILES: Readonly<Record<string, string>>;
|
|
27
28
|
interface CollectableFile {
|
|
@@ -42,5 +43,7 @@ export declare function transpileProject(input: TranspileProjectInput): Transpil
|
|
|
42
43
|
* lualib bundle.
|
|
43
44
|
*/
|
|
44
45
|
export declare function compileHsmModules(): Readonly<Record<string, string>>;
|
|
46
|
+
/** The source map of every module `compileHsmModules` returns, under the same keys. */
|
|
47
|
+
export declare function compileHsmSourceMaps(): Readonly<Record<string, string>>;
|
|
45
48
|
export declare function transpile(source: string): TranspileResult;
|
|
46
49
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defold-typescript/transpiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "TypeScript-to-Lua build pipeline tuned for Defold's runtime.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,9 +35,12 @@
|
|
|
35
35
|
"test": "bun test"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@defold-typescript/types": "0.
|
|
38
|
+
"@defold-typescript/types": "0.53.0",
|
|
39
39
|
"@typescript-to-lua/language-extensions": "1.19.0",
|
|
40
40
|
"typescript": "6.0.2",
|
|
41
41
|
"typescript-to-lua": "^1.36.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"lua-wasm-bindings": "0.5.3"
|
|
42
45
|
}
|
|
43
46
|
}
|
package/src/companion-closure.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
|
+
import { findManagedBootstrap } from "./debug-bootstrap";
|
|
2
3
|
import { resolvesToFactoryExport } from "./lifecycle-erasure";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -49,6 +50,13 @@ export interface CompanionClosure {
|
|
|
49
50
|
* companion takes its own `require` of the same module instead.
|
|
50
51
|
*/
|
|
51
52
|
readonly importBindings: readonly string[];
|
|
53
|
+
/**
|
|
54
|
+
* The unedited `setup-debug` managed block's statements, in source order, or
|
|
55
|
+
* empty. They are exempt from the order rule because the emit runs them ahead
|
|
56
|
+
* of the companion's `require`, so the debugger starts before anything the
|
|
57
|
+
* companion loads.
|
|
58
|
+
*/
|
|
59
|
+
readonly bootstrap: readonly ts.Statement[];
|
|
52
60
|
/** Shapes a split cannot preserve. Empty means the source is splittable. */
|
|
53
61
|
readonly violations: readonly ClosureViolation[];
|
|
54
62
|
}
|
|
@@ -717,11 +725,20 @@ export function computeCompanionClosure(
|
|
|
717
725
|
});
|
|
718
726
|
}
|
|
719
727
|
|
|
728
|
+
const managed = findManagedBootstrap(sourceFile);
|
|
729
|
+
const bootstrap = managed?.canonical === true ? managed.statements : [];
|
|
730
|
+
const editedBlock = new Set(managed?.canonical === false ? managed.statements : []);
|
|
731
|
+
const exempt = new Set(bootstrap);
|
|
732
|
+
|
|
720
733
|
// The companion loads as a unit, so the constraint is pairwise: any effectful
|
|
721
734
|
// script statement with any closure declaration below it is reordered by the
|
|
722
735
|
// split, whatever the first member's position.
|
|
723
736
|
for (const statement of sourceFile.statements) {
|
|
724
|
-
if (
|
|
737
|
+
if (
|
|
738
|
+
closureStatements.has(statement) ||
|
|
739
|
+
exempt.has(statement) ||
|
|
740
|
+
isEffectFreeStatement(statement, checker)
|
|
741
|
+
) {
|
|
725
742
|
continue;
|
|
726
743
|
}
|
|
727
744
|
const start = statement.getStart(sourceFile);
|
|
@@ -735,14 +752,18 @@ export function computeCompanionClosure(
|
|
|
735
752
|
siteOf(statement, "runs in the script"),
|
|
736
753
|
siteOf(hoisted.declaration.node, "moves into the companion"),
|
|
737
754
|
];
|
|
755
|
+
const name = hoisted.declaration.name;
|
|
738
756
|
violations.push({
|
|
739
757
|
kind: "initialization-order",
|
|
740
|
-
member:
|
|
758
|
+
member: name,
|
|
741
759
|
sites,
|
|
742
|
-
message:
|
|
743
|
-
`top-level work
|
|
744
|
-
|
|
745
|
-
|
|
760
|
+
message: editedBlock.has(statement)
|
|
761
|
+
? `the setup-debug managed block was edited and now runs top-level work before "${name}", ` +
|
|
762
|
+
`which the companion would run first (${formatSites(sites)}). ` +
|
|
763
|
+
"Run `defold-typescript setup-debug` to restore the block."
|
|
764
|
+
: `top-level work precedes "${name}", which the companion would run first, ` +
|
|
765
|
+
`reversing their order (${formatSites(sites)}). ` +
|
|
766
|
+
"Move that work below the companion's declarations, or into a lifecycle hook.",
|
|
746
767
|
});
|
|
747
768
|
}
|
|
748
769
|
|
|
@@ -756,6 +777,7 @@ export function computeCompanionClosure(
|
|
|
756
777
|
internals: ordered
|
|
757
778
|
.filter(({ symbol }) => !exportSet.has(symbol) && scriptReferences.has(symbol))
|
|
758
779
|
.map(({ declaration }) => declaration.name),
|
|
780
|
+
bootstrap,
|
|
759
781
|
violations,
|
|
760
782
|
};
|
|
761
783
|
}
|
package/src/companion-emit.ts
CHANGED
|
@@ -66,6 +66,8 @@ function isRuntimeScriptSource(sourceFile: ts.SourceFile, checker: ts.TypeChecke
|
|
|
66
66
|
interface CompanionPlan {
|
|
67
67
|
/** Full start of each top-level statement the companion takes over. */
|
|
68
68
|
readonly closureStarts: ReadonlySet<number>;
|
|
69
|
+
/** Full start of each unedited `setup-debug` block statement. */
|
|
70
|
+
readonly bootstrapStarts: ReadonlySet<number>;
|
|
69
71
|
/** Members the script reaches by bare name, which the companion must expose. */
|
|
70
72
|
readonly internals: readonly string[];
|
|
71
73
|
/** Import bindings the companion needs its own `require` of. */
|
|
@@ -86,6 +88,7 @@ function planFor(sourceFile: ts.SourceFile, checker: ts.TypeChecker): CompanionP
|
|
|
86
88
|
}
|
|
87
89
|
return {
|
|
88
90
|
closureStarts: new Set(closure.statements.map((statement) => statement.pos)),
|
|
91
|
+
bootstrapStarts: new Set(closure.bootstrap.map((statement) => statement.pos)),
|
|
89
92
|
internals: closure.internals,
|
|
90
93
|
importBindings: closure.importBindings,
|
|
91
94
|
requirePath: requirePathForRel(sourceFile.fileName),
|
|
@@ -466,6 +469,10 @@ interface SplitHalves {
|
|
|
466
469
|
* first. The run stops at the first statement that reads the companion, and at
|
|
467
470
|
* the first non-declaration — a lifecycle hook closes over the internals it
|
|
468
471
|
* reads, so its declaration must precede the hook's.
|
|
472
|
+
*
|
|
473
|
+
* The `setup-debug` block's gate is taken out of `script` before this runs and
|
|
474
|
+
* placed immediately ahead of the `require`, so the debugger starts before any
|
|
475
|
+
* companion code does while `lldebugger` itself is still required above it.
|
|
469
476
|
*/
|
|
470
477
|
function requireInsertionPoint(script: readonly Statement[], reads: ReadonlySet<string>): number {
|
|
471
478
|
let index = 0;
|
|
@@ -524,6 +531,7 @@ export interface CompanionEmitter {
|
|
|
524
531
|
export function createCompanionEmitPlugin(): CompanionEmitter {
|
|
525
532
|
const plans = new Map<string, CompanionPlan | undefined>();
|
|
526
533
|
const owned = new Map<string, Set<Statement>>();
|
|
534
|
+
const bootstraps = new Map<string, Set<Statement>>();
|
|
527
535
|
const companions = new Map<string, string>();
|
|
528
536
|
|
|
529
537
|
function planOf(sourceFile: ts.SourceFile, checker: ts.TypeChecker): CompanionPlan | undefined {
|
|
@@ -552,6 +560,24 @@ export function createCompanionEmitPlugin(): CompanionEmitter {
|
|
|
552
560
|
return statements;
|
|
553
561
|
}
|
|
554
562
|
|
|
563
|
+
function captureBootstrap(node: ts.IfStatement, context: TransformationContext): Statement[] {
|
|
564
|
+
const statements = context.superTransformStatements(node);
|
|
565
|
+
const sourceFile = context.sourceFile;
|
|
566
|
+
const plan = planOf(sourceFile, context.checker);
|
|
567
|
+
if (plan === undefined || !plan.bootstrapStarts.has(node.pos)) {
|
|
568
|
+
return statements;
|
|
569
|
+
}
|
|
570
|
+
let bucket = bootstraps.get(sourceFile.fileName);
|
|
571
|
+
if (bucket === undefined) {
|
|
572
|
+
bucket = new Set();
|
|
573
|
+
bootstraps.set(sourceFile.fileName, bucket);
|
|
574
|
+
}
|
|
575
|
+
for (const statement of statements) {
|
|
576
|
+
bucket.add(statement);
|
|
577
|
+
}
|
|
578
|
+
return statements;
|
|
579
|
+
}
|
|
580
|
+
|
|
555
581
|
const visitors: Plugin["visitors"] = {
|
|
556
582
|
[ts.SyntaxKind.VariableStatement]: capture,
|
|
557
583
|
[ts.SyntaxKind.FunctionDeclaration]: capture,
|
|
@@ -561,6 +587,7 @@ export function createCompanionEmitPlugin(): CompanionEmitter {
|
|
|
561
587
|
// because TSTL emits its `require` bindings through its own hoisting. The
|
|
562
588
|
// companion takes a copy of that prelude instead — see `copiedPrelude`.
|
|
563
589
|
[ts.SyntaxKind.ExportDeclaration]: capture,
|
|
590
|
+
[ts.SyntaxKind.IfStatement]: captureBootstrap,
|
|
564
591
|
};
|
|
565
592
|
|
|
566
593
|
let configuredNoResolvePaths: readonly string[] = [];
|
|
@@ -582,7 +609,8 @@ export function createCompanionEmitPlugin(): CompanionEmitter {
|
|
|
582
609
|
if (plan === undefined || bucket === undefined || bucket.size === 0) {
|
|
583
610
|
continue;
|
|
584
611
|
}
|
|
585
|
-
|
|
612
|
+
const bootstrap = bootstraps.get(sourceFile.fileName) ?? new Set();
|
|
613
|
+
emitCompanion(program, emitHost, file, sourceFile, plan, bucket, bootstrap, companions);
|
|
586
614
|
emitted.push(plan.requirePath);
|
|
587
615
|
}
|
|
588
616
|
// The companion is written by the CLI, not by TSTL's emit plan, so TSTL's
|
|
@@ -606,6 +634,7 @@ function emitCompanion(
|
|
|
606
634
|
sourceFile: ts.SourceFile,
|
|
607
635
|
plan: CompanionPlan,
|
|
608
636
|
owned: ReadonlySet<Statement>,
|
|
637
|
+
bootstrap: ReadonlySet<Statement>,
|
|
609
638
|
companions: Map<string, string>,
|
|
610
639
|
): void {
|
|
611
640
|
const luaAst = file.luaAst;
|
|
@@ -644,9 +673,11 @@ function emitCompanion(
|
|
|
644
673
|
];
|
|
645
674
|
|
|
646
675
|
const reads = new Set([EXPORTS_NAME, ...plan.internals]);
|
|
647
|
-
const
|
|
676
|
+
const rest = halves.script.filter((statement) => !bootstrap.has(statement));
|
|
677
|
+
const insertAt = requireInsertionPoint(rest, reads);
|
|
648
678
|
const scriptStatements: Statement[] = [
|
|
649
|
-
...
|
|
679
|
+
...rest.slice(0, insertAt),
|
|
680
|
+
...halves.script.filter((statement) => bootstrap.has(statement)),
|
|
650
681
|
createVariableDeclarationStatement(
|
|
651
682
|
exportsIdentifier(),
|
|
652
683
|
createCallExpression(createIdentifier("require"), [createStringLiteral(plan.requirePath)]),
|
|
@@ -654,7 +685,7 @@ function emitCompanion(
|
|
|
654
685
|
...plan.internals.map((name) =>
|
|
655
686
|
createVariableDeclarationStatement(createIdentifier(name), internalsField(name)),
|
|
656
687
|
),
|
|
657
|
-
...
|
|
688
|
+
...rest.slice(insertAt),
|
|
658
689
|
createReturnStatement([exportsIdentifier()]),
|
|
659
690
|
];
|
|
660
691
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
|
|
3
|
+
// Managed-block sentinels. A re-run locates the single BEGIN…END pair and
|
|
4
|
+
// refreshes the enclosed text if it drifted, so wording changes self-heal.
|
|
5
|
+
// They are `//` line comments, stripped by TSTL, so they never reach the
|
|
6
|
+
// emitted `.ts.script`.
|
|
7
|
+
export const DEBUG_BLOCK_BEGIN =
|
|
8
|
+
"// defold-typescript:setup-debug BEGIN — managed block, do not edit";
|
|
9
|
+
export const DEBUG_BLOCK_END = "// defold-typescript:setup-debug END";
|
|
10
|
+
|
|
11
|
+
// The canonical managed block: import + gated start, marker-free, no
|
|
12
|
+
// `declare module` (that lives in the ambient `.d.ts`). Mirrors the snippet in
|
|
13
|
+
// `packages/docs/guide/debugging.md` exactly; a guide-comparison test locks the two.
|
|
14
|
+
// The blank line before END keeps Biome's organizeImports assist from gluing
|
|
15
|
+
// the END comment onto a following import as its leading comment (which would
|
|
16
|
+
// then demand a blank line before the whole group). With blank lines on both
|
|
17
|
+
// sides of END, an import injected above the user's own imports stays clean.
|
|
18
|
+
export const DEBUG_MANAGED_BLOCK = `${DEBUG_BLOCK_BEGIN}
|
|
19
|
+
import * as lldebugger from "lldebugger.debug";
|
|
20
|
+
|
|
21
|
+
if (sys.get_engine_info().is_debug) {
|
|
22
|
+
lldebugger.start();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
${DEBUG_BLOCK_END}`;
|
|
26
|
+
|
|
27
|
+
export interface ManagedBootstrap {
|
|
28
|
+
/** The top-level statements wholly between the sentinels, in source order. */
|
|
29
|
+
readonly statements: readonly ts.Statement[];
|
|
30
|
+
/**
|
|
31
|
+
* Whether the text between the sentinels is the canonical body token for
|
|
32
|
+
* token, so line endings, quote style, comments and optional semicolons never
|
|
33
|
+
* decide it.
|
|
34
|
+
*/
|
|
35
|
+
readonly canonical: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function occurrences(haystack: string, needle: string): number {
|
|
39
|
+
return haystack.split(needle).length - 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function bodyTokens(text: string): string[] {
|
|
43
|
+
const scanner = ts.createScanner(ts.ScriptTarget.Latest, true, ts.LanguageVariant.Standard, text);
|
|
44
|
+
const tokens: string[] = [];
|
|
45
|
+
for (let kind = scanner.scan(); kind !== ts.SyntaxKind.EndOfFileToken; kind = scanner.scan()) {
|
|
46
|
+
if (kind === ts.SyntaxKind.SemicolonToken) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const spelling =
|
|
50
|
+
kind === ts.SyntaxKind.StringLiteral ? scanner.getTokenValue() : scanner.getTokenText();
|
|
51
|
+
tokens.push(`${kind}:${spelling}`);
|
|
52
|
+
}
|
|
53
|
+
return tokens;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const CANONICAL_BODY_TOKENS = bodyTokens(
|
|
57
|
+
DEBUG_MANAGED_BLOCK.slice(
|
|
58
|
+
DEBUG_BLOCK_BEGIN.length,
|
|
59
|
+
DEBUG_MANAGED_BLOCK.length - DEBUG_BLOCK_END.length,
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The `setup-debug` managed block in a source, located the way
|
|
65
|
+
* `upsertManagedBlock` locates it: exactly one BEGIN and one END, BEGIN first.
|
|
66
|
+
* Any other sentinel shape is one `setup-debug` itself refuses to edit, so it
|
|
67
|
+
* is not a managed block at all.
|
|
68
|
+
*/
|
|
69
|
+
export function findManagedBootstrap(sourceFile: ts.SourceFile): ManagedBootstrap | undefined {
|
|
70
|
+
const text = sourceFile.text;
|
|
71
|
+
if (occurrences(text, DEBUG_BLOCK_BEGIN) !== 1 || occurrences(text, DEBUG_BLOCK_END) !== 1) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const beginAt = text.indexOf(DEBUG_BLOCK_BEGIN);
|
|
75
|
+
const endAt = text.indexOf(DEBUG_BLOCK_END);
|
|
76
|
+
if (endAt < beginAt) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const bodyStart = beginAt + DEBUG_BLOCK_BEGIN.length;
|
|
80
|
+
const statements = sourceFile.statements.filter(
|
|
81
|
+
(statement) => statement.getStart(sourceFile) >= bodyStart && statement.end <= endAt,
|
|
82
|
+
);
|
|
83
|
+
const tokens = bodyTokens(text.slice(bodyStart, endAt));
|
|
84
|
+
const canonical =
|
|
85
|
+
tokens.length === CANONICAL_BODY_TOKENS.length &&
|
|
86
|
+
tokens.every((token, index) => token === CANONICAL_BODY_TOKENS[index]);
|
|
87
|
+
return { statements, canonical };
|
|
88
|
+
}
|
package/src/hsm-builtin.ts
CHANGED
|
@@ -7,8 +7,8 @@ import { scanEmittedRequires } from "./lua-require-scan";
|
|
|
7
7
|
|
|
8
8
|
// `@defold-typescript/types/hsm` is the `index` module; every other module is a
|
|
9
9
|
// subpath of it. Each lowers to a require of `defold_typescript_hsm.<module>`,
|
|
10
|
-
// which the CLI
|
|
11
|
-
// runtime.
|
|
10
|
+
// which the CLI respells to the module's place in its runtime folder, the way it
|
|
11
|
+
// handles the timers runtime.
|
|
12
12
|
export const HSM_MODULE_SPECIFIER = "@defold-typescript/types/hsm";
|
|
13
13
|
export const HSM_REQUIRE_ROOT = "defold_typescript_hsm";
|
|
14
14
|
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,13 @@ export {
|
|
|
11
11
|
componentIdOfSameObjectAddress,
|
|
12
12
|
} from "./component-animation-index";
|
|
13
13
|
export { buildConfigKeyIndex, readGameProjectSetting } from "./config-key-index";
|
|
14
|
+
export type { ManagedBootstrap } from "./debug-bootstrap";
|
|
15
|
+
export {
|
|
16
|
+
DEBUG_BLOCK_BEGIN,
|
|
17
|
+
DEBUG_BLOCK_END,
|
|
18
|
+
DEBUG_MANAGED_BLOCK,
|
|
19
|
+
findManagedBootstrap,
|
|
20
|
+
} from "./debug-bootstrap";
|
|
14
21
|
export { findEmittedRequires, LUALIB_REQUIRE_NAME } from "./emitted-requires";
|
|
15
22
|
export type { GuiFlipbookIndex } from "./gui-flipbook-index";
|
|
16
23
|
export { buildGuiFlipbookIndex } from "./gui-flipbook-index";
|
|
@@ -46,6 +53,7 @@ export {
|
|
|
46
53
|
readGameProjectDependencies,
|
|
47
54
|
readLibrarySceneDocuments,
|
|
48
55
|
} from "./library-dependencies";
|
|
56
|
+
export { scanEmittedRequires } from "./lua-require-scan";
|
|
49
57
|
export { findMainEntryFactoryImports } from "./main-entry-factory-imports";
|
|
50
58
|
export type { BuildConfig, ScriptKind, SourceOutputKind } from "./output-paths";
|
|
51
59
|
export {
|
package/src/lifecycle-erasure.ts
CHANGED
|
@@ -35,22 +35,29 @@ const FACTORY_SPECIFIERS = new Set([
|
|
|
35
35
|
`${FACTORY_MODULE}/render-script`,
|
|
36
36
|
]);
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// The lifecycle factory a callee resolves to, aliases followed, or `undefined`
|
|
39
|
+
// when it is not one of this package's factory exports.
|
|
40
|
+
function factoryExportName(callee: ts.Expression, checker: ts.TypeChecker): string | undefined {
|
|
39
41
|
let symbol = checker.getSymbolAtLocation(callee);
|
|
40
42
|
if (symbol === undefined) {
|
|
41
|
-
return
|
|
43
|
+
return undefined;
|
|
42
44
|
}
|
|
43
45
|
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
44
46
|
symbol = checker.getAliasedSymbol(symbol);
|
|
45
47
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
const name = symbol.getName();
|
|
49
|
+
if (!FACTORY_NAMES.has(name)) {
|
|
50
|
+
return undefined;
|
|
48
51
|
}
|
|
49
52
|
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
50
53
|
if (declaration === undefined) {
|
|
51
|
-
return
|
|
54
|
+
return undefined;
|
|
52
55
|
}
|
|
53
|
-
return declaration.getSourceFile().fileName.includes(FACTORY_MODULE);
|
|
56
|
+
return declaration.getSourceFile().fileName.includes(FACTORY_MODULE) ? name : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolvesToFactoryExport(callee: ts.Expression, checker: ts.TypeChecker): boolean {
|
|
60
|
+
return factoryExportName(callee, checker) !== undefined;
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
function isThisParameter(param: ts.ParameterDeclaration): boolean {
|
|
@@ -208,6 +215,20 @@ function emitInitMerge(
|
|
|
208
215
|
return [builderDecl, createAssignmentStatement(createIdentifier("init"), initFn, property)];
|
|
209
216
|
}
|
|
210
217
|
|
|
218
|
+
// Build-time markers, never engine functions: bob reads `resource.<kind>("path")`
|
|
219
|
+
// only as a `go.property` default (`LuaScanner.java:923-933` at defold
|
|
220
|
+
// `a81dbb04`), and the engine's `resource` table registers none of them
|
|
221
|
+
// (`script_resource.cpp:3447-3463` in 1.14.0). Called anywhere else, each is nil.
|
|
222
|
+
export const RESOURCE_PROPERTY_MARKERS: ReadonlySet<string> = new Set([
|
|
223
|
+
"resource.atlas",
|
|
224
|
+
"resource.buffer",
|
|
225
|
+
"resource.font",
|
|
226
|
+
"resource.material",
|
|
227
|
+
"resource.render_target",
|
|
228
|
+
"resource.texture",
|
|
229
|
+
"resource.tile_source",
|
|
230
|
+
]);
|
|
231
|
+
|
|
211
232
|
export interface PropertyMember {
|
|
212
233
|
readonly name: string;
|
|
213
234
|
readonly initializer: ts.Expression;
|
|
@@ -275,14 +296,171 @@ function emitPropertyRegistrations(
|
|
|
275
296
|
);
|
|
276
297
|
}
|
|
277
298
|
|
|
299
|
+
// Defold builds `go.property` only in `.script` files, so a GUI or render
|
|
300
|
+
// script's `properties` block has nothing it could lower to.
|
|
301
|
+
function scriptOnlyProperties(
|
|
302
|
+
property: ts.ObjectLiteralElementLike,
|
|
303
|
+
factory: string,
|
|
304
|
+
): ts.Diagnostic {
|
|
305
|
+
const file = property.getSourceFile();
|
|
306
|
+
const start = property.getStart(file);
|
|
307
|
+
return {
|
|
308
|
+
file,
|
|
309
|
+
start,
|
|
310
|
+
length: property.getEnd() - start,
|
|
311
|
+
category: ts.DiagnosticCategory.Error,
|
|
312
|
+
code: 0,
|
|
313
|
+
messageText: `\`${factory}\` cannot declare \`properties\`: Defold builds \`go.property\` only in \`.script\` files. Declare the property in a \`defineScript\` and send its value with \`msg.post\`.`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// The outermost expression that only wraps `node` in parentheses or a type-only
|
|
318
|
+
// form, so `(resource.atlas("/a.atlas") as Hash)` sits where the bare call would.
|
|
319
|
+
function outermostWrapper(node: ts.Expression): ts.Expression {
|
|
320
|
+
let current = node;
|
|
321
|
+
while (
|
|
322
|
+
ts.isParenthesizedExpression(current.parent) ||
|
|
323
|
+
ts.isAsExpression(current.parent) ||
|
|
324
|
+
ts.isSatisfiesExpression(current.parent) ||
|
|
325
|
+
ts.isNonNullExpression(current.parent) ||
|
|
326
|
+
ts.isTypeAssertionExpression(current.parent)
|
|
327
|
+
) {
|
|
328
|
+
current = current.parent;
|
|
329
|
+
}
|
|
330
|
+
return current;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// The inverse of `outermostWrapper`: the expression a run of parentheses and
|
|
334
|
+
// type-only forms wraps, which TSTL emits in place of the whole run.
|
|
335
|
+
function innermostExpression(node: ts.Expression): ts.Expression {
|
|
336
|
+
let current = node;
|
|
337
|
+
while (
|
|
338
|
+
ts.isParenthesizedExpression(current) ||
|
|
339
|
+
ts.isAsExpression(current) ||
|
|
340
|
+
ts.isSatisfiesExpression(current) ||
|
|
341
|
+
ts.isNonNullExpression(current) ||
|
|
342
|
+
ts.isTypeAssertionExpression(current)
|
|
343
|
+
) {
|
|
344
|
+
current = current.expression;
|
|
345
|
+
}
|
|
346
|
+
return current;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Keyed by the declaration's fully qualified name: only a symbol declared in an
|
|
350
|
+
// ambient `declare global` block carries the `global.` prefix, so a project's
|
|
351
|
+
// own `resource` never matches.
|
|
352
|
+
function globalName(symbol: ts.Symbol | undefined, checker: ts.TypeChecker): string | undefined {
|
|
353
|
+
if (symbol === undefined) {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
const fqn = checker.getFullyQualifiedName(symbol);
|
|
357
|
+
return fqn.startsWith("global.") ? fqn.slice("global.".length) : undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function calledGlobal(call: ts.CallExpression, checker: ts.TypeChecker): string | undefined {
|
|
361
|
+
const declaration = checker.getResolvedSignature(call)?.declaration;
|
|
362
|
+
if (
|
|
363
|
+
declaration === undefined ||
|
|
364
|
+
!ts.isFunctionDeclaration(declaration) ||
|
|
365
|
+
declaration.name === undefined
|
|
366
|
+
) {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
return globalName(checker.getSymbolAtLocation(declaration.name), checker);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// A `properties` default of a factory call, or the default argument of a
|
|
373
|
+
// direct `go.property` call: the two places bob reads a marker.
|
|
374
|
+
function isPropertyDefault(call: ts.CallExpression, checker: ts.TypeChecker): boolean {
|
|
375
|
+
const value = outermostWrapper(call);
|
|
376
|
+
const parent = value.parent;
|
|
377
|
+
if (ts.isCallExpression(parent)) {
|
|
378
|
+
return parent.arguments[1] === value && calledGlobal(parent, checker) === "go.property";
|
|
379
|
+
}
|
|
380
|
+
const properties = parent?.parent?.parent;
|
|
381
|
+
if (
|
|
382
|
+
properties === undefined ||
|
|
383
|
+
!ts.isPropertyAssignment(properties) ||
|
|
384
|
+
!isPropertiesHook(properties)
|
|
385
|
+
) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
const hooks = properties.parent;
|
|
389
|
+
return (
|
|
390
|
+
ts.isObjectLiteralExpression(hooks) &&
|
|
391
|
+
factoryHooks(hooks.parent as ts.Expression, checker) === hooks &&
|
|
392
|
+
propertyMembers(properties).some((member) => member.initializer === value)
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function misplacedMarker(node: ts.Expression, marker: string): ts.Diagnostic {
|
|
397
|
+
const file = node.getSourceFile();
|
|
398
|
+
const start = node.getStart(file);
|
|
399
|
+
return {
|
|
400
|
+
file,
|
|
401
|
+
start,
|
|
402
|
+
length: node.getEnd() - start,
|
|
403
|
+
category: ts.DiagnosticCategory.Error,
|
|
404
|
+
code: 0,
|
|
405
|
+
messageText: `\`${marker}\` is a build-time marker: Defold's build reads it only in a \`go.property\` default, and the engine binds no such function. Use it as a \`defineScript\` \`properties\` default.`,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Bob reads a default as a marker only when the emitted text is
|
|
410
|
+
// `resource.<kind>("<path>")` or `resource.<kind>()`: the object token must be
|
|
411
|
+
// `resource` with a dot `NAME` key, and the first argument a string literal or
|
|
412
|
+
// absent (`LuaScanner.java:801-822`, `:923-933` and `:493-510` at defold
|
|
413
|
+
// `a81dbb04`). The receiver test is textual like bob's; the marker resolution
|
|
414
|
+
// has already excluded a project's own `resource`.
|
|
415
|
+
function readsAsMarker(call: ts.CallExpression): boolean {
|
|
416
|
+
const callee = innermostExpression(call.expression);
|
|
417
|
+
if (ts.isOptionalChain(call) || ts.isOptionalChain(callee)) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
if (
|
|
421
|
+
!ts.isPropertyAccessExpression(callee) &&
|
|
422
|
+
!(
|
|
423
|
+
ts.isElementAccessExpression(callee) &&
|
|
424
|
+
ts.isStringLiteralLike(innermostExpression(callee.argumentExpression))
|
|
425
|
+
)
|
|
426
|
+
) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
const receiver = innermostExpression(callee.expression);
|
|
430
|
+
if (!ts.isIdentifier(receiver) || receiver.text !== "resource") {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
const [path] = call.arguments;
|
|
434
|
+
return path === undefined || ts.isStringLiteralLike(innermostExpression(path));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function unreadableMarker(node: ts.CallExpression, marker: string): ts.Diagnostic {
|
|
438
|
+
const file = node.getSourceFile();
|
|
439
|
+
const start = node.getStart(file);
|
|
440
|
+
return {
|
|
441
|
+
file,
|
|
442
|
+
start,
|
|
443
|
+
length: node.getEnd() - start,
|
|
444
|
+
category: ts.DiagnosticCategory.Error,
|
|
445
|
+
code: 0,
|
|
446
|
+
messageText: `\`${marker}\` in a property default must be written \`${marker}("<path>")\`, with a string literal path: Defold's build reads no other form.`,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isCallee(node: ts.Expression): boolean {
|
|
451
|
+
const callee = outermostWrapper(node);
|
|
452
|
+
return ts.isCallExpression(callee.parent) && callee.parent.expression === callee;
|
|
453
|
+
}
|
|
454
|
+
|
|
278
455
|
function eraseFactoryCall(
|
|
279
456
|
expression: ts.Expression,
|
|
280
457
|
context: TransformationContext,
|
|
281
458
|
): Statement[] | undefined {
|
|
282
459
|
const hooks = factoryHooks(expression, context.checker);
|
|
283
|
-
if (hooks === undefined) {
|
|
460
|
+
if (hooks === undefined || !ts.isCallExpression(expression)) {
|
|
284
461
|
return undefined;
|
|
285
462
|
}
|
|
463
|
+
const factory = factoryExportName(expression.expression, context.checker);
|
|
286
464
|
const registrations: Statement[] = [];
|
|
287
465
|
const statements: Statement[] = [];
|
|
288
466
|
for (const property of hooks.properties) {
|
|
@@ -291,7 +469,11 @@ function eraseFactoryCall(
|
|
|
291
469
|
continue;
|
|
292
470
|
}
|
|
293
471
|
if (name === "properties") {
|
|
294
|
-
|
|
472
|
+
if (factory === "defineScript") {
|
|
473
|
+
registrations.push(...emitPropertyRegistrations(property, context));
|
|
474
|
+
} else if (factory !== undefined && propertyMembers(property).length > 0) {
|
|
475
|
+
context.diagnostics.push(scriptOnlyProperties(property, factory));
|
|
476
|
+
}
|
|
295
477
|
continue;
|
|
296
478
|
}
|
|
297
479
|
const fn = hookFunction(property);
|
|
@@ -374,5 +556,48 @@ export const lifecycleErasurePlugin: Plugin = {
|
|
|
374
556
|
},
|
|
375
557
|
[ts.SyntaxKind.ImportDeclaration]: (node, context) =>
|
|
376
558
|
isFactoryOnlyImport(node) ? [] : context.superTransformStatements(node),
|
|
559
|
+
// TSTL's optional-chain lowering also visits nodes it synthesizes, which have
|
|
560
|
+
// no parent and no source span; only parsed nodes are checked.
|
|
561
|
+
[ts.SyntaxKind.CallExpression]: (node, context) => {
|
|
562
|
+
const marker = node.parent === undefined ? undefined : calledGlobal(node, context.checker);
|
|
563
|
+
if (marker !== undefined && RESOURCE_PROPERTY_MARKERS.has(marker)) {
|
|
564
|
+
if (!isPropertyDefault(node, context.checker)) {
|
|
565
|
+
context.diagnostics.push(misplacedMarker(node, marker));
|
|
566
|
+
} else if (!readsAsMarker(node)) {
|
|
567
|
+
context.diagnostics.push(unreadableMarker(node, marker));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return context.superTransformExpression(node);
|
|
571
|
+
},
|
|
572
|
+
[ts.SyntaxKind.PropertyAccessExpression]: (node, context) => {
|
|
573
|
+
if (node.parent !== undefined && !isCallee(node)) {
|
|
574
|
+
const marker = globalName(context.checker.getSymbolAtLocation(node.name), context.checker);
|
|
575
|
+
if (marker !== undefined && RESOURCE_PROPERTY_MARKERS.has(marker)) {
|
|
576
|
+
context.diagnostics.push(misplacedMarker(node, marker));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return context.superTransformExpression(node);
|
|
580
|
+
},
|
|
581
|
+
[ts.SyntaxKind.ElementAccessExpression]: (node, context) => {
|
|
582
|
+
if (node.parent !== undefined && !isCallee(node)) {
|
|
583
|
+
// The checker resolves a string key only when it is the direct argument,
|
|
584
|
+
// so a wrapped key is looked up by name on the receiver's type, as the
|
|
585
|
+
// checker does for a direct one.
|
|
586
|
+
const key = innermostExpression(node.argumentExpression);
|
|
587
|
+
const marker = ts.isStringLiteralLike(key)
|
|
588
|
+
? globalName(
|
|
589
|
+
context.checker.getPropertyOfType(
|
|
590
|
+
context.checker.getTypeAtLocation(node.expression),
|
|
591
|
+
key.text,
|
|
592
|
+
),
|
|
593
|
+
context.checker,
|
|
594
|
+
)
|
|
595
|
+
: undefined;
|
|
596
|
+
if (marker !== undefined && RESOURCE_PROPERTY_MARKERS.has(marker)) {
|
|
597
|
+
context.diagnostics.push(misplacedMarker(node, marker));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return context.superTransformExpression(node);
|
|
601
|
+
},
|
|
377
602
|
},
|
|
378
603
|
};
|
package/src/timers-runtime.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The importable polyfill specifier (matched by the type checker and the
|
|
2
2
|
// lowering pass) and the flat Lua require name the import is rewritten to. The
|
|
3
|
-
// CLI writes the runtime
|
|
4
|
-
//
|
|
3
|
+
// CLI writes the runtime into its runtime folder beside `lualib_bundle` and
|
|
4
|
+
// respells every `require("defold_typescript_timers")` to that path.
|
|
5
5
|
export const TIMERS_MODULE_SPECIFIER = "@defold-typescript/types/timers";
|
|
6
6
|
export const TIMERS_REQUIRE_NAME = "defold_typescript_timers";
|
|
7
7
|
|