@defold-typescript/transpiler 0.42.0 → 0.44.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/emitted-requires.d.ts +1 -1
- package/dist/hsm/async.ts +73 -0
- package/dist/hsm/debug.ts +119 -0
- package/dist/hsm/defold.ts +39 -0
- package/dist/hsm/index.ts +1418 -0
- package/dist/hsm-builtin.d.ts +26 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +340 -130
- package/dist/transpile.d.ts +11 -0
- package/package.json +4 -3
- package/src/emitted-requires.ts +6 -4
- package/src/hsm-builtin.ts +246 -0
- package/src/index.ts +11 -1
- package/src/session.ts +2 -0
- package/src/transpile.ts +118 -26
package/dist/transpile.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface TranspileProjectResult {
|
|
|
21
21
|
readonly companions?: Readonly<Record<string, string>>;
|
|
22
22
|
readonly lualib?: string;
|
|
23
23
|
readonly timersRuntime?: string;
|
|
24
|
+
readonly hsmModules?: Readonly<Record<string, string>>;
|
|
24
25
|
}
|
|
25
26
|
export declare const AMBIENT_FILES: Readonly<Record<string, string>>;
|
|
26
27
|
interface CollectableFile {
|
|
@@ -31,5 +32,15 @@ interface CollectableFile {
|
|
|
31
32
|
}
|
|
32
33
|
export declare function collectOutputs(transpiledFiles: readonly CollectableFile[], diagnostics: readonly ts.Diagnostic[], userKeys: ReadonlySet<string>, emittedCompanions?: ReadonlyMap<string, string>): TranspileProjectResult;
|
|
33
34
|
export declare function transpileProject(input: TranspileProjectInput): TranspileProjectResult;
|
|
35
|
+
/**
|
|
36
|
+
* Every `hsm` module compiled to Lua, keyed by module name, once per process.
|
|
37
|
+
* The source is the shipped TypeScript, never a prebuilt Lua copy, so the Lua a
|
|
38
|
+
* project gets always comes from the compiler that builds it. Each file sits at
|
|
39
|
+
* `defold_typescript_hsm/<module>.ts`, so a require between modules is already
|
|
40
|
+
* spelled the way the user's lowered imports are. Throws when the compile
|
|
41
|
+
* reports anything, or when a module outside `HSM_LUALIB_MODULES` needs the
|
|
42
|
+
* lualib bundle.
|
|
43
|
+
*/
|
|
44
|
+
export declare function compileHsmModules(): Readonly<Record<string, string>>;
|
|
34
45
|
export declare function transpile(source: string): TranspileResult;
|
|
35
46
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defold-typescript/transpiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "TypeScript-to-Lua build pipeline tuned for Defold's runtime.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,13 +30,14 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"build": "rm -rf dist && bun build src/index.ts --target=node --format=esm --packages=external --outdir=dist && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
33
|
+
"build": "rm -rf dist && bun scripts/copy-hsm.ts && bun build src/index.ts --target=node --format=esm --packages=external --outdir=dist && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
34
34
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
35
35
|
"test": "bun test"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@defold-typescript/types": "0.
|
|
38
|
+
"@defold-typescript/types": "0.44.0",
|
|
39
39
|
"@typescript-to-lua/language-extensions": "1.19.0",
|
|
40
|
+
"typescript": "6.0.2",
|
|
40
41
|
"typescript-to-lua": "^1.36.0"
|
|
41
42
|
}
|
|
42
43
|
}
|
package/src/emitted-requires.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { isHsmRequireName } from "./hsm-builtin";
|
|
1
2
|
import { scanEmittedRequires } from "./lua-require-scan";
|
|
2
3
|
import { TIMERS_REQUIRE_NAME } from "./timers-runtime";
|
|
3
4
|
|
|
4
5
|
export const LUALIB_REQUIRE_NAME = "lualib_bundle";
|
|
5
6
|
|
|
6
|
-
// Written
|
|
7
|
-
// source, so a require of
|
|
7
|
+
// Written by both build paths whenever a chunk requires them and backed by no
|
|
8
|
+
// TypeScript source, so a require of one can never be unresolvable. The hsm
|
|
9
|
+
// modules join them through `isHsmRequireName`.
|
|
8
10
|
const GENERATED_RUNTIME_REQUIRES: ReadonlySet<string> = new Set([
|
|
9
11
|
LUALIB_REQUIRE_NAME,
|
|
10
12
|
TIMERS_REQUIRE_NAME,
|
|
@@ -12,7 +14,7 @@ const GENERATED_RUNTIME_REQUIRES: ReadonlySet<string> = new Set([
|
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* The distinct string-literal module paths an emitted Lua chunk requires, in
|
|
15
|
-
* source order. The
|
|
17
|
+
* source order. The generated runtimes and hsm modules are skipped, as is any require whose
|
|
16
18
|
* argument is not a string literal — neither can name a source the build owns.
|
|
17
19
|
*/
|
|
18
20
|
export function findEmittedRequires(lua: string): string[] {
|
|
@@ -20,7 +22,7 @@ export function findEmittedRequires(lua: string): string[] {
|
|
|
20
22
|
const seen = new Set<string>();
|
|
21
23
|
|
|
22
24
|
for (const { path } of scanEmittedRequires(lua)) {
|
|
23
|
-
if (GENERATED_RUNTIME_REQUIRES.has(path) || seen.has(path)) {
|
|
25
|
+
if (GENERATED_RUNTIME_REQUIRES.has(path) || isHsmRequireName(path) || seen.has(path)) {
|
|
24
26
|
continue;
|
|
25
27
|
}
|
|
26
28
|
seen.add(path);
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import * as ts from "typescript";
|
|
5
|
+
import { LuaLibFeature, type Plugin, type TransformationContext } from "typescript-to-lua";
|
|
6
|
+
import { scanEmittedRequires } from "./lua-require-scan";
|
|
7
|
+
|
|
8
|
+
// `@defold-typescript/types/hsm` is the `index` module; every other module is a
|
|
9
|
+
// subpath of it. Each lowers to a require of `defold_typescript_hsm.<module>`,
|
|
10
|
+
// which the CLI writes under the output root the way it writes the timers
|
|
11
|
+
// runtime.
|
|
12
|
+
export const HSM_MODULE_SPECIFIER = "@defold-typescript/types/hsm";
|
|
13
|
+
export const HSM_REQUIRE_ROOT = "defold_typescript_hsm";
|
|
14
|
+
|
|
15
|
+
// The hsm modules whose Lua requires the lualib bundle. Every other module stays
|
|
16
|
+
// plain Lua, so a program that imports none of these ships no bundle.
|
|
17
|
+
export const HSM_LUALIB_MODULES: ReadonlySet<string> = new Set(["async"]);
|
|
18
|
+
|
|
19
|
+
export interface HsmModule {
|
|
20
|
+
/** The source file's basename without `.ts`. */
|
|
21
|
+
readonly name: string;
|
|
22
|
+
/** What a user imports. */
|
|
23
|
+
readonly specifier: string;
|
|
24
|
+
/** What the import lowers to, and the module path the CLI writes it under. */
|
|
25
|
+
readonly requireName: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function hsmRequireName(name: string): string {
|
|
29
|
+
return `${HSM_REQUIRE_ROOT}.${name}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hsmSpecifier(name: string): string {
|
|
33
|
+
return name === "index" ? HSM_MODULE_SPECIFIER : `${HSM_MODULE_SPECIFIER}/${name}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Same anchoring as the CLI's version lookup: the module URL, never
|
|
37
|
+
// `import.meta.dir`, so the bundled `dist/index.js` under node still lands on
|
|
38
|
+
// the package root.
|
|
39
|
+
function defaultPackageRoot(): string {
|
|
40
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The one filter the transpiler build (what ships in `dist/hsm/`) and the
|
|
44
|
+
// compile (what reaches a project) both read, so a new module ships without
|
|
45
|
+
// editing a list.
|
|
46
|
+
export function hsmSourceFiles(dir: string): string[] {
|
|
47
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
48
|
+
.filter(
|
|
49
|
+
(entry) =>
|
|
50
|
+
entry.isFile() &&
|
|
51
|
+
entry.name.endsWith(".ts") &&
|
|
52
|
+
!entry.name.endsWith(".test.ts") &&
|
|
53
|
+
!entry.name.endsWith(".d.ts"),
|
|
54
|
+
)
|
|
55
|
+
.map((entry) => entry.name)
|
|
56
|
+
.sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The workspace sibling wins so a stale `dist/` never shadows the live source
|
|
60
|
+
// inside this repo; a published install has only the `dist/hsm/` copy.
|
|
61
|
+
export function resolveHsmSourceDir(packageRoot: string = defaultPackageRoot()): string | null {
|
|
62
|
+
const workspace = path.join(packageRoot, "..", "hsm", "src");
|
|
63
|
+
if (existsSync(workspace)) {
|
|
64
|
+
return workspace;
|
|
65
|
+
}
|
|
66
|
+
const shipped = path.join(packageRoot, "dist", "hsm");
|
|
67
|
+
if (existsSync(shipped)) {
|
|
68
|
+
return shipped;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function requireHsmSourceDir(): string {
|
|
74
|
+
const dir = resolveHsmSourceDir();
|
|
75
|
+
if (dir === null) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"@defold-typescript/transpiler: the hsm source is missing from this install; reinstall @defold-typescript/transpiler.",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return dir;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let modules: readonly HsmModule[] | undefined;
|
|
84
|
+
|
|
85
|
+
export function hsmModules(): readonly HsmModule[] {
|
|
86
|
+
modules ??= hsmSourceFiles(requireHsmSourceDir()).map((file) => {
|
|
87
|
+
const name = file.slice(0, -".ts".length);
|
|
88
|
+
return { name, specifier: hsmSpecifier(name), requireName: hsmRequireName(name) };
|
|
89
|
+
});
|
|
90
|
+
return modules;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The hsm module a require path names, or undefined for any other require. */
|
|
94
|
+
function hsmModuleOfRequire(requirePath: string): HsmModule | undefined {
|
|
95
|
+
if (!requirePath.startsWith(`${HSM_REQUIRE_ROOT}.`)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return hsmModules().find((module) => module.requireName === requirePath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function isHsmRequireName(requirePath: string): boolean {
|
|
102
|
+
return hsmModuleOfRequire(requirePath) !== undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hsmModuleOfSpecifier(node: ts.Expression | undefined): HsmModule | undefined {
|
|
106
|
+
if (node === undefined || !ts.isStringLiteral(node)) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const { text } = node;
|
|
110
|
+
if (text !== HSM_MODULE_SPECIFIER && !text.startsWith(`${HSM_MODULE_SPECIFIER}/`)) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
return hsmModules().find((module) => module.specifier === text);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function requiredHsmNames(lua: string): string[] {
|
|
117
|
+
const names: string[] = [];
|
|
118
|
+
for (const { path: requirePath } of scanEmittedRequires(lua)) {
|
|
119
|
+
const module = hsmModuleOfRequire(requirePath);
|
|
120
|
+
if (module !== undefined) {
|
|
121
|
+
names.push(module.name);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return names;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The hsm modules a program needs: every one its chunks require, plus every one
|
|
129
|
+
* those require in turn. Read from emitted Lua rather than the import AST, so an
|
|
130
|
+
* import TypeScript elides (`import type`, an unused binding) selects nothing.
|
|
131
|
+
* `compile` runs only when some chunk requires a module.
|
|
132
|
+
*/
|
|
133
|
+
export function hsmClosure(
|
|
134
|
+
chunks: Iterable<string>,
|
|
135
|
+
compile: () => Readonly<Record<string, string>>,
|
|
136
|
+
): Record<string, string> | undefined {
|
|
137
|
+
const pending: string[] = [];
|
|
138
|
+
for (const lua of chunks) {
|
|
139
|
+
pending.push(...requiredHsmNames(lua));
|
|
140
|
+
}
|
|
141
|
+
if (pending.length === 0) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
const compiled = compile();
|
|
145
|
+
const selected: Record<string, string> = {};
|
|
146
|
+
for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
|
|
147
|
+
const lua = compiled[name];
|
|
148
|
+
if (name in selected || lua === undefined) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
selected[name] = lua;
|
|
152
|
+
pending.push(...requiredHsmNames(lua));
|
|
153
|
+
}
|
|
154
|
+
return Object.fromEntries(Object.entries(selected).sort(([a], [b]) => a.localeCompare(b)));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Rewrite an hsm specifier to its flat require, the way the timers lowering
|
|
158
|
+
// does: `@NoResolution:` keeps TSTL from looking for a Lua source behind the
|
|
159
|
+
// `.d.ts` the import type-checks against.
|
|
160
|
+
function lowerSpecifier(module: HsmModule): ts.StringLiteral {
|
|
161
|
+
return ts.factory.createStringLiteral(`@NoResolution:${module.requireName}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type Lowered = ReturnType<TransformationContext["superTransformStatements"]>;
|
|
165
|
+
|
|
166
|
+
function loweredAnything(lowered: Lowered): boolean {
|
|
167
|
+
return Array.isArray(lowered) ? lowered.length > 0 : lowered !== undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The same resolver verdict TSTL's own import transform uses to decide which
|
|
171
|
+
// bindings it requires, so the bundle and the emitted require cannot disagree.
|
|
172
|
+
// Takes the original node: the resolver has no binding for a synthesized copy.
|
|
173
|
+
function importSurvivesElision(
|
|
174
|
+
node: ts.ImportDeclaration,
|
|
175
|
+
context: TransformationContext,
|
|
176
|
+
): boolean {
|
|
177
|
+
const clause = node.importClause;
|
|
178
|
+
if (clause === undefined) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
const { resolver } = context;
|
|
182
|
+
if (clause.name !== undefined && resolver.isReferencedAliasDeclaration(clause)) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
const bindings = clause.namedBindings;
|
|
186
|
+
if (bindings === undefined) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
190
|
+
return resolver.isReferencedAliasDeclaration(bindings);
|
|
191
|
+
}
|
|
192
|
+
return bindings.elements.some((element) => resolver.isReferencedAliasDeclaration(element));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// TSTL writes `lualib_bundle.lua` only for features a user chunk registers; it
|
|
196
|
+
// never sees the hsm module's own source, which the CLI writes beside the build.
|
|
197
|
+
// An elided import (type-only, unused) registers nothing. The import visitor
|
|
198
|
+
// passes the resolver's elision verdict, because TSTL's import transform always
|
|
199
|
+
// returns nothing and queues its require on the scope instead; the export
|
|
200
|
+
// visitor passes whether its lowered statements are non-empty, because TSTL's
|
|
201
|
+
// export transform returns them.
|
|
202
|
+
function registerLualib(module: HsmModule, emitted: boolean, context: TransformationContext): void {
|
|
203
|
+
if (emitted && HSM_LUALIB_MODULES.has(module.name)) {
|
|
204
|
+
context.usedLuaLibFeatures.add(LuaLibFeature.Promise);
|
|
205
|
+
context.usedLuaLibFeatures.add(LuaLibFeature.New);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const hsmLoweringPlugin: Plugin = {
|
|
210
|
+
visitors: {
|
|
211
|
+
[ts.SyntaxKind.ImportDeclaration]: (node, context) => {
|
|
212
|
+
const module = hsmModuleOfSpecifier(node.moduleSpecifier);
|
|
213
|
+
if (module === undefined) {
|
|
214
|
+
return context.superTransformStatements(node);
|
|
215
|
+
}
|
|
216
|
+
registerLualib(module, importSurvivesElision(node, context), context);
|
|
217
|
+
return context.superTransformStatements(
|
|
218
|
+
ts.factory.updateImportDeclaration(
|
|
219
|
+
node,
|
|
220
|
+
node.modifiers,
|
|
221
|
+
node.importClause,
|
|
222
|
+
lowerSpecifier(module),
|
|
223
|
+
node.attributes,
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
},
|
|
227
|
+
[ts.SyntaxKind.ExportDeclaration]: (node, context) => {
|
|
228
|
+
const module = hsmModuleOfSpecifier(node.moduleSpecifier);
|
|
229
|
+
if (module === undefined) {
|
|
230
|
+
return context.superTransformStatements(node);
|
|
231
|
+
}
|
|
232
|
+
const lowered = context.superTransformStatements(
|
|
233
|
+
ts.factory.updateExportDeclaration(
|
|
234
|
+
node,
|
|
235
|
+
node.modifiers,
|
|
236
|
+
node.isTypeOnly,
|
|
237
|
+
node.exportClause,
|
|
238
|
+
lowerSpecifier(module),
|
|
239
|
+
node.attributes,
|
|
240
|
+
),
|
|
241
|
+
);
|
|
242
|
+
registerLualib(module, loweredAnything(lowered), context);
|
|
243
|
+
return lowered;
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,16 @@ export {
|
|
|
20
20
|
guiScriptResourceOf,
|
|
21
21
|
guiScriptResourcesOf,
|
|
22
22
|
} from "./gui-node-index";
|
|
23
|
+
export type { HsmModule } from "./hsm-builtin";
|
|
24
|
+
export {
|
|
25
|
+
HSM_MODULE_SPECIFIER,
|
|
26
|
+
HSM_REQUIRE_ROOT,
|
|
27
|
+
hsmModules,
|
|
28
|
+
hsmRequireName,
|
|
29
|
+
hsmSourceFiles,
|
|
30
|
+
requireHsmSourceDir,
|
|
31
|
+
resolveHsmSourceDir,
|
|
32
|
+
} from "./hsm-builtin";
|
|
23
33
|
export { buildInputActionIndex } from "./input-action-index";
|
|
24
34
|
export type {
|
|
25
35
|
ExtensionDependency,
|
|
@@ -99,7 +109,7 @@ export type {
|
|
|
99
109
|
TranspileProjectResult,
|
|
100
110
|
TranspileResult,
|
|
101
111
|
} from "./transpile";
|
|
102
|
-
export { transpile, transpileProject } from "./transpile";
|
|
112
|
+
export { compileHsmModules, transpile, transpileProject } from "./transpile";
|
|
103
113
|
export type { ClassifiedSlot } from "./url-address-slots";
|
|
104
114
|
export {
|
|
105
115
|
addressClassOfArgument,
|
package/src/session.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as ts from "typescript";
|
|
|
5
5
|
import * as tstl from "typescript-to-lua";
|
|
6
6
|
import { type CompanionEmitter, createCompanionEmitPlugin } from "./companion-emit";
|
|
7
7
|
import { editorScriptErasurePlugin } from "./editor-script-erasure";
|
|
8
|
+
import { hsmLoweringPlugin } from "./hsm-builtin";
|
|
8
9
|
import { lifecycleErasurePlugin } from "./lifecycle-erasure";
|
|
9
10
|
import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
|
|
10
11
|
import { messageGuardLoweringPlugin } from "./message-guard-lowering";
|
|
@@ -44,6 +45,7 @@ function compilerOptions(companionEmit: CompanionEmitter): tstl.CompilerOptions
|
|
|
44
45
|
{ plugin: windowEventGuardLoweringPlugin },
|
|
45
46
|
{ plugin: messageDispatchLoweringPlugin },
|
|
46
47
|
{ plugin: timersLoweringPlugin },
|
|
48
|
+
{ plugin: hsmLoweringPlugin },
|
|
47
49
|
{ plugin: typeApplicationErasurePlugin },
|
|
48
50
|
{ plugin: companionEmit.plugin },
|
|
49
51
|
],
|
package/src/transpile.ts
CHANGED
|
@@ -9,7 +9,16 @@ import {
|
|
|
9
9
|
findDirectGoPropertyCalls,
|
|
10
10
|
GO_PROPERTY_DIRECT_CALL_MESSAGE,
|
|
11
11
|
} from "./go-property-direct-call";
|
|
12
|
+
import {
|
|
13
|
+
HSM_LUALIB_MODULES,
|
|
14
|
+
HSM_REQUIRE_ROOT,
|
|
15
|
+
hsmClosure,
|
|
16
|
+
hsmLoweringPlugin,
|
|
17
|
+
hsmModules,
|
|
18
|
+
requireHsmSourceDir,
|
|
19
|
+
} from "./hsm-builtin";
|
|
12
20
|
import { lifecycleErasurePlugin } from "./lifecycle-erasure";
|
|
21
|
+
import { scanEmittedRequires } from "./lua-require-scan";
|
|
13
22
|
import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
|
|
14
23
|
import { messageGuardLoweringPlugin } from "./message-guard-lowering";
|
|
15
24
|
import { importsTimersModule, timersLoweringPlugin } from "./timers-lowering";
|
|
@@ -63,6 +72,13 @@ export interface TranspileProjectResult {
|
|
|
63
72
|
// must write this to the output root. Present only when a user file imports
|
|
64
73
|
// the module (pay-for-use).
|
|
65
74
|
readonly timersRuntime?: string;
|
|
75
|
+
// The compiled `hsm` modules the program requires, keyed by module name
|
|
76
|
+
// (`index`, `defold`, ...). An hsm import lowers to
|
|
77
|
+
// `require("defold_typescript_hsm.<module>")`, so the CLI must write each one
|
|
78
|
+
// under the output root. Present only when some chunk requires one
|
|
79
|
+
// (pay-for-use); never carries a source map, since the `.ts` it would point
|
|
80
|
+
// at is not in the user's project.
|
|
81
|
+
readonly hsmModules?: Readonly<Record<string, string>>;
|
|
66
82
|
}
|
|
67
83
|
|
|
68
84
|
function flattenDiagnosticMessage(
|
|
@@ -180,6 +196,14 @@ function buildAmbientFiles(): Record<string, string> {
|
|
|
180
196
|
"",
|
|
181
197
|
].join("\n"),
|
|
182
198
|
};
|
|
199
|
+
// The generated `hsm` declarations, at the paths the package `exports` map
|
|
200
|
+
// names, so the build types user code against the same `.d.ts` tsserver
|
|
201
|
+
// reads. hsm's own `.ts` compiles separately, in `compileHsmModules`.
|
|
202
|
+
for (const entry of readdirSync(path.join(TYPES_PKG_ROOT, "hsm"))) {
|
|
203
|
+
if (entry.endsWith(".d.ts")) {
|
|
204
|
+
files[`node_modules/@defold-typescript/types/hsm/${entry}`] = readAmbient(`hsm/${entry}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
183
207
|
// Seed the Lua 5.1 standard library (math/os/string/table/coroutine + base
|
|
184
208
|
// globals) so user code can call e.g. `math.randomseed(os.time())`.
|
|
185
209
|
Object.assign(files, collectLuaTypesClosure());
|
|
@@ -287,6 +311,8 @@ export function collectOutputs(
|
|
|
287
311
|
}
|
|
288
312
|
}
|
|
289
313
|
|
|
314
|
+
const hsm = hsmClosure([...Object.values(lua), ...Object.values(companions)], compileHsmModules);
|
|
315
|
+
|
|
290
316
|
return {
|
|
291
317
|
lua,
|
|
292
318
|
sourceMaps,
|
|
@@ -294,38 +320,48 @@ export function collectOutputs(
|
|
|
294
320
|
...(Object.keys(companions).length > 0 ? { companions } : {}),
|
|
295
321
|
...(lualib !== undefined ? { lualib } : {}),
|
|
296
322
|
...(timersImported ? { timersRuntime: TIMERS_RUNTIME } : {}),
|
|
323
|
+
...(hsm !== undefined ? { hsmModules: hsm } : {}),
|
|
297
324
|
};
|
|
298
325
|
}
|
|
299
326
|
|
|
327
|
+
function transpileVirtual(
|
|
328
|
+
files: Readonly<Record<string, string>>,
|
|
329
|
+
companionEmit: ReturnType<typeof createCompanionEmitPlugin>,
|
|
330
|
+
): tstl.TranspileVirtualProjectResult {
|
|
331
|
+
return tstl.transpileVirtualProject(
|
|
332
|
+
{ ...AMBIENT_FILES, ...files },
|
|
333
|
+
{
|
|
334
|
+
luaTarget: tstl.LuaTarget.Lua51,
|
|
335
|
+
sourceMap: true,
|
|
336
|
+
// Already-resolved CompilerOptions, so the value is the lib file name, not
|
|
337
|
+
// the tsconfig spelling "ES2022". Drops the default `lib.dom`, whose
|
|
338
|
+
// `declare var window` shadows Defold's `window` namespace.
|
|
339
|
+
lib: ["lib.es2022.d.ts"],
|
|
340
|
+
// Don't cross-check the seeded ambient .d.ts surface against itself; we only
|
|
341
|
+
// care about diagnostics on user files (mirrors the editor's skipLibCheck).
|
|
342
|
+
skipLibCheck: true,
|
|
343
|
+
// Defold scripts are not OO: free helper functions never receive a context,
|
|
344
|
+
// so suppress TSTL's implicit `self` parameter and the `_G` call-site filler.
|
|
345
|
+
noImplicitSelf: true,
|
|
346
|
+
luaPlugins: [
|
|
347
|
+
{ plugin: lifecycleErasurePlugin },
|
|
348
|
+
{ plugin: editorScriptErasurePlugin },
|
|
349
|
+
{ plugin: messageGuardLoweringPlugin },
|
|
350
|
+
{ plugin: windowEventGuardLoweringPlugin },
|
|
351
|
+
{ plugin: messageDispatchLoweringPlugin },
|
|
352
|
+
{ plugin: timersLoweringPlugin },
|
|
353
|
+
{ plugin: hsmLoweringPlugin },
|
|
354
|
+
{ plugin: typeApplicationErasurePlugin },
|
|
355
|
+
{ plugin: companionEmit.plugin },
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
300
361
|
export function transpileProject(input: TranspileProjectInput): TranspileProjectResult {
|
|
301
362
|
const userKeys = new Set(Object.keys(input.files));
|
|
302
|
-
const merged: Record<string, string> = { ...AMBIENT_FILES, ...input.files };
|
|
303
363
|
const companionEmit = createCompanionEmitPlugin();
|
|
304
|
-
|
|
305
|
-
const result = tstl.transpileVirtualProject(merged, {
|
|
306
|
-
luaTarget: tstl.LuaTarget.Lua51,
|
|
307
|
-
sourceMap: true,
|
|
308
|
-
// Already-resolved CompilerOptions, so the value is the lib file name, not
|
|
309
|
-
// the tsconfig spelling "ES2022". Drops the default `lib.dom`, whose
|
|
310
|
-
// `declare var window` shadows Defold's `window` namespace.
|
|
311
|
-
lib: ["lib.es2022.d.ts"],
|
|
312
|
-
// Don't cross-check the seeded ambient .d.ts surface against itself; we only
|
|
313
|
-
// care about diagnostics on user files (mirrors the editor's skipLibCheck).
|
|
314
|
-
skipLibCheck: true,
|
|
315
|
-
// Defold scripts are not OO: free helper functions never receive a context,
|
|
316
|
-
// so suppress TSTL's implicit `self` parameter and the `_G` call-site filler.
|
|
317
|
-
noImplicitSelf: true,
|
|
318
|
-
luaPlugins: [
|
|
319
|
-
{ plugin: lifecycleErasurePlugin },
|
|
320
|
-
{ plugin: editorScriptErasurePlugin },
|
|
321
|
-
{ plugin: messageGuardLoweringPlugin },
|
|
322
|
-
{ plugin: windowEventGuardLoweringPlugin },
|
|
323
|
-
{ plugin: messageDispatchLoweringPlugin },
|
|
324
|
-
{ plugin: timersLoweringPlugin },
|
|
325
|
-
{ plugin: typeApplicationErasurePlugin },
|
|
326
|
-
{ plugin: companionEmit.plugin },
|
|
327
|
-
],
|
|
328
|
-
});
|
|
364
|
+
const result = transpileVirtual(input.files, companionEmit);
|
|
329
365
|
|
|
330
366
|
return collectOutputs(
|
|
331
367
|
result.transpiledFiles,
|
|
@@ -335,6 +371,62 @@ export function transpileProject(input: TranspileProjectInput): TranspileProject
|
|
|
335
371
|
);
|
|
336
372
|
}
|
|
337
373
|
|
|
374
|
+
let compiledHsm: Readonly<Record<string, string>> | undefined;
|
|
375
|
+
|
|
376
|
+
function requiresLualib(lua: string): boolean {
|
|
377
|
+
return scanEmittedRequires(lua).some(({ path: requirePath }) => requirePath === "lualib_bundle");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Every `hsm` module compiled to Lua, keyed by module name, once per process.
|
|
382
|
+
* The source is the shipped TypeScript, never a prebuilt Lua copy, so the Lua a
|
|
383
|
+
* project gets always comes from the compiler that builds it. Each file sits at
|
|
384
|
+
* `defold_typescript_hsm/<module>.ts`, so a require between modules is already
|
|
385
|
+
* spelled the way the user's lowered imports are. Throws when the compile
|
|
386
|
+
* reports anything, or when a module outside `HSM_LUALIB_MODULES` needs the
|
|
387
|
+
* lualib bundle.
|
|
388
|
+
*/
|
|
389
|
+
export function compileHsmModules(): Readonly<Record<string, string>> {
|
|
390
|
+
if (compiledHsm !== undefined) {
|
|
391
|
+
return compiledHsm;
|
|
392
|
+
}
|
|
393
|
+
const sourceDir = requireHsmSourceDir();
|
|
394
|
+
const nameByKey = new Map<string, string>();
|
|
395
|
+
const files: Record<string, string> = {};
|
|
396
|
+
for (const { name } of hsmModules()) {
|
|
397
|
+
const key = `${HSM_REQUIRE_ROOT}/${name}.ts`;
|
|
398
|
+
nameByKey.set(key, name);
|
|
399
|
+
files[key] = readFileSync(path.join(sourceDir, `${name}.ts`), "utf8");
|
|
400
|
+
}
|
|
401
|
+
const result = transpileVirtual(files, createCompanionEmitPlugin());
|
|
402
|
+
|
|
403
|
+
const problems = result.diagnostics.map(
|
|
404
|
+
(d) => `${d.file?.fileName ?? "(project)"}: ${flattenDiagnosticMessage(d.messageText)}`,
|
|
405
|
+
);
|
|
406
|
+
const lua: Record<string, string> = {};
|
|
407
|
+
for (const file of result.transpiledFiles) {
|
|
408
|
+
if (isLualibBundle(file)) {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const source = file.sourceFiles.find((s) => nameByKey.has(s.fileName));
|
|
412
|
+
const name = source === undefined ? undefined : nameByKey.get(source.fileName);
|
|
413
|
+
if (name === undefined || typeof file.lua !== "string") {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (!HSM_LUALIB_MODULES.has(name) && requiresLualib(file.lua)) {
|
|
417
|
+
problems.push(`the compiled ${name} module requires lualib_bundle`);
|
|
418
|
+
}
|
|
419
|
+
lua[name] = file.lua;
|
|
420
|
+
}
|
|
421
|
+
if (problems.length > 0) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
`@defold-typescript/transpiler: the hsm library does not compile:\n ${problems.join("\n ")}`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
compiledHsm = lua;
|
|
427
|
+
return lua;
|
|
428
|
+
}
|
|
429
|
+
|
|
338
430
|
export function transpile(source: string): TranspileResult {
|
|
339
431
|
const project = transpileProject({ files: { "main.ts": source } });
|
|
340
432
|
return {
|