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