@defold-typescript/transpiler 0.45.0 → 0.47.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/hsm/async.ts CHANGED
@@ -1,8 +1,20 @@
1
1
  import type { EventObject, TaskStart } from "./index";
2
2
 
3
- /** @noSelf */
3
+ /**
4
+ * The second argument of a `sequence` function: a wait that leaving the state cancels, and a flag
5
+ * that tells the state was left.
6
+ * @noSelf
7
+ */
4
8
  export interface SequenceSignal {
9
+ /**
10
+ * Turns `true` once the state is left. Check it after awaiting anything other than `wait`: such
11
+ * a promise can still resolve after the state is left, and the code after it then runs.
12
+ */
5
13
  readonly aborted: boolean;
14
+ /**
15
+ * Resolves after `seconds`, counted by `timer.delay` rather than by `update(dt)`. A wait still
16
+ * pending when the state is left never resolves, so the code after it does not run.
17
+ */
6
18
  readonly wait: (seconds: number) => Promise<void>;
7
19
  }
8
20
 
@@ -32,6 +44,12 @@ function rethrow(reason: unknown): void {
32
44
 
33
45
  // The sequence plugs into the state's task, so its cleanup cancels the pending
34
46
  // waits on every way out of the state; a wait left pending is never resolved.
47
+ /**
48
+ * Turns an `async` function into a state's `task`. The function gets the context and a
49
+ * `SequenceSignal`, and the event it returns is sent to the machine; one returned after the state
50
+ * is left is ignored. An error thrown inside it leaves the machine where it is and is raised
51
+ * again from a timer callback, so it shows in the engine console.
52
+ */
35
53
  export function sequence<Ctx, E extends EventObject>(
36
54
  // biome-ignore lint/suspicious/noConfusingVoidType: `void` lets a sequence end without returning an event.
37
55
  run: (ctx: Ctx, signal: SequenceSignal) => Promise<NoInfer<E> | void>,
package/dist/hsm/debug.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  import type { EventObject, MachineInstance } from "./index";
2
2
 
3
- /** @noSelf */
3
+ /**
4
+ * Returned by `inspect`. Keep it on `self` and call its `draw` from `update`.
5
+ * @noSelf
6
+ */
4
7
  export interface MachineInspector {
8
+ /**
9
+ * Draws the label and the active leaves as debug text, 40 units above `target`'s world
10
+ * position. Each call also advances the frame number the console lines show. In a release
11
+ * build it does nothing.
12
+ */
5
13
  readonly draw: (target: Hash | Url | string) => void;
6
14
  }
7
15
 
@@ -86,6 +94,12 @@ function shownLeaves(leaves: readonly string[]): string {
86
94
  return leaves.length === 0 ? "(stopped)" : shownList(leaves, raw);
87
95
  }
88
96
 
97
+ /**
98
+ * Prints one line when it is called, then one line for each move `instance` makes, each starting
99
+ * with `hsm` and the label. Call it once after `start`. A label another `inspect` call already
100
+ * used gets an ordinal, such as `enemy#2`. In a release build it registers nothing and returns an
101
+ * inspector whose `draw` does nothing, so the calls can stay in shipped code.
102
+ */
89
103
  export function inspect<Ctx, E extends EventObject, P extends string>(
90
104
  instance: MachineInstance<Ctx, E, P>,
91
105
  label: string,
@@ -1,9 +1,27 @@
1
+ /**
2
+ * The event `messageEvents` builds from the Defold message `K`, for a machine's event union. It
3
+ * holds the message id as `type`, the payload fields declared through `BuiltinMessages` and
4
+ * `CustomMessages`, and the `sender`.
5
+ */
1
6
  export type MessageEvent<K extends MessageId> = K extends MessageId
2
- ? { readonly type: K; readonly sender: Url } & Omit<MessagePayload<K>, "type" | "sender">
7
+ ? {
8
+ /** The message id. A payload field named `type` never overrides it. */
9
+ readonly type: K;
10
+ /** The URL the message came from. A payload field named `sender` never overrides it. */
11
+ readonly sender: Url;
12
+ } & Omit<MessagePayload<K>, "type" | "sender">
3
13
  : never;
4
14
 
5
- /** @noSelf */
15
+ /**
16
+ * Returned by `messageEvents`. Build it once at module scope: it hashes the ids when it is
17
+ * created.
18
+ * @noSelf
19
+ */
6
20
  export interface MessageEventMapper<K extends MessageId> {
21
+ /**
22
+ * Turns the parameters of `on_message` into the event for that message, a new table on each
23
+ * call. Returns `undefined` for a message id that `messageEvents` was not given.
24
+ */
7
25
  readonly toEvent: (
8
26
  message_id: Hash,
9
27
  message: Record<string | number, unknown>,
@@ -11,6 +29,11 @@ export interface MessageEventMapper<K extends MessageId> {
11
29
  ) => MessageEvent<K> | undefined;
12
30
  }
13
31
 
32
+ /**
33
+ * Builds a mapper that turns the Defold messages listed in `ids` into typed machine events of
34
+ * the shape `{ type: id, ...payload, sender }`. Call it once at module scope: the ids are hashed
35
+ * here.
36
+ */
14
37
  export function messageEvents<const K extends MessageId>(ids: readonly K[]): MessageEventMapper<K> {
15
38
  const hashes: Hash[] = [];
16
39
  for (let i = 0; i < ids.length; i++) {
package/dist/hsm/index.ts CHANGED
@@ -1,10 +1,25 @@
1
+ /**
2
+ * An event a machine handles: an object with a string `type`, plus any payload fields. A
3
+ * machine's event type is a union of these.
4
+ */
1
5
  export interface EventObject {
6
+ /** Names the event. A state's `on` takes these values as its keys. */
2
7
  readonly type: string;
3
8
  }
4
9
 
10
+ /**
11
+ * What made a move happen: `event` for a sent event, `after` for a timer, `update` for a path an
12
+ * `update` hook returned, `always` for an `always` rule, `stop` for `stop()`, and `reload` for a
13
+ * hot reload that changed the current state.
14
+ */
5
15
  export type MoveCause = "event" | "after" | "update" | "stop" | "reload" | "always";
6
16
 
7
- /** @noSelf */
17
+ /**
18
+ * A listener given to `onMove`. It gets the leaf left, the leaf entered (`undefined` after
19
+ * `stop()`), the cause, and the event, which is `undefined` unless the cause is `event`. It runs
20
+ * after the move's `enter` hooks.
21
+ * @noSelf
22
+ */
8
23
  export type MoveListener<E extends EventObject, P extends string = string> = (
9
24
  from: P,
10
25
  to: P | undefined,
@@ -12,14 +27,41 @@ export type MoveListener<E extends EventObject, P extends string = string> = (
12
27
  event: E | undefined,
13
28
  ) => void;
14
29
 
15
- /** @noSelf */
30
+ /**
31
+ * One running machine, returned by `start`. `P` is the union of the machine's state paths; the
32
+ * `machine` a hook, a `run` or a `task` receives uses plain `string` paths instead.
33
+ * @noSelf
34
+ */
16
35
  export interface MachineInstance<Ctx, E extends EventObject, P extends string = string> {
36
+ /** The context passed to `start`. */
17
37
  readonly ctx: Ctx;
38
+ /**
39
+ * The full path of the deepest active state, or `undefined` once stopped. In a parallel state
40
+ * it is the first region's leaf.
41
+ */
18
42
  readonly path: P | undefined;
43
+ /**
44
+ * The full path of every active leaf, in region order: one path without parallel states, empty
45
+ * once stopped. It is the same array on every read, updated in place.
46
+ */
19
47
  readonly leaves: readonly P[];
48
+ /** Whether the state at the full path `path` is active. Ancestors of the current state count. */
20
49
  readonly matches: (path: P) => boolean;
50
+ /**
51
+ * Handles `event`, or queues it when called during a step. An event no active state handles is
52
+ * dropped, and once the machine is stopped the call does nothing.
53
+ */
21
54
  readonly send: (event: E) => void;
55
+ /**
56
+ * Advances the machine by `dt` seconds: runs the `update` hooks, then the `after` timers if no
57
+ * hook moved it. A machine whose `update` is not called is frozen in time, and once the machine
58
+ * is stopped the call does nothing.
59
+ */
22
60
  readonly update: (dt: number) => void;
61
+ /**
62
+ * Exits every active state, deepest first, and sets `path` to `undefined`. Called during a
63
+ * step, it lets that step finish and drops the queued events.
64
+ */
23
65
  readonly stop: () => void;
24
66
  /**
25
67
  * Adds a listener called after each move completes, with the leaf left, the leaf entered and
@@ -30,6 +72,10 @@ export interface MachineInstance<Ctx, E extends EventObject, P extends string =
30
72
  readonly onMove: (listener: MoveListener<E, P>) => () => void;
31
73
  }
32
74
 
75
+ /**
76
+ * A function a rule's `run` calls during the move, after the `exit` hooks and before the `enter`
77
+ * hooks. Its `event` is the variant the `on` key names.
78
+ */
33
79
  export type MoveAction<Ctx, E extends EventObject, V extends E = E> = (
34
80
  ctx: Ctx,
35
81
  event: V,
@@ -45,7 +91,11 @@ interface RuleList<Rule> {
45
91
  readonly length: number;
46
92
  }
47
93
 
48
- /** @noSelf */
94
+ /**
95
+ * The object form of a rule under `on`: a `to`, a `when` and a `run`, each optional. A rule with
96
+ * no `to` runs its `run` and nothing else: no state exits or enters.
97
+ * @noSelf
98
+ */
49
99
  export interface TransitionConfig<
50
100
  Ctx,
51
101
  E extends EventObject,
@@ -79,6 +129,10 @@ export interface TransitionConfig<
79
129
  readonly reenter?: never;
80
130
  }
81
131
 
132
+ /**
133
+ * What an `on` key maps to: a target path, a `TransitionConfig`, or a non-empty list of them
134
+ * tried in order. The first rule with no `when`, or whose `when` returns `true`, is taken.
135
+ */
82
136
  export type TransitionSpec<
83
137
  Ctx,
84
138
  E extends EventObject,
@@ -86,8 +140,16 @@ export type TransitionSpec<
86
140
  To extends string = string,
87
141
  > = To | TransitionConfig<Ctx, E, V, To> | RuleList<TransitionConfig<Ctx, E, V, To>>;
88
142
 
89
- /** @noSelf */
143
+ /**
144
+ * The object form of an `always` rule: a `to` and an optional `when`. It gets no event and has
145
+ * no `run`; side effects belong in the target's `enter`.
146
+ * @noSelf
147
+ */
90
148
  export interface AlwaysConfig<Ctx, To extends string = string> {
149
+ /**
150
+ * The full path of the state to move to. An `always` rule without one throws when the machine
151
+ * is defined.
152
+ */
91
153
  readonly to: To;
92
154
  /** Checked on every recheck, so it must have no side effects. */
93
155
  readonly when?: (ctx: Ctx) => boolean;
@@ -97,11 +159,19 @@ export interface AlwaysConfig<Ctx, To extends string = string> {
97
159
  readonly guard?: never;
98
160
  }
99
161
 
162
+ /**
163
+ * What `always` takes: a target path, an `AlwaysConfig`, or a non-empty list of them. The first
164
+ * whose `when` is missing or passes is taken.
165
+ */
100
166
  export type AlwaysSpec<Ctx, To extends string = string> =
101
167
  | To
102
168
  | AlwaysConfig<Ctx, To>
103
169
  | RuleList<AlwaysConfig<Ctx, To>>;
104
170
 
171
+ /**
172
+ * A state's rules by event `type`. Only the machine's event types are keys, and each rule's
173
+ * `when` and `run` see the event variant its key names.
174
+ */
105
175
  export type OnConfig<Ctx, E extends EventObject, To extends string = string> = {
106
176
  readonly [K in E["type"]]?: TransitionSpec<Ctx, E, Extract<E, { type: K }>, To>;
107
177
  };
@@ -116,6 +186,11 @@ export type StateHook<Ctx, E extends EventObject> = (
116
186
  machine: MachineInstance<Ctx, E>,
117
187
  ) => void;
118
188
 
189
+ /**
190
+ * Runs on every `update(dt)` while the state is active, deepest state first. It returns a full
191
+ * path to move there, or `undefined` to pass. The first hook to return a path wins: the hooks
192
+ * above it and the `after` timers do not run on that call, and the move has no `run`.
193
+ */
119
194
  export type UpdateHook<Ctx, E extends EventObject, To extends string = string> = (
120
195
  ctx: Ctx,
121
196
  dt: number,
@@ -134,8 +209,17 @@ export type TaskStart<Ctx, E extends EventObject> = (
134
209
  // biome-ignore lint/suspicious/noConfusingVoidType: `void` keeps an expression-body task returning a void call (`=> go.animate(...)`) valid.
135
210
  ) => (() => void) | void;
136
211
 
137
- /** @noSelf */
212
+ /**
213
+ * A machine definition, returned by the second `defineMachine` call. Its `start` creates the
214
+ * instances that run.
215
+ * @noSelf
216
+ */
138
217
  export interface Machine<Ctx, E extends EventObject, P extends string = string> {
218
+ /**
219
+ * Creates an instance with `ctx` as its context and enters the root's `initial` chain. Each
220
+ * call returns a separate instance, which has already handled the events its `enter` hooks
221
+ * sent.
222
+ */
139
223
  readonly start: (ctx: Ctx) => MachineInstance<Ctx, E, P>;
140
224
  }
141
225
 
@@ -173,6 +257,10 @@ type TreeOf<C> = C extends { readonly states: infer Children }
173
257
  ? { readonly [K in keyof Children]: TreeOf<Children[K]> }
174
258
  : unknown;
175
259
 
260
+ /**
261
+ * The union of a config's state paths, each with its leading `/`. Paths are spelled out four
262
+ * levels deep; below that, any string under a fourth-level path is accepted.
263
+ */
176
264
  export type StatePath<C> = TreePath<TreeOf<C>>;
177
265
 
178
266
  // The levels of child states below a node. It counts tree levels, so above a parallel state
@@ -206,14 +294,41 @@ type AnyChild<Ctx, E extends EventObject, T, To extends string> = unknown extend
206
294
  // at the first such callback, before the states beside it are read.
207
295
  /** @noSelf */
208
296
  interface StateBase<Ctx, E extends EventObject, T, Self extends string, To extends string> {
297
+ /** Child states, by name. A name is non-empty and contains no `/`. */
209
298
  readonly states?: {
210
299
  readonly [K in keyof T]: StateNode<Ctx, E, T[K], `${Self}/${K & string}`, To>;
211
300
  } & NoInfer<AnyChild<Ctx, E, T, To>>;
301
+ /**
302
+ * Rules by event `type`, each a target path, a rule object or a list of them. An event starts
303
+ * at the deepest active state and moves up to the parent while no rule takes it.
304
+ */
212
305
  readonly on?: NoInfer<OnConfig<Ctx, E, To>>;
306
+ /**
307
+ * Targets by delay in seconds of game time. The delay is counted by `update(dt)` while the
308
+ * state is active, from zero on each entry. One `update` call fires at most one timer, and
309
+ * none when an `update` hook moved the machine.
310
+ */
213
311
  readonly after?: NoInfer<{ readonly [seconds: number]: To }>;
312
+ /**
313
+ * Targets taken right after any move that leaves this state active; the first whose `when` is
314
+ * missing or passes wins. It is checked after a move only: not after a rule with no `to`, and
315
+ * not on an `update` call that moves nothing.
316
+ */
214
317
  readonly always?: AlwaysSpec<Ctx, To>;
318
+ /**
319
+ * Runs each time the state is entered, outermost state first: on a move, at `start`, and when
320
+ * a hot reload adds the state. It gets no event.
321
+ */
215
322
  readonly enter?: StateHook<Ctx, E>;
323
+ /**
324
+ * Runs each time the state is left, deepest state first, including on `stop()`. A state a hot
325
+ * reload removes is dropped without it.
326
+ */
216
327
  readonly exit?: StateHook<Ctx, E>;
328
+ /**
329
+ * Runs on every `update(dt)` while the state is active. Return one of the machine's full paths
330
+ * to move there, or `undefined` to stay.
331
+ */
217
332
  readonly update?: NoInfer<UpdateHook<Ctx, E, To>>;
218
333
  /** Started on each entry; see `TaskStart`. */
219
334
  readonly task?: TaskStart<Ctx, E>;
@@ -224,14 +339,32 @@ interface StateBase<Ctx, E extends EventObject, T, Self extends string, To exten
224
339
  /** @noSelf */
225
340
  interface CompoundState<Ctx, E extends EventObject, T, Self extends string, To extends string>
226
341
  extends StateBase<Ctx, E, T, Self, To> {
342
+ /**
343
+ * `"parallel"` keeps every child active at once, each as a region with its own active state.
344
+ * Not allowed on the root.
345
+ */
227
346
  readonly type?: never;
347
+ /**
348
+ * The full path of the child entered with this state, such as `/on/bright` inside `on`.
349
+ * Required when `states` is set, unless the state is parallel.
350
+ */
228
351
  readonly initial?: NoInfer<unknown extends T ? string : `${Self}/${keyof T & string}`>;
352
+ /**
353
+ * Makes the state remember which child was active when it was last left, and enter it again
354
+ * instead of `initial`. The count is how many levels down to restore, and `"all"` restores
355
+ * every level below the state. The first entry uses `initial`, and a move that targets a state
356
+ * inside this one lands on that target.
357
+ */
229
358
  readonly restoreDepth?: NoInfer<TreeDepth<T, 4> | "all">;
230
359
  }
231
360
 
232
361
  /** @noSelf */
233
362
  interface ParallelState<Ctx, E extends EventObject, T, Self extends string, To extends string>
234
363
  extends StateBase<Ctx, E, T, Self, To> {
364
+ /**
365
+ * `"parallel"` keeps every child active at once, each as a region with its own active state.
366
+ * Not allowed on the root.
367
+ */
235
368
  readonly type: "parallel";
236
369
  readonly initial?: never;
237
370
  readonly restoreDepth?: never;
@@ -252,8 +385,17 @@ type RootState<Ctx, E extends EventObject, T> = CompoundState<
252
385
  readonly states: object;
253
386
  };
254
387
 
388
+ /**
389
+ * The config of one state. Every field is optional. A state with child `states` is a compound
390
+ * state and needs an `initial`, unless its `type` is `"parallel"`; a state without children is a
391
+ * leaf.
392
+ */
255
393
  export type StateConfig<Ctx, E extends EventObject> = StateNode<Ctx, E, unknown, string, string>;
256
394
 
395
+ /**
396
+ * The root config passed to `defineMachine`. It takes the fields of a state and requires
397
+ * `initial` and `states`. The root has no name and cannot be parallel.
398
+ */
257
399
  export type MachineConfig<Ctx, E extends EventObject> = CompoundState<
258
400
  Ctx,
259
401
  E,
@@ -723,6 +865,22 @@ interface Registered<Ctx, E extends EventObject> {
723
865
 
724
866
  const registered: { [key: string]: Registered<unknown, EventObject> } = {};
725
867
 
868
+ // Adds the fields the root's privateCtx returns to ctx. With onlyMissing, a field ctx already
869
+ // has keeps its value: a hot reload adds new private fields and leaves the old ones alone.
870
+ function fillPrivate(
871
+ target: { [name: string]: unknown },
872
+ root: unknown,
873
+ onlyMissing: boolean,
874
+ ): void {
875
+ const factory = (root as Partial<PrivateCtxConfig<unknown, object>>).privateCtx;
876
+ const fields = (factory?.() || {}) as { [name: string]: unknown };
877
+ for (const name in fields) {
878
+ if (!onlyMissing || target[name] === undefined) {
879
+ target[name] = fields[name];
880
+ }
881
+ }
882
+ }
883
+
726
884
  function createMachine<Ctx, E extends EventObject>(
727
885
  definition: Definition<Ctx, E>,
728
886
  ): Machine<Ctx, E> {
@@ -838,6 +996,7 @@ function createMachine<Ctx, E extends EventObject>(
838
996
  afterDelays = compiled.afterDelays;
839
997
  afterTargets = compiled.afterTargets;
840
998
  always = compiled.always;
999
+ fillPrivate(ctx as never, configs[ROOT], true);
841
1000
  growSlots();
842
1001
  const dropped: number[] = [];
843
1002
  for (let i = 0; i < order.length; i++) {
@@ -1411,6 +1570,7 @@ function createMachine<Ctx, E extends EventObject>(
1411
1570
  }
1412
1571
 
1413
1572
  busy = true;
1573
+ fillPrivate(ctx as never, configs[ROOT], false);
1414
1574
  enterState(ROOT);
1415
1575
  enterDefaults(ROOT, 0);
1416
1576
  refreshLeaves();
@@ -1445,8 +1605,126 @@ function define<Ctx, E extends EventObject>(
1445
1605
 
1446
1606
  // Ctx and E are given explicitly and the config is inferred, so the config takes a second call.
1447
1607
  // A key makes a later definition under it (a hot-reloaded module re-running) rebind live instances.
1608
+ /**
1609
+ * Defines a machine in two calls: `defineMachine<Ctx, E>()` fixes the context and event types and
1610
+ * takes an optional hot-reload key, and the second call takes the config. The config is checked
1611
+ * when that call runs, and a mistake throws, naming the state path. With a key, a later
1612
+ * definition under it puts its states into the machine already built, so every running instance
1613
+ * sees the edit; without one, each call builds a separate machine.
1614
+ */
1448
1615
  export function defineMachine<Ctx, E extends EventObject>(
1449
1616
  key?: string,
1450
1617
  ): <T>(config: RootState<Ctx, E, T>) => Machine<Ctx, E, TreePath<T>> {
1451
1618
  return ((config: MachineConfig<Ctx, E>) => define(key, config)) as never;
1452
1619
  }
1620
+
1621
+ /**
1622
+ * The key a `definePrivateMachine` config adds to the root, beside `initial` and `states`.
1623
+ * @noSelf
1624
+ */
1625
+ export interface PrivateCtxConfig<Ctx, Priv> {
1626
+ /**
1627
+ * Builds the fields only the machine uses. It runs once per `start`, before the root's `enter`
1628
+ * hook, so every instance gets fresh fields of its own; a value written straight into the
1629
+ * config would be one array or table shared by every instance. It takes no arguments: to set
1630
+ * a private field from a field of `Ctx`, do it in the root's `enter` hook.
1631
+ *
1632
+ * The fields are added to the object passed to `start`, which stays the instance's `ctx`, so
1633
+ * restarting with `start(old.ctx)` keeps the public fields and builds the private ones fresh.
1634
+ * A hot reload under the machine's key adds the fields a new factory introduces and leaves
1635
+ * every field that already holds a value alone. A private field may not share a name with a
1636
+ * field of `Ctx`.
1637
+ *
1638
+ * The fields are hidden from callers by type only. At run time `ctx` is one table, and hooks,
1639
+ * `hsm-view` and a debugger see every field.
1640
+ */
1641
+ readonly privateCtx: () => Priv & { readonly [K in keyof Ctx & keyof Priv]: never };
1642
+ }
1643
+
1644
+ /**
1645
+ * Like `defineMachine`, for a machine that keeps fields of its own, such as a counter, a heat
1646
+ * level or a list it builds up. The config's `privateCtx` returns those fields. Every hook,
1647
+ * `when`, `run`, `task` and `update` sees them beside `Ctx`, while `start` takes only `Ctx` and
1648
+ * the instance it returns shows only `Ctx` in its `ctx`. A machine without fields of its own
1649
+ * uses `defineMachine`.
1650
+ *
1651
+ * There are two ways to type the fields.
1652
+ *
1653
+ * @example
1654
+ * **Inferred.** For a few fields, write them inline and let TypeScript infer their types. Add
1655
+ * `as` where the inferred type is wrong: an empty array is `never[]` until it reads
1656
+ * `[] as Hash[]`, and a field holding one of several strings is any `string` until it reads
1657
+ * `"cool" as "cool" | "hot"`.
1658
+ *
1659
+ * ```ts
1660
+ * type TurretEvent = { type: "SPOTTED" } | { type: "LOST" };
1661
+ *
1662
+ * export const turret = definePrivateMachine<{ readonly fireRate: number }, TurretEvent>("turret")({
1663
+ * privateCtx: () => ({ heat: 0, targets: [] as Hash[] }),
1664
+ * initial: "/idle",
1665
+ * states: {
1666
+ * idle: { on: { SPOTTED: "/firing" } },
1667
+ * firing: {
1668
+ * enter: (ctx) => {
1669
+ * ctx.heat += 1 / ctx.fireRate;
1670
+ * },
1671
+ * on: { LOST: "/idle" },
1672
+ * },
1673
+ * },
1674
+ * });
1675
+ *
1676
+ * const gun = turret.start({ fireRate: 2 }); // gun.ctx shows fireRate only
1677
+ * ```
1678
+ *
1679
+ * @example
1680
+ * **A named type.** For more fields, or when a helper outside the config needs the whole
1681
+ * context, declare the fields as an interface and make it the factory's return type. The fields
1682
+ * need no `as`, a misspelled or unknown field is a compile error, and the whole context is
1683
+ * `TurretOptions & TurretPrivate`. `satisfies TurretPrivate` does not do this: it checks the
1684
+ * object but keeps its narrow types, so an empty array stays `never[]`.
1685
+ *
1686
+ * ```ts
1687
+ * interface TurretOptions {
1688
+ * readonly fireRate: number;
1689
+ * }
1690
+ *
1691
+ * interface TurretPrivate {
1692
+ * heat: number;
1693
+ * mode: "cool" | "hot";
1694
+ * }
1695
+ *
1696
+ * type TurretEvent = { type: "SPOTTED" } | { type: "LOST" };
1697
+ *
1698
+ * const overheated = (ctx: TurretOptions & TurretPrivate) => ctx.heat > ctx.fireRate;
1699
+ *
1700
+ * export const turret = definePrivateMachine<TurretOptions, TurretEvent>("turret")({
1701
+ * privateCtx: (): TurretPrivate => ({ heat: 0, mode: "cool" }),
1702
+ * initial: "/idle",
1703
+ * states: {
1704
+ * idle: { on: { SPOTTED: "/firing" } },
1705
+ * firing: {
1706
+ * enter: (ctx) => {
1707
+ * ctx.heat += 1 / ctx.fireRate;
1708
+ * },
1709
+ * always: { to: "/cooling", when: overheated },
1710
+ * on: { LOST: "/idle" },
1711
+ * },
1712
+ * cooling: {
1713
+ * enter: (ctx) => {
1714
+ * ctx.mode = "hot";
1715
+ * },
1716
+ * exit: (ctx) => {
1717
+ * ctx.heat = 0;
1718
+ * ctx.mode = "cool";
1719
+ * },
1720
+ * after: { 2: "/idle" },
1721
+ * },
1722
+ * },
1723
+ * });
1724
+ * ```
1725
+ */
1726
+ export const definePrivateMachine: <Ctx, E extends EventObject>(
1727
+ key?: string,
1728
+ ) => <T, Priv extends object>(
1729
+ config: RootState<Ctx & Priv, E, T> & PrivateCtxConfig<Ctx, Priv>,
1730
+ ) => Machine<Ctx, E, TreePath<T>> = defineMachine as never;
package/dist/index.js CHANGED
@@ -3150,6 +3150,14 @@ function resolvedRequirePaths(sourceFile, checker) {
3150
3150
  }
3151
3151
  return paths;
3152
3152
  }
3153
+ var NO_RESOLUTION_MARKER = "@NoResolution:";
3154
+ function companionRequirePath(argument, paths) {
3155
+ const resolved = paths.get(argument);
3156
+ if (resolved !== undefined) {
3157
+ return resolved;
3158
+ }
3159
+ return argument.startsWith(NO_RESOLUTION_MARKER) ? argument.slice(NO_RESOLUTION_MARKER.length) : argument;
3160
+ }
3153
3161
  function resolveMovedRequires(statements, paths) {
3154
3162
  for (const statement of statements) {
3155
3163
  forEachLuaNode(statement, (node) => {
@@ -3163,10 +3171,7 @@ function resolveMovedRequires(statements, paths) {
3163
3171
  if (argument === undefined || !isStringLiteral7(argument)) {
3164
3172
  return;
3165
3173
  }
3166
- const resolved = paths.get(argument.value);
3167
- if (resolved !== undefined) {
3168
- argument.value = resolved;
3169
- }
3174
+ argument.value = companionRequirePath(argument.value, paths);
3170
3175
  });
3171
3176
  }
3172
3177
  }
@@ -3186,8 +3191,8 @@ function withResolvedRequire(statement, paths) {
3186
3191
  if (argument === undefined || !isStringLiteral7(argument)) {
3187
3192
  return statement;
3188
3193
  }
3189
- const resolved = paths.get(argument.value);
3190
- if (resolved === undefined) {
3194
+ const resolved = companionRequirePath(argument.value, paths);
3195
+ if (resolved === argument.value) {
3191
3196
  return statement;
3192
3197
  }
3193
3198
  return createVariableDeclarationStatement3(createIdentifier4(name.text), createCallExpression4(createIdentifier4("require"), [createStringLiteral3(resolved)]));
@@ -3655,12 +3660,13 @@ function collectOutputs(transpiledFiles, diagnostics, userKeys, emittedCompanion
3655
3660
  ...hsm !== undefined ? { hsmModules: hsm } : {}
3656
3661
  };
3657
3662
  }
3658
- function transpileVirtual(files, companionEmit) {
3663
+ function transpileVirtual(files, companionEmit, overrides = {}) {
3659
3664
  return tstl2.transpileVirtualProject({ ...AMBIENT_FILES, ...files }, {
3660
3665
  luaTarget: tstl2.LuaTarget.Lua51,
3661
3666
  sourceMap: true,
3662
3667
  lib: ["lib.es2022.d.ts"],
3663
3668
  skipLibCheck: true,
3669
+ resolvePackageJsonImports: false,
3664
3670
  noImplicitSelf: true,
3665
3671
  luaPlugins: [
3666
3672
  { plugin: lifecycleErasurePlugin },
@@ -3672,7 +3678,8 @@ function transpileVirtual(files, companionEmit) {
3672
3678
  { plugin: hsmLoweringPlugin },
3673
3679
  { plugin: typeApplicationErasurePlugin },
3674
3680
  { plugin: companionEmit.plugin }
3675
- ]
3681
+ ],
3682
+ ...overrides
3676
3683
  });
3677
3684
  }
3678
3685
  function transpileProject(input) {
@@ -3697,7 +3704,7 @@ function compileHsmModules() {
3697
3704
  nameByKey.set(key, name);
3698
3705
  files[key] = readFileSync(path3.join(sourceDir, `${name}.ts`), "utf8");
3699
3706
  }
3700
- const result = transpileVirtual(files, createCompanionEmitPlugin());
3707
+ const result = transpileVirtual(files, createCompanionEmitPlugin(), { removeComments: true });
3701
3708
  const problems = result.diagnostics.map((d) => `${d.file?.fileName ?? "(project)"}: ${flattenDiagnosticMessage(d.messageText)}`);
3702
3709
  const lua = {};
3703
3710
  for (const file of result.transpiledFiles) {
@@ -3739,6 +3746,7 @@ function compilerOptions(companionEmit) {
3739
3746
  sourceMap: true,
3740
3747
  lib: ["lib.es2022.d.ts"],
3741
3748
  skipLibCheck: true,
3749
+ resolvePackageJsonImports: false,
3742
3750
  noImplicitSelf: true,
3743
3751
  luaPlugins: [
3744
3752
  { plugin: lifecycleErasurePlugin },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/transpiler",
3
- "version": "0.45.0",
3
+ "version": "0.47.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.45.0",
38
+ "@defold-typescript/types": "0.47.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"
@@ -290,6 +290,27 @@ function resolvedRequirePaths(
290
290
  return paths;
291
291
  }
292
292
 
293
+ /**
294
+ * The prefix a lowering puts on the require of a runtime module with no source in
295
+ * the program (`hsm`, the timers), which TSTL's resolver strips from the script.
296
+ */
297
+ const NO_RESOLUTION_MARKER = "@NoResolution:";
298
+
299
+ /**
300
+ * How the companion spells the require the Lua AST spells `argument`: a project
301
+ * source by its resolved path, and a marked runtime module without the marker.
302
+ * Left on, the marker is a path Defold cannot load and Windows cannot even name.
303
+ */
304
+ function companionRequirePath(argument: string, paths: ReadonlyMap<string, string>): string {
305
+ const resolved = paths.get(argument);
306
+ if (resolved !== undefined) {
307
+ return resolved;
308
+ }
309
+ return argument.startsWith(NO_RESOLUTION_MARKER)
310
+ ? argument.slice(NO_RESOLUTION_MARKER.length)
311
+ : argument;
312
+ }
313
+
293
314
  /**
294
315
  * Rewrites every `require` a moved statement carries, at whatever depth — a
295
316
  * re-export's is nested in the `do … end` block TSTL emits for it.
@@ -313,10 +334,7 @@ function resolveMovedRequires(
313
334
  if (argument === undefined || !isStringLiteral(argument)) {
314
335
  return;
315
336
  }
316
- const resolved = paths.get(argument.value);
317
- if (resolved !== undefined) {
318
- argument.value = resolved;
319
- }
337
+ argument.value = companionRequirePath(argument.value, paths);
320
338
  });
321
339
  }
322
340
  }
@@ -339,8 +357,8 @@ function withResolvedRequire(statement: Statement, paths: ReadonlyMap<string, st
339
357
  if (argument === undefined || !isStringLiteral(argument)) {
340
358
  return statement;
341
359
  }
342
- const resolved = paths.get(argument.value);
343
- if (resolved === undefined) {
360
+ const resolved = companionRequirePath(argument.value, paths);
361
+ if (resolved === argument.value) {
344
362
  return statement;
345
363
  }
346
364
  return createVariableDeclarationStatement(
package/src/session.ts CHANGED
@@ -35,6 +35,13 @@ function compilerOptions(companionEmit: CompanionEmitter): tstl.CompilerOptions
35
35
  // Don't cross-check the seeded ambient .d.ts surface against itself; only user
36
36
  // files matter (mirrors transpileProject and the editor's skipLibCheck).
37
37
  skipLibCheck: true,
38
+ // TypeScript turns this on under its default `bundler` resolution. With it on,
39
+ // an error that has to name a module-declared type the file does not import
40
+ // (`Url`, `Hash`, `Vector3`) throws inside the checker, which builds a module
41
+ // specifier from an undefined base path and the virtual host's empty current
42
+ // directory. The virtual host reads every `package.json` as empty, so no `#`
43
+ // import resolves with the option on or off.
44
+ resolvePackageJsonImports: false,
38
45
  // Defold scripts are not OO: free helper functions never receive a context,
39
46
  // so suppress TSTL's implicit `self` parameter and the `_G` call-site filler.
40
47
  noImplicitSelf: true,
package/src/transpile.ts CHANGED
@@ -327,6 +327,7 @@ export function collectOutputs(
327
327
  function transpileVirtual(
328
328
  files: Readonly<Record<string, string>>,
329
329
  companionEmit: ReturnType<typeof createCompanionEmitPlugin>,
330
+ overrides: Pick<tstl.CompilerOptions, "removeComments"> = {},
330
331
  ): tstl.TranspileVirtualProjectResult {
331
332
  return tstl.transpileVirtualProject(
332
333
  { ...AMBIENT_FILES, ...files },
@@ -340,6 +341,13 @@ function transpileVirtual(
340
341
  // Don't cross-check the seeded ambient .d.ts surface against itself; we only
341
342
  // care about diagnostics on user files (mirrors the editor's skipLibCheck).
342
343
  skipLibCheck: true,
344
+ // TypeScript turns this on under its default `bundler` resolution. With it on,
345
+ // an error that has to name a module-declared type the file does not import
346
+ // (`Url`, `Hash`, `Vector3`) throws inside the checker, which builds a module
347
+ // specifier from an undefined base path and the virtual host's empty current
348
+ // directory. The virtual host reads every `package.json` as empty, so no `#`
349
+ // import resolves with the option on or off.
350
+ resolvePackageJsonImports: false,
343
351
  // Defold scripts are not OO: free helper functions never receive a context,
344
352
  // so suppress TSTL's implicit `self` parameter and the `_G` call-site filler.
345
353
  noImplicitSelf: true,
@@ -354,6 +362,7 @@ function transpileVirtual(
354
362
  { plugin: typeApplicationErasurePlugin },
355
363
  { plugin: companionEmit.plugin },
356
364
  ],
365
+ ...overrides,
357
366
  },
358
367
  );
359
368
  }
@@ -398,7 +407,9 @@ export function compileHsmModules(): Readonly<Record<string, string>> {
398
407
  nameByKey.set(key, name);
399
408
  files[key] = readFileSync(path.join(sourceDir, `${name}.ts`), "utf8");
400
409
  }
401
- const result = transpileVirtual(files, createCompanionEmitPlugin());
410
+ // The source's doc comments are for the editor, which reads the declarations; the Lua a
411
+ // project gets carries none of them.
412
+ const result = transpileVirtual(files, createCompanionEmitPlugin(), { removeComments: true });
402
413
 
403
414
  const problems = result.diagnostics.map(
404
415
  (d) => `${d.file?.fileName ?? "(project)"}: ${flattenDiagnosticMessage(d.messageText)}`,