@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.
@@ -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.42.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.42.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
  }
@@ -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,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 {