@defold-typescript/transpiler 0.44.0 → 0.45.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.
Files changed (2) hide show
  1. package/dist/hsm/index.ts +151 -117
  2. package/package.json +2 -2
package/dist/hsm/index.ts CHANGED
@@ -36,13 +36,27 @@ export type MoveAction<Ctx, E extends EventObject, V extends E = E> = (
36
36
  machine: MachineInstance<Ctx, E>,
37
37
  ) => void;
38
38
 
39
+ // A list with a first entry: Lua cannot tell [] from {}, so an empty list would compile to a
40
+ // transition with no to. It is an object type, not a tuple, because TypeScript reports a bad
41
+ // `to` in the object form on that `to` only when the spec union holds no array type.
42
+ interface RuleList<Rule> {
43
+ readonly 0: Rule;
44
+ readonly [index: number]: Rule;
45
+ readonly length: number;
46
+ }
47
+
39
48
  /** @noSelf */
40
- export interface TransitionConfig<Ctx, E extends EventObject, V extends E = E> {
49
+ export interface TransitionConfig<
50
+ Ctx,
51
+ E extends EventObject,
52
+ V extends E = E,
53
+ To extends string = string,
54
+ > {
41
55
  /**
42
56
  * The full path of the state to move to. A `to` naming the state the rule is on exits and
43
57
  * re-enters it, restarting its timers and `task`; a `to` below it keeps it active.
44
58
  */
45
- readonly to?: string;
59
+ readonly to?: To;
46
60
  /**
47
61
  * The rule is taken only when this returns `true`. It must have no side effects: it is also
48
62
  * checked when the move does not happen, in guarded lists and on every `always` recheck.
@@ -65,15 +79,16 @@ export interface TransitionConfig<Ctx, E extends EventObject, V extends E = E> {
65
79
  readonly reenter?: never;
66
80
  }
67
81
 
68
- export type TransitionSpec<Ctx, E extends EventObject, V extends E = E> =
69
- | string
70
- | TransitionConfig<Ctx, E, V>
71
- // Lua cannot tell [] from {}, so an empty list would compile to a transition with no to.
72
- | readonly [TransitionConfig<Ctx, E, V>, ...TransitionConfig<Ctx, E, V>[]];
82
+ export type TransitionSpec<
83
+ Ctx,
84
+ E extends EventObject,
85
+ V extends E = E,
86
+ To extends string = string,
87
+ > = To | TransitionConfig<Ctx, E, V, To> | RuleList<TransitionConfig<Ctx, E, V, To>>;
73
88
 
74
89
  /** @noSelf */
75
- export interface AlwaysConfig<Ctx> {
76
- readonly to: string;
90
+ export interface AlwaysConfig<Ctx, To extends string = string> {
91
+ readonly to: To;
77
92
  /** Checked on every recheck, so it must have no side effects. */
78
93
  readonly when?: (ctx: Ctx) => boolean;
79
94
  /** Renamed to `to`. */
@@ -82,13 +97,13 @@ export interface AlwaysConfig<Ctx> {
82
97
  readonly guard?: never;
83
98
  }
84
99
 
85
- export type AlwaysSpec<Ctx> =
86
- | string
87
- | AlwaysConfig<Ctx>
88
- | readonly [AlwaysConfig<Ctx>, ...AlwaysConfig<Ctx>[]];
100
+ export type AlwaysSpec<Ctx, To extends string = string> =
101
+ | To
102
+ | AlwaysConfig<Ctx, To>
103
+ | RuleList<AlwaysConfig<Ctx, To>>;
89
104
 
90
- export type OnConfig<Ctx, E extends EventObject> = {
91
- readonly [K in E["type"]]?: TransitionSpec<Ctx, E, Extract<E, { type: K }>>;
105
+ export type OnConfig<Ctx, E extends EventObject, To extends string = string> = {
106
+ readonly [K in E["type"]]?: TransitionSpec<Ctx, E, Extract<E, { type: K }>, To>;
92
107
  };
93
108
 
94
109
  /**
@@ -101,11 +116,11 @@ export type StateHook<Ctx, E extends EventObject> = (
101
116
  machine: MachineInstance<Ctx, E>,
102
117
  ) => void;
103
118
 
104
- export type UpdateHook<Ctx, E extends EventObject> = (
119
+ export type UpdateHook<Ctx, E extends EventObject, To extends string = string> = (
105
120
  ctx: Ctx,
106
121
  dt: number,
107
122
  machine: MachineInstance<Ctx, E>,
108
- ) => string | undefined;
123
+ ) => To | undefined;
109
124
 
110
125
  /**
111
126
  * Starts work that lives as long as the state. `finish` sends one event and is ignored after the
@@ -119,29 +134,6 @@ export type TaskStart<Ctx, E extends EventObject> = (
119
134
  // biome-ignore lint/suspicious/noConfusingVoidType: `void` keeps an expression-body task returning a void call (`=> go.animate(...)`) valid.
120
135
  ) => (() => void) | void;
121
136
 
122
- /** @noSelf */
123
- export interface StateConfig<Ctx, E extends EventObject> {
124
- readonly type?: "parallel";
125
- readonly initial?: string;
126
- readonly restoreDepth?: number | "all";
127
- readonly states?: { readonly [name: string]: StateConfig<Ctx, E> };
128
- readonly on?: OnConfig<Ctx, E>;
129
- readonly after?: { readonly [seconds: number]: string };
130
- readonly always?: AlwaysSpec<Ctx>;
131
- readonly enter?: StateHook<Ctx, E>;
132
- readonly exit?: StateHook<Ctx, E>;
133
- readonly update?: UpdateHook<Ctx, E>;
134
- /** Started on each entry; see `TaskStart`. */
135
- readonly task?: TaskStart<Ctx, E>;
136
- /** Renamed to `task`. */
137
- readonly invoke?: never;
138
- }
139
-
140
- export interface MachineConfig<Ctx, E extends EventObject> extends StateConfig<Ctx, E> {
141
- readonly initial: string;
142
- readonly states: { readonly [name: string]: StateConfig<Ctx, E> };
143
- }
144
-
145
137
  /** @noSelf */
146
138
  export interface Machine<Ctx, E extends EventObject, P extends string = string> {
147
139
  readonly start: (ctx: Ctx) => MachineInstance<Ctx, E, P>;
@@ -149,92 +141,129 @@ export interface Machine<Ctx, E extends EventObject, P extends string = string>
149
141
 
150
142
  type PathDepth = [never, 0, 1, 2, 3];
151
143
 
152
- type PathsBelow<S, D extends number> = S extends { readonly states: infer Children }
153
- ? {
154
- [K in keyof Children & string]:
144
+ type NextCount = [never, 2, 3, 4, 5, 6];
145
+
146
+ // The paths a started machine can be in. A node with no children is `unknown`.
147
+ type TreePathsBelow<T, D extends number> = {
148
+ [K in keyof T & string]:
149
+ | K
150
+ | `${K}/${D extends 1
151
+ ? keyof T[K] extends never
152
+ ? never
153
+ : string
154
+ : TreePathsBelow<T[K], PathDepth[D]>}`;
155
+ }[keyof T & string];
156
+
157
+ type TreePath<T> = `/${TreePathsBelow<T, 4>}`;
158
+
159
+ // The paths a config slot accepts. TypeScript checks a config once before it types the
160
+ // callbacks, and a level whose states hold only callbacks has no inferred children in that
161
+ // check, so a node with no inferred children accepts any path below it.
162
+ type SlotPathsBelow<T, D extends number> = unknown extends T
163
+ ? string
164
+ : {
165
+ [K in keyof T & string]:
155
166
  | K
156
- | `${K}/${D extends 1
157
- ? Children[K] extends { readonly states: object }
158
- ? string
159
- : never
160
- : PathsBelow<Children[K], PathDepth[D]>}`;
161
- }[keyof Children & string]
162
- : never;
167
+ | `${K}/${D extends 1 ? string : SlotPathsBelow<T[K], PathDepth[D]>}`;
168
+ }[keyof T & string];
163
169
 
164
- export type StatePath<C> = `/${PathsBelow<C, 4>}`;
170
+ type SlotPath<T> = `/${SlotPathsBelow<T, 4>}`;
165
171
 
166
- type NextCount = [never, 2, 3, 4, 5, 6];
172
+ type TreeOf<C> = C extends { readonly states: infer Children }
173
+ ? { readonly [K in keyof Children]: TreeOf<Children[K]> }
174
+ : unknown;
167
175
 
168
- type RestoreCountBelow<Children, D extends number> = {
169
- [K in keyof Children]: RestoreCount<Children[K], D>;
170
- }[keyof Children];
176
+ export type StatePath<C> = TreePath<TreeOf<C>>;
171
177
 
172
- // The counts a state's subtree can hold: a compound state is one level plus its deepest child,
173
- // a parallel state adds no level of its own, and a leaf holds none.
174
- type RestoreCount<S, D extends number> = S extends { readonly states: infer Children }
175
- ? [D] extends [never]
178
+ // The levels of child states below a node. It counts tree levels, so above a parallel state
179
+ // it is an upper bound on the counts restoreDepth takes.
180
+ type TreeDepth<T, D extends number> = unknown extends T
181
+ ? number
182
+ : [D] extends [never]
176
183
  ? number
177
- : S extends { readonly type: "parallel" }
178
- ? RestoreCountBelow<Children, PathDepth[D]>
179
- :
180
- | 1
181
- | (RestoreCountBelow<Children, PathDepth[D]> extends infer N
182
- ? N extends number
183
- ? number extends N
184
- ? number
185
- : NextCount[N]
186
- : never
187
- : never)
188
- : never;
189
-
190
- interface TransitionCheck<T> {
191
- readonly to?: T;
192
- readonly when?: unknown;
193
- readonly run?: unknown;
184
+ :
185
+ | 1
186
+ | ({
187
+ [K in keyof T]: unknown extends T[K] ? never : TreeDepth<T[K], PathDepth[D]>;
188
+ }[keyof T] extends infer N
189
+ ? N extends number
190
+ ? number extends N
191
+ ? number
192
+ : NextCount[N]
193
+ : never
194
+ : never);
195
+
196
+ type AnyChild<Ctx, E extends EventObject, T, To extends string> = unknown extends T
197
+ ? { readonly [name: string]: StateNode<Ctx, E, unknown, string, To> }
198
+ : unknown;
199
+
200
+ // `states` is the only slot that infers the tree; every other slot that mentions it is wrapped
201
+ // in NoInfer. `on`, `after` and `update` are wrapped whole so inference never reads them:
202
+ // without exactOptionalPropertyTypes, reading `on` asks for the paths while the tree is still
203
+ // being inferred, and a valid config fails as circular. `always` is wrapped only through `To`:
204
+ // NoInfer over a union that holds object types hides its string members from completion.
205
+ // No callback parameter mentions the tree: one that did would make TypeScript settle the tree
206
+ // at the first such callback, before the states beside it are read.
207
+ /** @noSelf */
208
+ interface StateBase<Ctx, E extends EventObject, T, Self extends string, To extends string> {
209
+ readonly states?: {
210
+ readonly [K in keyof T]: StateNode<Ctx, E, T[K], `${Self}/${K & string}`, To>;
211
+ } & NoInfer<AnyChild<Ctx, E, T, To>>;
212
+ readonly on?: NoInfer<OnConfig<Ctx, E, To>>;
213
+ readonly after?: NoInfer<{ readonly [seconds: number]: To }>;
214
+ readonly always?: AlwaysSpec<Ctx, To>;
215
+ readonly enter?: StateHook<Ctx, E>;
216
+ readonly exit?: StateHook<Ctx, E>;
217
+ readonly update?: NoInfer<UpdateHook<Ctx, E, To>>;
218
+ /** Started on each entry; see `TaskStart`. */
219
+ readonly task?: TaskStart<Ctx, E>;
220
+ /** Renamed to `task`. */
221
+ readonly invoke?: never;
194
222
  }
195
223
 
196
- type SpecCheck<T> = T | TransitionCheck<T> | readonly TransitionCheck<T>[];
197
-
198
- // Hooks are listed as unknown so a hooks-only leaf still shares a property with this
199
- // all-optional type; without them TypeScript rejects it as a weak-type mismatch.
200
- type PathCheck<S, Self extends string, All extends string, Ev extends string> = {
201
- readonly type?: unknown;
202
- readonly initial?: S extends { readonly type: "parallel" }
203
- ? never
204
- : S extends { readonly states: infer Children }
205
- ? `${Self}/${keyof Children & string}`
206
- : never;
207
- readonly restoreDepth?: S extends { readonly type: "parallel" }
208
- ? never
209
- : S extends { readonly states: object }
210
- ? RestoreCount<S, 4> | "all"
211
- : never;
212
- readonly states?: S extends { readonly states: infer Children }
213
- ? {
214
- readonly [K in keyof Children]: PathCheck<Children[K], `${Self}/${K & string}`, All, Ev>;
215
- }
216
- : unknown;
217
- readonly on?: S extends { readonly on: infer On }
218
- ? { readonly [K in keyof On]: K extends Ev ? SpecCheck<All> : never }
219
- : unknown;
220
- readonly after?: S extends { readonly after: infer After }
221
- ? { readonly [K in keyof After]: All }
222
- : unknown;
223
- readonly always?: S extends { readonly always: unknown } ? SpecCheck<All> : unknown;
224
- readonly enter?: unknown;
225
- readonly exit?: unknown;
226
- readonly update?: unknown;
227
- readonly task?: unknown;
228
- };
224
+ /** @noSelf */
225
+ interface CompoundState<Ctx, E extends EventObject, T, Self extends string, To extends string>
226
+ extends StateBase<Ctx, E, T, Self, To> {
227
+ readonly type?: never;
228
+ readonly initial?: NoInfer<unknown extends T ? string : `${Self}/${keyof T & string}`>;
229
+ readonly restoreDepth?: NoInfer<TreeDepth<T, 4> | "all">;
230
+ }
229
231
 
230
- export interface MachineConfigError {
231
- readonly "hsm: an initial or a to names an unknown state path, or an on key an unknown event": never;
232
+ /** @noSelf */
233
+ interface ParallelState<Ctx, E extends EventObject, T, Self extends string, To extends string>
234
+ extends StateBase<Ctx, E, T, Self, To> {
235
+ readonly type: "parallel";
236
+ readonly initial?: never;
237
+ readonly restoreDepth?: never;
232
238
  }
233
239
 
234
- export type DefinedMachine<Ctx, E extends EventObject, C> =
235
- C extends PathCheck<C, "", StatePath<C>, E["type"]>
236
- ? Machine<Ctx, E, StatePath<C>>
237
- : MachineConfigError;
240
+ type StateNode<Ctx, E extends EventObject, T, Self extends string, To extends string> =
241
+ | CompoundState<Ctx, E, T, Self, To>
242
+ | ParallelState<Ctx, E, T, Self, To>;
243
+
244
+ type RootState<Ctx, E extends EventObject, T> = CompoundState<
245
+ Ctx,
246
+ E,
247
+ T,
248
+ "",
249
+ NoInfer<SlotPath<T>>
250
+ > & {
251
+ readonly initial: string;
252
+ readonly states: object;
253
+ };
254
+
255
+ export type StateConfig<Ctx, E extends EventObject> = StateNode<Ctx, E, unknown, string, string>;
256
+
257
+ export type MachineConfig<Ctx, E extends EventObject> = CompoundState<
258
+ Ctx,
259
+ E,
260
+ unknown,
261
+ string,
262
+ string
263
+ > & {
264
+ readonly initial: string;
265
+ readonly states: { readonly [name: string]: StateConfig<Ctx, E> };
266
+ };
238
267
 
239
268
  type Guard<Ctx, E extends EventObject> = (ctx: Ctx, event: E) => boolean;
240
269
 
@@ -550,7 +579,12 @@ function compileAlways<Ctx, E extends EventObject>(
550
579
  ): void {
551
580
  const list: Transition<Ctx, E>[] = [];
552
581
  compiled.always[index] = list;
553
- const spec = (compiled.configs[index] as StateConfig<Ctx, E>).always;
582
+ // Read as an array type: the transpiler shifts an index to Lua's base 1 only on an array.
583
+ const spec = (compiled.configs[index] as StateConfig<Ctx, E>).always as
584
+ | string
585
+ | AlwaysConfig<Ctx>
586
+ | readonly AlwaysConfig<Ctx>[]
587
+ | undefined;
554
588
  if (spec === undefined) {
555
589
  return;
556
590
  }
@@ -1413,6 +1447,6 @@ function define<Ctx, E extends EventObject>(
1413
1447
  // A key makes a later definition under it (a hot-reloaded module re-running) rebind live instances.
1414
1448
  export function defineMachine<Ctx, E extends EventObject>(
1415
1449
  key?: string,
1416
- ): <const C extends MachineConfig<Ctx, E>>(config: C) => DefinedMachine<Ctx, E, C> {
1450
+ ): <T>(config: RootState<Ctx, E, T>) => Machine<Ctx, E, TreePath<T>> {
1417
1451
  return ((config: MachineConfig<Ctx, E>) => define(key, config)) as never;
1418
1452
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/transpiler",
3
- "version": "0.44.0",
3
+ "version": "0.45.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.44.0",
38
+ "@defold-typescript/types": "0.45.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"