@defold-typescript/transpiler 0.41.0 → 0.43.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.
@@ -1,2 +1,3 @@
1
1
  export declare const SCAFFOLDED_DEFIGNORE_LINES: string[];
2
- export declare function isDefignoredPath(displayPath: string): boolean;
2
+ export declare function parseDefignore(text: string): string[];
3
+ export declare function isDefignoredPath(displayPath: string, lines?: readonly string[]): boolean;
@@ -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,14 @@ 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 needs the lualib bundle, which hsm must never do.
42
+ */
43
+ export declare function compileHsmModules(): Readonly<Record<string, string>>;
34
44
  export declare function transpile(source: string): TranspileResult;
35
45
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/transpiler",
3
- "version": "0.41.0",
3
+ "version": "0.43.0",
4
4
  "description": "TypeScript-to-Lua build pipeline tuned for Defold's runtime.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,12 +30,12 @@
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.41.0",
38
+ "@defold-typescript/types": "0.43.0",
39
39
  "@typescript-to-lua/language-extensions": "1.19.0",
40
40
  "typescript-to-lua": "^1.36.0"
41
41
  }
@@ -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 unconditionally by both build paths and backed by no TypeScript
7
- // source, so a require of either can never be unresolvable.
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 two generated runtimes are skipped, as is any require whose
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,195 @@
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 type { Plugin } 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
+ export interface HsmModule {
16
+ /** The source file's basename without `.ts`. */
17
+ readonly name: string;
18
+ /** What a user imports. */
19
+ readonly specifier: string;
20
+ /** What the import lowers to, and the module path the CLI writes it under. */
21
+ readonly requireName: string;
22
+ }
23
+
24
+ export function hsmRequireName(name: string): string {
25
+ return `${HSM_REQUIRE_ROOT}.${name}`;
26
+ }
27
+
28
+ function hsmSpecifier(name: string): string {
29
+ return name === "index" ? HSM_MODULE_SPECIFIER : `${HSM_MODULE_SPECIFIER}/${name}`;
30
+ }
31
+
32
+ // Same anchoring as the CLI's version lookup: the module URL, never
33
+ // `import.meta.dir`, so the bundled `dist/index.js` under node still lands on
34
+ // the package root.
35
+ function defaultPackageRoot(): string {
36
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
37
+ }
38
+
39
+ // The one filter the transpiler build (what ships in `dist/hsm/`) and the
40
+ // compile (what reaches a project) both read, so a new module ships without
41
+ // editing a list.
42
+ export function hsmSourceFiles(dir: string): string[] {
43
+ return readdirSync(dir, { withFileTypes: true })
44
+ .filter(
45
+ (entry) =>
46
+ entry.isFile() &&
47
+ entry.name.endsWith(".ts") &&
48
+ !entry.name.endsWith(".test.ts") &&
49
+ !entry.name.endsWith(".d.ts"),
50
+ )
51
+ .map((entry) => entry.name)
52
+ .sort();
53
+ }
54
+
55
+ // The workspace sibling wins so a stale `dist/` never shadows the live source
56
+ // inside this repo; a published install has only the `dist/hsm/` copy.
57
+ export function resolveHsmSourceDir(packageRoot: string = defaultPackageRoot()): string | null {
58
+ const workspace = path.join(packageRoot, "..", "hsm", "src");
59
+ if (existsSync(workspace)) {
60
+ return workspace;
61
+ }
62
+ const shipped = path.join(packageRoot, "dist", "hsm");
63
+ if (existsSync(shipped)) {
64
+ return shipped;
65
+ }
66
+ return null;
67
+ }
68
+
69
+ export function requireHsmSourceDir(): string {
70
+ const dir = resolveHsmSourceDir();
71
+ if (dir === null) {
72
+ throw new Error(
73
+ "@defold-typescript/transpiler: the hsm source is missing from this install; reinstall @defold-typescript/transpiler.",
74
+ );
75
+ }
76
+ return dir;
77
+ }
78
+
79
+ let modules: readonly HsmModule[] | undefined;
80
+
81
+ export function hsmModules(): readonly HsmModule[] {
82
+ modules ??= hsmSourceFiles(requireHsmSourceDir()).map((file) => {
83
+ const name = file.slice(0, -".ts".length);
84
+ return { name, specifier: hsmSpecifier(name), requireName: hsmRequireName(name) };
85
+ });
86
+ return modules;
87
+ }
88
+
89
+ /** The hsm module a require path names, or undefined for any other require. */
90
+ function hsmModuleOfRequire(requirePath: string): HsmModule | undefined {
91
+ if (!requirePath.startsWith(`${HSM_REQUIRE_ROOT}.`)) {
92
+ return undefined;
93
+ }
94
+ return hsmModules().find((module) => module.requireName === requirePath);
95
+ }
96
+
97
+ export function isHsmRequireName(requirePath: string): boolean {
98
+ return hsmModuleOfRequire(requirePath) !== undefined;
99
+ }
100
+
101
+ function hsmModuleOfSpecifier(node: ts.Expression | undefined): HsmModule | undefined {
102
+ if (node === undefined || !ts.isStringLiteral(node)) {
103
+ return undefined;
104
+ }
105
+ const { text } = node;
106
+ if (text !== HSM_MODULE_SPECIFIER && !text.startsWith(`${HSM_MODULE_SPECIFIER}/`)) {
107
+ return undefined;
108
+ }
109
+ return hsmModules().find((module) => module.specifier === text);
110
+ }
111
+
112
+ function requiredHsmNames(lua: string): string[] {
113
+ const names: string[] = [];
114
+ for (const { path: requirePath } of scanEmittedRequires(lua)) {
115
+ const module = hsmModuleOfRequire(requirePath);
116
+ if (module !== undefined) {
117
+ names.push(module.name);
118
+ }
119
+ }
120
+ return names;
121
+ }
122
+
123
+ /**
124
+ * The hsm modules a program needs: every one its chunks require, plus every one
125
+ * those require in turn. Read from emitted Lua rather than the import AST, so an
126
+ * import TypeScript elides (`import type`, an unused binding) selects nothing.
127
+ * `compile` runs only when some chunk requires a module.
128
+ */
129
+ export function hsmClosure(
130
+ chunks: Iterable<string>,
131
+ compile: () => Readonly<Record<string, string>>,
132
+ ): Record<string, string> | undefined {
133
+ const pending: string[] = [];
134
+ for (const lua of chunks) {
135
+ pending.push(...requiredHsmNames(lua));
136
+ }
137
+ if (pending.length === 0) {
138
+ return undefined;
139
+ }
140
+ const compiled = compile();
141
+ const selected: Record<string, string> = {};
142
+ for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
143
+ const lua = compiled[name];
144
+ if (name in selected || lua === undefined) {
145
+ continue;
146
+ }
147
+ selected[name] = lua;
148
+ pending.push(...requiredHsmNames(lua));
149
+ }
150
+ return Object.fromEntries(Object.entries(selected).sort(([a], [b]) => a.localeCompare(b)));
151
+ }
152
+
153
+ // Rewrite an hsm specifier to its flat require, the way the timers lowering
154
+ // does: `@NoResolution:` keeps TSTL from looking for a Lua source behind the
155
+ // `.d.ts` the import type-checks against. No lualib feature is registered,
156
+ // because hsm compiles to plain Lua.
157
+ function lowerSpecifier(module: HsmModule): ts.StringLiteral {
158
+ return ts.factory.createStringLiteral(`@NoResolution:${module.requireName}`);
159
+ }
160
+
161
+ export const hsmLoweringPlugin: Plugin = {
162
+ visitors: {
163
+ [ts.SyntaxKind.ImportDeclaration]: (node, context) => {
164
+ const module = hsmModuleOfSpecifier(node.moduleSpecifier);
165
+ if (module === undefined) {
166
+ return context.superTransformStatements(node);
167
+ }
168
+ return context.superTransformStatements(
169
+ ts.factory.updateImportDeclaration(
170
+ node,
171
+ node.modifiers,
172
+ node.importClause,
173
+ lowerSpecifier(module),
174
+ node.attributes,
175
+ ),
176
+ );
177
+ },
178
+ [ts.SyntaxKind.ExportDeclaration]: (node, context) => {
179
+ const module = hsmModuleOfSpecifier(node.moduleSpecifier);
180
+ if (module === undefined) {
181
+ return context.superTransformStatements(node);
182
+ }
183
+ return context.superTransformStatements(
184
+ ts.factory.updateExportDeclaration(
185
+ node,
186
+ node.modifiers,
187
+ node.isTypeOnly,
188
+ node.exportClause,
189
+ lowerSpecifier(module),
190
+ node.attributes,
191
+ ),
192
+ );
193
+ },
194
+ },
195
+ };
package/src/index.ts CHANGED
@@ -20,6 +20,15 @@ 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
+ resolveHsmSourceDir,
31
+ } from "./hsm-builtin";
23
32
  export { buildInputActionIndex } from "./input-action-index";
24
33
  export type {
25
34
  ExtensionDependency,
@@ -48,7 +57,11 @@ export {
48
57
  stripIncludeBase,
49
58
  } from "./output-paths";
50
59
  export { getProgramDiagnostics } from "./program-diagnostics";
51
- export { isDefignoredPath, SCAFFOLDED_DEFIGNORE_LINES } from "./project-resources";
60
+ export {
61
+ isDefignoredPath,
62
+ parseDefignore,
63
+ SCAFFOLDED_DEFIGNORE_LINES,
64
+ } from "./project-resources";
52
65
  export { rewriteEmittedRequires } from "./require-rewrite";
53
66
  export { buildSceneAddressDeclaration } from "./scene-address-declaration";
54
67
  export type { SceneCollectionRoles, SceneCollectionRolesInput } from "./scene-collection-roles";
@@ -95,7 +108,7 @@ export type {
95
108
  TranspileProjectResult,
96
109
  TranspileResult,
97
110
  } from "./transpile";
98
- export { transpile, transpileProject } from "./transpile";
111
+ export { compileHsmModules, transpile, transpileProject } from "./transpile";
99
112
  export type { ClassifiedSlot } from "./url-address-slots";
100
113
  export {
101
114
  addressClassOfArgument,
@@ -9,12 +9,25 @@
9
9
  // `.vscode`).
10
10
  export const SCAFFOLDED_DEFIGNORE_LINES = ["/node_modules", "/.defold-types", "/.vscode"];
11
11
 
12
- // Whether a project-relative display path lies under one of the lines `init`
13
- // scaffolds. Root-anchored, because a `.defignore` line is a root-relative
14
- // path: `assets/node_modules/tiles.atlas` is a resource Defold loads.
15
- export function isDefignoredPath(displayPath: string): boolean {
12
+ // A project's own `.defignore` text as root-anchored lines, the shape
13
+ // `isDefignoredPath` matches against. A line without its leading `/` names the
14
+ // same root-relative path.
15
+ export function parseDefignore(text: string): string[] {
16
+ return text
17
+ .split("\n")
18
+ .map((line) => line.trim())
19
+ .filter((line) => line !== "")
20
+ .map((line) => (line.startsWith("/") ? line : `/${line}`));
21
+ }
22
+
23
+ // Whether a project-relative display path lies under one of `lines`, by
24
+ // default the ones `init` scaffolds. Root-anchored, because a `.defignore` line
25
+ // is a root-relative path: `assets/node_modules/tiles.atlas` is a resource
26
+ // Defold loads.
27
+ export function isDefignoredPath(
28
+ displayPath: string,
29
+ lines: readonly string[] = SCAFFOLDED_DEFIGNORE_LINES,
30
+ ): boolean {
16
31
  const rooted = `/${displayPath.replace(/^\/+/, "")}`;
17
- return SCAFFOLDED_DEFIGNORE_LINES.some(
18
- (line) => rooted === line || rooted.startsWith(`${line}/`),
19
- );
32
+ return lines.some((line) => rooted === line || rooted.startsWith(`${line}/`));
20
33
  }
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,6 +9,13 @@ import {
9
9
  findDirectGoPropertyCalls,
10
10
  GO_PROPERTY_DIRECT_CALL_MESSAGE,
11
11
  } from "./go-property-direct-call";
12
+ import {
13
+ HSM_REQUIRE_ROOT,
14
+ hsmClosure,
15
+ hsmLoweringPlugin,
16
+ hsmModules,
17
+ requireHsmSourceDir,
18
+ } from "./hsm-builtin";
12
19
  import { lifecycleErasurePlugin } from "./lifecycle-erasure";
13
20
  import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
14
21
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
@@ -63,6 +70,13 @@ export interface TranspileProjectResult {
63
70
  // must write this to the output root. Present only when a user file imports
64
71
  // the module (pay-for-use).
65
72
  readonly timersRuntime?: string;
73
+ // The compiled `hsm` modules the program requires, keyed by module name
74
+ // (`index`, `defold`, ...). An hsm import lowers to
75
+ // `require("defold_typescript_hsm.<module>")`, so the CLI must write each one
76
+ // under the output root. Present only when some chunk requires one
77
+ // (pay-for-use); never carries a source map, since the `.ts` it would point
78
+ // at is not in the user's project.
79
+ readonly hsmModules?: Readonly<Record<string, string>>;
66
80
  }
67
81
 
68
82
  function flattenDiagnosticMessage(
@@ -180,6 +194,14 @@ function buildAmbientFiles(): Record<string, string> {
180
194
  "",
181
195
  ].join("\n"),
182
196
  };
197
+ // The generated `hsm` declarations, at the paths the package `exports` map
198
+ // names, so the build types user code against the same `.d.ts` tsserver
199
+ // reads. hsm's own `.ts` compiles separately, in `compileHsmModules`.
200
+ for (const entry of readdirSync(path.join(TYPES_PKG_ROOT, "hsm"))) {
201
+ if (entry.endsWith(".d.ts")) {
202
+ files[`node_modules/@defold-typescript/types/hsm/${entry}`] = readAmbient(`hsm/${entry}`);
203
+ }
204
+ }
183
205
  // Seed the Lua 5.1 standard library (math/os/string/table/coroutine + base
184
206
  // globals) so user code can call e.g. `math.randomseed(os.time())`.
185
207
  Object.assign(files, collectLuaTypesClosure());
@@ -287,6 +309,8 @@ export function collectOutputs(
287
309
  }
288
310
  }
289
311
 
312
+ const hsm = hsmClosure([...Object.values(lua), ...Object.values(companions)], compileHsmModules);
313
+
290
314
  return {
291
315
  lua,
292
316
  sourceMaps,
@@ -294,38 +318,48 @@ export function collectOutputs(
294
318
  ...(Object.keys(companions).length > 0 ? { companions } : {}),
295
319
  ...(lualib !== undefined ? { lualib } : {}),
296
320
  ...(timersImported ? { timersRuntime: TIMERS_RUNTIME } : {}),
321
+ ...(hsm !== undefined ? { hsmModules: hsm } : {}),
297
322
  };
298
323
  }
299
324
 
325
+ function transpileVirtual(
326
+ files: Readonly<Record<string, string>>,
327
+ companionEmit: ReturnType<typeof createCompanionEmitPlugin>,
328
+ ): tstl.TranspileVirtualProjectResult {
329
+ return tstl.transpileVirtualProject(
330
+ { ...AMBIENT_FILES, ...files },
331
+ {
332
+ luaTarget: tstl.LuaTarget.Lua51,
333
+ sourceMap: true,
334
+ // Already-resolved CompilerOptions, so the value is the lib file name, not
335
+ // the tsconfig spelling "ES2022". Drops the default `lib.dom`, whose
336
+ // `declare var window` shadows Defold's `window` namespace.
337
+ lib: ["lib.es2022.d.ts"],
338
+ // Don't cross-check the seeded ambient .d.ts surface against itself; we only
339
+ // care about diagnostics on user files (mirrors the editor's skipLibCheck).
340
+ skipLibCheck: true,
341
+ // Defold scripts are not OO: free helper functions never receive a context,
342
+ // so suppress TSTL's implicit `self` parameter and the `_G` call-site filler.
343
+ noImplicitSelf: true,
344
+ luaPlugins: [
345
+ { plugin: lifecycleErasurePlugin },
346
+ { plugin: editorScriptErasurePlugin },
347
+ { plugin: messageGuardLoweringPlugin },
348
+ { plugin: windowEventGuardLoweringPlugin },
349
+ { plugin: messageDispatchLoweringPlugin },
350
+ { plugin: timersLoweringPlugin },
351
+ { plugin: hsmLoweringPlugin },
352
+ { plugin: typeApplicationErasurePlugin },
353
+ { plugin: companionEmit.plugin },
354
+ ],
355
+ },
356
+ );
357
+ }
358
+
300
359
  export function transpileProject(input: TranspileProjectInput): TranspileProjectResult {
301
360
  const userKeys = new Set(Object.keys(input.files));
302
- const merged: Record<string, string> = { ...AMBIENT_FILES, ...input.files };
303
361
  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
- });
362
+ const result = transpileVirtual(input.files, companionEmit);
329
363
 
330
364
  return collectOutputs(
331
365
  result.transpiledFiles,
@@ -335,6 +369,54 @@ export function transpileProject(input: TranspileProjectInput): TranspileProject
335
369
  );
336
370
  }
337
371
 
372
+ let compiledHsm: Readonly<Record<string, string>> | undefined;
373
+
374
+ /**
375
+ * Every `hsm` module compiled to Lua, keyed by module name, once per process.
376
+ * The source is the shipped TypeScript, never a prebuilt Lua copy, so the Lua a
377
+ * project gets always comes from the compiler that builds it. Each file sits at
378
+ * `defold_typescript_hsm/<module>.ts`, so a require between modules is already
379
+ * spelled the way the user's lowered imports are. Throws when the compile
380
+ * reports anything or needs the lualib bundle, which hsm must never do.
381
+ */
382
+ export function compileHsmModules(): Readonly<Record<string, string>> {
383
+ if (compiledHsm !== undefined) {
384
+ return compiledHsm;
385
+ }
386
+ const sourceDir = requireHsmSourceDir();
387
+ const nameByKey = new Map<string, string>();
388
+ const files: Record<string, string> = {};
389
+ for (const { name } of hsmModules()) {
390
+ const key = `${HSM_REQUIRE_ROOT}/${name}.ts`;
391
+ nameByKey.set(key, name);
392
+ files[key] = readFileSync(path.join(sourceDir, `${name}.ts`), "utf8");
393
+ }
394
+ const result = transpileVirtual(files, createCompanionEmitPlugin());
395
+
396
+ const problems = result.diagnostics.map(
397
+ (d) => `${d.file?.fileName ?? "(project)"}: ${flattenDiagnosticMessage(d.messageText)}`,
398
+ );
399
+ const lua: Record<string, string> = {};
400
+ for (const file of result.transpiledFiles) {
401
+ if (isLualibBundle(file)) {
402
+ problems.push("the compiled Lua requires lualib_bundle");
403
+ continue;
404
+ }
405
+ const source = file.sourceFiles.find((s) => nameByKey.has(s.fileName));
406
+ const name = source === undefined ? undefined : nameByKey.get(source.fileName);
407
+ if (name !== undefined && typeof file.lua === "string") {
408
+ lua[name] = file.lua;
409
+ }
410
+ }
411
+ if (problems.length > 0) {
412
+ throw new Error(
413
+ `@defold-typescript/transpiler: the hsm library does not compile:\n ${problems.join("\n ")}`,
414
+ );
415
+ }
416
+ compiledHsm = lua;
417
+ return lua;
418
+ }
419
+
338
420
  export function transpile(source: string): TranspileResult {
339
421
  const project = transpileProject({ files: { "main.ts": source } });
340
422
  return {