@defold-typescript/transpiler 0.50.1 → 0.52.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.
@@ -3,6 +3,7 @@ import { type Plugin } from "typescript-to-lua";
3
3
  export declare const FACTORY_MODULE = "@defold-typescript/types";
4
4
  export declare const FACTORY_NAMES: Set<string>;
5
5
  export declare function resolvesToFactoryExport(callee: ts.Expression, checker: ts.TypeChecker): boolean;
6
+ export declare const RESOURCE_PROPERTY_MARKERS: ReadonlySet<string>;
6
7
  export interface PropertyMember {
7
8
  readonly name: string;
8
9
  readonly initializer: ts.Expression;
@@ -0,0 +1,2 @@
1
+ import { type Plugin } from "typescript-to-lua";
2
+ export declare const physicsEventGuardLoweringPlugin: Plugin;
@@ -22,6 +22,7 @@ export interface TranspileProjectResult {
22
22
  readonly lualib?: string;
23
23
  readonly timersRuntime?: string;
24
24
  readonly hsmModules?: Readonly<Record<string, string>>;
25
+ readonly hsmSourceMaps?: Readonly<Record<string, string>>;
25
26
  }
26
27
  export declare const AMBIENT_FILES: Readonly<Record<string, string>>;
27
28
  interface CollectableFile {
@@ -42,5 +43,7 @@ export declare function transpileProject(input: TranspileProjectInput): Transpil
42
43
  * lualib bundle.
43
44
  */
44
45
  export declare function compileHsmModules(): Readonly<Record<string, string>>;
46
+ /** The source map of every module `compileHsmModules` returns, under the same keys. */
47
+ export declare function compileHsmSourceMaps(): Readonly<Record<string, string>>;
45
48
  export declare function transpile(source: string): TranspileResult;
46
49
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/transpiler",
3
- "version": "0.50.1",
3
+ "version": "0.52.0",
4
4
  "description": "TypeScript-to-Lua build pipeline tuned for Defold's runtime.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "test": "bun test"
36
36
  },
37
37
  "dependencies": {
38
- "@defold-typescript/types": "0.50.1",
38
+ "@defold-typescript/types": "0.52.0",
39
39
  "@typescript-to-lua/language-extensions": "1.19.0",
40
40
  "typescript": "6.0.2",
41
41
  "typescript-to-lua": "^1.36.0"
@@ -7,8 +7,8 @@ import { scanEmittedRequires } from "./lua-require-scan";
7
7
 
8
8
  // `@defold-typescript/types/hsm` is the `index` module; every other module is a
9
9
  // subpath of it. Each lowers to a require of `defold_typescript_hsm.<module>`,
10
- // which the CLI writes under the output root the way it writes the timers
11
- // runtime.
10
+ // which the CLI respells to the module's place in its runtime folder, the way it
11
+ // handles the timers runtime.
12
12
  export const HSM_MODULE_SPECIFIER = "@defold-typescript/types/hsm";
13
13
  export const HSM_REQUIRE_ROOT = "defold_typescript_hsm";
14
14
 
package/src/index.ts CHANGED
@@ -46,6 +46,7 @@ export {
46
46
  readGameProjectDependencies,
47
47
  readLibrarySceneDocuments,
48
48
  } from "./library-dependencies";
49
+ export { scanEmittedRequires } from "./lua-require-scan";
49
50
  export { findMainEntryFactoryImports } from "./main-entry-factory-imports";
50
51
  export type { BuildConfig, ScriptKind, SourceOutputKind } from "./output-paths";
51
52
  export {
@@ -35,22 +35,29 @@ const FACTORY_SPECIFIERS = new Set([
35
35
  `${FACTORY_MODULE}/render-script`,
36
36
  ]);
37
37
 
38
- export function resolvesToFactoryExport(callee: ts.Expression, checker: ts.TypeChecker): boolean {
38
+ // The lifecycle factory a callee resolves to, aliases followed, or `undefined`
39
+ // when it is not one of this package's factory exports.
40
+ function factoryExportName(callee: ts.Expression, checker: ts.TypeChecker): string | undefined {
39
41
  let symbol = checker.getSymbolAtLocation(callee);
40
42
  if (symbol === undefined) {
41
- return false;
43
+ return undefined;
42
44
  }
43
45
  if (symbol.flags & ts.SymbolFlags.Alias) {
44
46
  symbol = checker.getAliasedSymbol(symbol);
45
47
  }
46
- if (!FACTORY_NAMES.has(symbol.getName())) {
47
- return false;
48
+ const name = symbol.getName();
49
+ if (!FACTORY_NAMES.has(name)) {
50
+ return undefined;
48
51
  }
49
52
  const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
50
53
  if (declaration === undefined) {
51
- return false;
54
+ return undefined;
52
55
  }
53
- return declaration.getSourceFile().fileName.includes(FACTORY_MODULE);
56
+ return declaration.getSourceFile().fileName.includes(FACTORY_MODULE) ? name : undefined;
57
+ }
58
+
59
+ export function resolvesToFactoryExport(callee: ts.Expression, checker: ts.TypeChecker): boolean {
60
+ return factoryExportName(callee, checker) !== undefined;
54
61
  }
55
62
 
56
63
  function isThisParameter(param: ts.ParameterDeclaration): boolean {
@@ -208,6 +215,20 @@ function emitInitMerge(
208
215
  return [builderDecl, createAssignmentStatement(createIdentifier("init"), initFn, property)];
209
216
  }
210
217
 
218
+ // Build-time markers, never engine functions: bob reads `resource.<kind>("path")`
219
+ // only as a `go.property` default (`LuaScanner.java:923-933` at defold
220
+ // `a81dbb04`), and the engine's `resource` table registers none of them
221
+ // (`script_resource.cpp:3447-3463` in 1.14.0). Called anywhere else, each is nil.
222
+ export const RESOURCE_PROPERTY_MARKERS: ReadonlySet<string> = new Set([
223
+ "resource.atlas",
224
+ "resource.buffer",
225
+ "resource.font",
226
+ "resource.material",
227
+ "resource.render_target",
228
+ "resource.texture",
229
+ "resource.tile_source",
230
+ ]);
231
+
211
232
  export interface PropertyMember {
212
233
  readonly name: string;
213
234
  readonly initializer: ts.Expression;
@@ -275,14 +296,171 @@ function emitPropertyRegistrations(
275
296
  );
276
297
  }
277
298
 
299
+ // Defold builds `go.property` only in `.script` files, so a GUI or render
300
+ // script's `properties` block has nothing it could lower to.
301
+ function scriptOnlyProperties(
302
+ property: ts.ObjectLiteralElementLike,
303
+ factory: string,
304
+ ): ts.Diagnostic {
305
+ const file = property.getSourceFile();
306
+ const start = property.getStart(file);
307
+ return {
308
+ file,
309
+ start,
310
+ length: property.getEnd() - start,
311
+ category: ts.DiagnosticCategory.Error,
312
+ code: 0,
313
+ messageText: `\`${factory}\` cannot declare \`properties\`: Defold builds \`go.property\` only in \`.script\` files. Declare the property in a \`defineScript\` and send its value with \`msg.post\`.`,
314
+ };
315
+ }
316
+
317
+ // The outermost expression that only wraps `node` in parentheses or a type-only
318
+ // form, so `(resource.atlas("/a.atlas") as Hash)` sits where the bare call would.
319
+ function outermostWrapper(node: ts.Expression): ts.Expression {
320
+ let current = node;
321
+ while (
322
+ ts.isParenthesizedExpression(current.parent) ||
323
+ ts.isAsExpression(current.parent) ||
324
+ ts.isSatisfiesExpression(current.parent) ||
325
+ ts.isNonNullExpression(current.parent) ||
326
+ ts.isTypeAssertionExpression(current.parent)
327
+ ) {
328
+ current = current.parent;
329
+ }
330
+ return current;
331
+ }
332
+
333
+ // The inverse of `outermostWrapper`: the expression a run of parentheses and
334
+ // type-only forms wraps, which TSTL emits in place of the whole run.
335
+ function innermostExpression(node: ts.Expression): ts.Expression {
336
+ let current = node;
337
+ while (
338
+ ts.isParenthesizedExpression(current) ||
339
+ ts.isAsExpression(current) ||
340
+ ts.isSatisfiesExpression(current) ||
341
+ ts.isNonNullExpression(current) ||
342
+ ts.isTypeAssertionExpression(current)
343
+ ) {
344
+ current = current.expression;
345
+ }
346
+ return current;
347
+ }
348
+
349
+ // Keyed by the declaration's fully qualified name: only a symbol declared in an
350
+ // ambient `declare global` block carries the `global.` prefix, so a project's
351
+ // own `resource` never matches.
352
+ function globalName(symbol: ts.Symbol | undefined, checker: ts.TypeChecker): string | undefined {
353
+ if (symbol === undefined) {
354
+ return undefined;
355
+ }
356
+ const fqn = checker.getFullyQualifiedName(symbol);
357
+ return fqn.startsWith("global.") ? fqn.slice("global.".length) : undefined;
358
+ }
359
+
360
+ function calledGlobal(call: ts.CallExpression, checker: ts.TypeChecker): string | undefined {
361
+ const declaration = checker.getResolvedSignature(call)?.declaration;
362
+ if (
363
+ declaration === undefined ||
364
+ !ts.isFunctionDeclaration(declaration) ||
365
+ declaration.name === undefined
366
+ ) {
367
+ return undefined;
368
+ }
369
+ return globalName(checker.getSymbolAtLocation(declaration.name), checker);
370
+ }
371
+
372
+ // A `properties` default of a factory call, or the default argument of a
373
+ // direct `go.property` call: the two places bob reads a marker.
374
+ function isPropertyDefault(call: ts.CallExpression, checker: ts.TypeChecker): boolean {
375
+ const value = outermostWrapper(call);
376
+ const parent = value.parent;
377
+ if (ts.isCallExpression(parent)) {
378
+ return parent.arguments[1] === value && calledGlobal(parent, checker) === "go.property";
379
+ }
380
+ const properties = parent?.parent?.parent;
381
+ if (
382
+ properties === undefined ||
383
+ !ts.isPropertyAssignment(properties) ||
384
+ !isPropertiesHook(properties)
385
+ ) {
386
+ return false;
387
+ }
388
+ const hooks = properties.parent;
389
+ return (
390
+ ts.isObjectLiteralExpression(hooks) &&
391
+ factoryHooks(hooks.parent as ts.Expression, checker) === hooks &&
392
+ propertyMembers(properties).some((member) => member.initializer === value)
393
+ );
394
+ }
395
+
396
+ function misplacedMarker(node: ts.Expression, marker: string): ts.Diagnostic {
397
+ const file = node.getSourceFile();
398
+ const start = node.getStart(file);
399
+ return {
400
+ file,
401
+ start,
402
+ length: node.getEnd() - start,
403
+ category: ts.DiagnosticCategory.Error,
404
+ code: 0,
405
+ messageText: `\`${marker}\` is a build-time marker: Defold's build reads it only in a \`go.property\` default, and the engine binds no such function. Use it as a \`defineScript\` \`properties\` default.`,
406
+ };
407
+ }
408
+
409
+ // Bob reads a default as a marker only when the emitted text is
410
+ // `resource.<kind>("<path>")` or `resource.<kind>()`: the object token must be
411
+ // `resource` with a dot `NAME` key, and the first argument a string literal or
412
+ // absent (`LuaScanner.java:801-822`, `:923-933` and `:493-510` at defold
413
+ // `a81dbb04`). The receiver test is textual like bob's; the marker resolution
414
+ // has already excluded a project's own `resource`.
415
+ function readsAsMarker(call: ts.CallExpression): boolean {
416
+ const callee = innermostExpression(call.expression);
417
+ if (ts.isOptionalChain(call) || ts.isOptionalChain(callee)) {
418
+ return false;
419
+ }
420
+ if (
421
+ !ts.isPropertyAccessExpression(callee) &&
422
+ !(
423
+ ts.isElementAccessExpression(callee) &&
424
+ ts.isStringLiteralLike(innermostExpression(callee.argumentExpression))
425
+ )
426
+ ) {
427
+ return false;
428
+ }
429
+ const receiver = innermostExpression(callee.expression);
430
+ if (!ts.isIdentifier(receiver) || receiver.text !== "resource") {
431
+ return false;
432
+ }
433
+ const [path] = call.arguments;
434
+ return path === undefined || ts.isStringLiteralLike(innermostExpression(path));
435
+ }
436
+
437
+ function unreadableMarker(node: ts.CallExpression, marker: string): ts.Diagnostic {
438
+ const file = node.getSourceFile();
439
+ const start = node.getStart(file);
440
+ return {
441
+ file,
442
+ start,
443
+ length: node.getEnd() - start,
444
+ category: ts.DiagnosticCategory.Error,
445
+ code: 0,
446
+ messageText: `\`${marker}\` in a property default must be written \`${marker}("<path>")\`, with a string literal path: Defold's build reads no other form.`,
447
+ };
448
+ }
449
+
450
+ function isCallee(node: ts.Expression): boolean {
451
+ const callee = outermostWrapper(node);
452
+ return ts.isCallExpression(callee.parent) && callee.parent.expression === callee;
453
+ }
454
+
278
455
  function eraseFactoryCall(
279
456
  expression: ts.Expression,
280
457
  context: TransformationContext,
281
458
  ): Statement[] | undefined {
282
459
  const hooks = factoryHooks(expression, context.checker);
283
- if (hooks === undefined) {
460
+ if (hooks === undefined || !ts.isCallExpression(expression)) {
284
461
  return undefined;
285
462
  }
463
+ const factory = factoryExportName(expression.expression, context.checker);
286
464
  const registrations: Statement[] = [];
287
465
  const statements: Statement[] = [];
288
466
  for (const property of hooks.properties) {
@@ -291,7 +469,11 @@ function eraseFactoryCall(
291
469
  continue;
292
470
  }
293
471
  if (name === "properties") {
294
- registrations.push(...emitPropertyRegistrations(property, context));
472
+ if (factory === "defineScript") {
473
+ registrations.push(...emitPropertyRegistrations(property, context));
474
+ } else if (factory !== undefined && propertyMembers(property).length > 0) {
475
+ context.diagnostics.push(scriptOnlyProperties(property, factory));
476
+ }
295
477
  continue;
296
478
  }
297
479
  const fn = hookFunction(property);
@@ -374,5 +556,48 @@ export const lifecycleErasurePlugin: Plugin = {
374
556
  },
375
557
  [ts.SyntaxKind.ImportDeclaration]: (node, context) =>
376
558
  isFactoryOnlyImport(node) ? [] : context.superTransformStatements(node),
559
+ // TSTL's optional-chain lowering also visits nodes it synthesizes, which have
560
+ // no parent and no source span; only parsed nodes are checked.
561
+ [ts.SyntaxKind.CallExpression]: (node, context) => {
562
+ const marker = node.parent === undefined ? undefined : calledGlobal(node, context.checker);
563
+ if (marker !== undefined && RESOURCE_PROPERTY_MARKERS.has(marker)) {
564
+ if (!isPropertyDefault(node, context.checker)) {
565
+ context.diagnostics.push(misplacedMarker(node, marker));
566
+ } else if (!readsAsMarker(node)) {
567
+ context.diagnostics.push(unreadableMarker(node, marker));
568
+ }
569
+ }
570
+ return context.superTransformExpression(node);
571
+ },
572
+ [ts.SyntaxKind.PropertyAccessExpression]: (node, context) => {
573
+ if (node.parent !== undefined && !isCallee(node)) {
574
+ const marker = globalName(context.checker.getSymbolAtLocation(node.name), context.checker);
575
+ if (marker !== undefined && RESOURCE_PROPERTY_MARKERS.has(marker)) {
576
+ context.diagnostics.push(misplacedMarker(node, marker));
577
+ }
578
+ }
579
+ return context.superTransformExpression(node);
580
+ },
581
+ [ts.SyntaxKind.ElementAccessExpression]: (node, context) => {
582
+ if (node.parent !== undefined && !isCallee(node)) {
583
+ // The checker resolves a string key only when it is the direct argument,
584
+ // so a wrapped key is looked up by name on the receiver's type, as the
585
+ // checker does for a direct one.
586
+ const key = innermostExpression(node.argumentExpression);
587
+ const marker = ts.isStringLiteralLike(key)
588
+ ? globalName(
589
+ context.checker.getPropertyOfType(
590
+ context.checker.getTypeAtLocation(node.expression),
591
+ key.text,
592
+ ),
593
+ context.checker,
594
+ )
595
+ : undefined;
596
+ if (marker !== undefined && RESOURCE_PROPERTY_MARKERS.has(marker)) {
597
+ context.diagnostics.push(misplacedMarker(node, marker));
598
+ }
599
+ }
600
+ return context.superTransformExpression(node);
601
+ },
377
602
  },
378
603
  };
@@ -0,0 +1,56 @@
1
+ import * as ts from "typescript";
2
+ import {
3
+ createBinaryExpression,
4
+ createCallExpression,
5
+ createIdentifier,
6
+ createStringLiteral,
7
+ createTableIndexExpression,
8
+ type Expression,
9
+ type Plugin,
10
+ SyntaxKind,
11
+ } from "typescript-to-lua";
12
+
13
+ const GUARD_MODULE = "@defold-typescript/types";
14
+ const GUARD_NAME = "isPhysicsEvent";
15
+
16
+ function resolvesToGuardExport(callee: ts.Expression, checker: ts.TypeChecker): boolean {
17
+ let symbol = checker.getSymbolAtLocation(callee);
18
+ if (symbol === undefined) {
19
+ return false;
20
+ }
21
+ if (symbol.flags & ts.SymbolFlags.Alias) {
22
+ symbol = checker.getAliasedSymbol(symbol);
23
+ }
24
+ if (symbol.getName() !== GUARD_NAME) {
25
+ return false;
26
+ }
27
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
28
+ if (declaration === undefined) {
29
+ return false;
30
+ }
31
+ return declaration.getSourceFile().fileName.includes(GUARD_MODULE);
32
+ }
33
+
34
+ export const physicsEventGuardLoweringPlugin: Plugin = {
35
+ visitors: {
36
+ [ts.SyntaxKind.CallExpression]: (node, context): Expression => {
37
+ const [eventArg, nameArg] = node.arguments;
38
+ if (
39
+ node.arguments.length === 2 &&
40
+ eventArg !== undefined &&
41
+ nameArg !== undefined &&
42
+ resolvesToGuardExport(node.expression, context.checker)
43
+ ) {
44
+ const event = context.transformExpression(eventArg);
45
+ const name = context.transformExpression(nameArg);
46
+ return createBinaryExpression(
47
+ createTableIndexExpression(event, createStringLiteral("type")),
48
+ createCallExpression(createIdentifier("hash"), [name]),
49
+ SyntaxKind.EqualityOperator,
50
+ node,
51
+ );
52
+ }
53
+ return context.superTransformExpression(node);
54
+ },
55
+ },
56
+ };
@@ -3,6 +3,7 @@ import * as tstl from "typescript-to-lua";
3
3
  import { lifecycleErasurePlugin } from "./lifecycle-erasure";
4
4
  import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
5
5
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
6
+ import { physicsEventGuardLoweringPlugin } from "./physics-event-guard-lowering";
6
7
  import { timersLoweringPlugin } from "./timers-lowering";
7
8
  import { typeApplicationErasurePlugin } from "./type-application-erasure";
8
9
  import { windowEventGuardLoweringPlugin } from "./window-event-guard-lowering";
@@ -13,6 +14,7 @@ const LUA_PLUGINS: tstl.Plugin[] = [
13
14
  lifecycleErasurePlugin,
14
15
  messageGuardLoweringPlugin,
15
16
  windowEventGuardLoweringPlugin,
17
+ physicsEventGuardLoweringPlugin,
16
18
  messageDispatchLoweringPlugin,
17
19
  timersLoweringPlugin,
18
20
  typeApplicationErasurePlugin,
package/src/session.ts CHANGED
@@ -9,6 +9,7 @@ import { hsmLoweringPlugin } from "./hsm-builtin";
9
9
  import { lifecycleErasurePlugin } from "./lifecycle-erasure";
10
10
  import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
11
11
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
12
+ import { physicsEventGuardLoweringPlugin } from "./physics-event-guard-lowering";
12
13
  import { timersLoweringPlugin } from "./timers-lowering";
13
14
  import { AMBIENT_FILES, collectOutputs, type TranspileProjectResult } from "./transpile";
14
15
  import { typeApplicationErasurePlugin } from "./type-application-erasure";
@@ -50,6 +51,7 @@ function compilerOptions(companionEmit: CompanionEmitter): tstl.CompilerOptions
50
51
  { plugin: editorScriptErasurePlugin },
51
52
  { plugin: messageGuardLoweringPlugin },
52
53
  { plugin: windowEventGuardLoweringPlugin },
54
+ { plugin: physicsEventGuardLoweringPlugin },
53
55
  { plugin: messageDispatchLoweringPlugin },
54
56
  { plugin: timersLoweringPlugin },
55
57
  { plugin: hsmLoweringPlugin },
@@ -1,7 +1,7 @@
1
1
  // The importable polyfill specifier (matched by the type checker and the
2
2
  // lowering pass) and the flat Lua require name the import is rewritten to. The
3
- // CLI writes the runtime to `<require name>.lua` at the output root, mirroring
4
- // `lualib_bundle`, so `require("defold_typescript_timers")` resolves in Defold.
3
+ // CLI writes the runtime into its runtime folder beside `lualib_bundle` and
4
+ // respells every `require("defold_typescript_timers")` to that path.
5
5
  export const TIMERS_MODULE_SPECIFIER = "@defold-typescript/types/timers";
6
6
  export const TIMERS_REQUIRE_NAME = "defold_typescript_timers";
7
7
 
package/src/transpile.ts CHANGED
@@ -21,6 +21,7 @@ import { lifecycleErasurePlugin } from "./lifecycle-erasure";
21
21
  import { scanEmittedRequires } from "./lua-require-scan";
22
22
  import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
23
23
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
24
+ import { physicsEventGuardLoweringPlugin } from "./physics-event-guard-lowering";
24
25
  import { importsTimersModule, timersLoweringPlugin } from "./timers-lowering";
25
26
  import { TIMERS_RUNTIME } from "./timers-runtime";
26
27
  import { typeApplicationErasurePlugin } from "./type-application-erasure";
@@ -62,23 +63,27 @@ export interface TranspileProjectResult {
62
63
  // TSTL synthesizes this bundle (no user source) whenever a lualib feature is
63
64
  // registered — either by a user source using one (`Object.keys`, spread,
64
65
  // `__TS__TypeOf`, ...) or by the timers import lowering, whose generated
65
- // runtime requires the bundle. The emitted `require("lualib_bundle")` only
66
- // resolves in Defold if the CLI writes it to the output root. Absent when
66
+ // runtime requires the bundle. The emitted `require("lualib_bundle")` is an
67
+ // internal name the CLI respells to where it writes the bundle. Absent when
67
68
  // neither trigger fires.
68
69
  readonly lualib?: string;
69
70
  // Hand-authored runtime Lua for the `@defold-typescript/types/timers`
70
71
  // polyfills (`setTimeout`/`wait`/...). Unlike `defineScript`, the import is
71
- // not erased — it lowers to `require("defold_typescript_timers")`, so the CLI
72
- // must write this to the output root. Present only when a user file imports
73
- // the module (pay-for-use).
72
+ // not erased — it lowers to `require("defold_typescript_timers")`, an
73
+ // internal name the CLI respells to where it writes this. Present only when
74
+ // a user file imports the module (pay-for-use).
74
75
  readonly timersRuntime?: string;
75
76
  // The compiled `hsm` modules the program requires, keyed by module name
76
77
  // (`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.
78
+ // `require("defold_typescript_hsm.<module>")`, an internal name the CLI
79
+ // respells to where it writes each one. Present only when some chunk requires one
80
+ // (pay-for-use).
81
81
  readonly hsmModules?: Readonly<Record<string, string>>;
82
+ // The source map of each module in `hsmModules`, under the same keys. Its
83
+ // `sources` names `<module>.ts` with an empty `sourceRoot`: only the CLI knows
84
+ // where the module is written and where the hsm source sits, so it sets the
85
+ // root that joins them.
86
+ readonly hsmSourceMaps?: Readonly<Record<string, string>>;
82
87
  }
83
88
 
84
89
  function flattenDiagnosticMessage(
@@ -312,6 +317,7 @@ export function collectOutputs(
312
317
  }
313
318
 
314
319
  const hsm = hsmClosure([...Object.values(lua), ...Object.values(companions)], compileHsmModules);
320
+ const hsmSourceMaps = hsm === undefined ? undefined : selectHsmSourceMaps(Object.keys(hsm));
315
321
 
316
322
  return {
317
323
  lua,
@@ -321,9 +327,22 @@ export function collectOutputs(
321
327
  ...(lualib !== undefined ? { lualib } : {}),
322
328
  ...(timersImported ? { timersRuntime: TIMERS_RUNTIME } : {}),
323
329
  ...(hsm !== undefined ? { hsmModules: hsm } : {}),
330
+ ...(hsmSourceMaps !== undefined ? { hsmSourceMaps } : {}),
324
331
  };
325
332
  }
326
333
 
334
+ function selectHsmSourceMaps(names: readonly string[]): Record<string, string> {
335
+ const maps = compileHsmSourceMaps();
336
+ const selected: Record<string, string> = {};
337
+ for (const name of names) {
338
+ const map = maps[name];
339
+ if (map !== undefined) {
340
+ selected[name] = map;
341
+ }
342
+ }
343
+ return selected;
344
+ }
345
+
327
346
  function transpileVirtual(
328
347
  files: Readonly<Record<string, string>>,
329
348
  companionEmit: ReturnType<typeof createCompanionEmitPlugin>,
@@ -356,6 +375,7 @@ function transpileVirtual(
356
375
  { plugin: editorScriptErasurePlugin },
357
376
  { plugin: messageGuardLoweringPlugin },
358
377
  { plugin: windowEventGuardLoweringPlugin },
378
+ { plugin: physicsEventGuardLoweringPlugin },
359
379
  { plugin: messageDispatchLoweringPlugin },
360
380
  { plugin: timersLoweringPlugin },
361
381
  { plugin: hsmLoweringPlugin },
@@ -380,7 +400,12 @@ export function transpileProject(input: TranspileProjectInput): TranspileProject
380
400
  );
381
401
  }
382
402
 
383
- let compiledHsm: Readonly<Record<string, string>> | undefined;
403
+ interface CompiledHsm {
404
+ readonly lua: Readonly<Record<string, string>>;
405
+ readonly sourceMaps: Readonly<Record<string, string>>;
406
+ }
407
+
408
+ let compiledHsm: CompiledHsm | undefined;
384
409
 
385
410
  function requiresLualib(lua: string): boolean {
386
411
  return scanEmittedRequires(lua).some(({ path: requirePath }) => requirePath === "lualib_bundle");
@@ -396,6 +421,15 @@ function requiresLualib(lua: string): boolean {
396
421
  * lualib bundle.
397
422
  */
398
423
  export function compileHsmModules(): Readonly<Record<string, string>> {
424
+ return compileHsm().lua;
425
+ }
426
+
427
+ /** The source map of every module `compileHsmModules` returns, under the same keys. */
428
+ export function compileHsmSourceMaps(): Readonly<Record<string, string>> {
429
+ return compileHsm().sourceMaps;
430
+ }
431
+
432
+ function compileHsm(): CompiledHsm {
399
433
  if (compiledHsm !== undefined) {
400
434
  return compiledHsm;
401
435
  }
@@ -415,6 +449,7 @@ export function compileHsmModules(): Readonly<Record<string, string>> {
415
449
  (d) => `${d.file?.fileName ?? "(project)"}: ${flattenDiagnosticMessage(d.messageText)}`,
416
450
  );
417
451
  const lua: Record<string, string> = {};
452
+ const sourceMaps: Record<string, string> = {};
418
453
  for (const file of result.transpiledFiles) {
419
454
  if (isLualibBundle(file)) {
420
455
  continue;
@@ -428,14 +463,17 @@ export function compileHsmModules(): Readonly<Record<string, string>> {
428
463
  problems.push(`the compiled ${name} module requires lualib_bundle`);
429
464
  }
430
465
  lua[name] = file.lua;
466
+ if (typeof file.luaSourceMap === "string") {
467
+ sourceMaps[name] = file.luaSourceMap;
468
+ }
431
469
  }
432
470
  if (problems.length > 0) {
433
471
  throw new Error(
434
472
  `@defold-typescript/transpiler: the hsm library does not compile:\n ${problems.join("\n ")}`,
435
473
  );
436
474
  }
437
- compiledHsm = lua;
438
- return lua;
475
+ compiledHsm = { lua, sourceMaps };
476
+ return compiledHsm;
439
477
  }
440
478
 
441
479
  export function transpile(source: string): TranspileResult {