@defold-typescript/transpiler 0.44.1 → 0.46.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 +19 -1
- package/dist/hsm/debug.ts +15 -1
- package/dist/hsm/defold.ts +25 -2
- package/dist/hsm/index.ts +305 -122
- package/dist/index.js +15 -9
- package/package.json +2 -2
- package/src/companion-emit.ts +24 -6
- package/src/transpile.ts +5 -1
package/dist/hsm/async.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import type { EventObject, TaskStart } from "./index";
|
|
2
2
|
|
|
3
|
-
/**
|
|
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
|
-
/**
|
|
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,
|
package/dist/hsm/defold.ts
CHANGED
|
@@ -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
|
-
? {
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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,19 +72,41 @@ 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,
|
|
36
82
|
machine: MachineInstance<Ctx, E>,
|
|
37
83
|
) => void;
|
|
38
84
|
|
|
39
|
-
|
|
40
|
-
|
|
85
|
+
// A list with a first entry: Lua cannot tell [] from {}, so an empty list would compile to a
|
|
86
|
+
// transition with no to. It is an object type, not a tuple, because TypeScript reports a bad
|
|
87
|
+
// `to` in the object form on that `to` only when the spec union holds no array type.
|
|
88
|
+
interface RuleList<Rule> {
|
|
89
|
+
readonly 0: Rule;
|
|
90
|
+
readonly [index: number]: Rule;
|
|
91
|
+
readonly length: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
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
|
+
*/
|
|
99
|
+
export interface TransitionConfig<
|
|
100
|
+
Ctx,
|
|
101
|
+
E extends EventObject,
|
|
102
|
+
V extends E = E,
|
|
103
|
+
To extends string = string,
|
|
104
|
+
> {
|
|
41
105
|
/**
|
|
42
106
|
* The full path of the state to move to. A `to` naming the state the rule is on exits and
|
|
43
107
|
* re-enters it, restarting its timers and `task`; a `to` below it keeps it active.
|
|
44
108
|
*/
|
|
45
|
-
readonly to?:
|
|
109
|
+
readonly to?: To;
|
|
46
110
|
/**
|
|
47
111
|
* The rule is taken only when this returns `true`. It must have no side effects: it is also
|
|
48
112
|
* checked when the move does not happen, in guarded lists and on every `always` recheck.
|
|
@@ -65,15 +129,28 @@ export interface TransitionConfig<Ctx, E extends EventObject, V extends E = E> {
|
|
|
65
129
|
readonly reenter?: never;
|
|
66
130
|
}
|
|
67
131
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
+
*/
|
|
136
|
+
export type TransitionSpec<
|
|
137
|
+
Ctx,
|
|
138
|
+
E extends EventObject,
|
|
139
|
+
V extends E = E,
|
|
140
|
+
To extends string = string,
|
|
141
|
+
> = To | TransitionConfig<Ctx, E, V, To> | RuleList<TransitionConfig<Ctx, E, V, To>>;
|
|
73
142
|
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
153
|
+
readonly to: To;
|
|
77
154
|
/** Checked on every recheck, so it must have no side effects. */
|
|
78
155
|
readonly when?: (ctx: Ctx) => boolean;
|
|
79
156
|
/** Renamed to `to`. */
|
|
@@ -82,13 +159,21 @@ export interface AlwaysConfig<Ctx> {
|
|
|
82
159
|
readonly guard?: never;
|
|
83
160
|
}
|
|
84
161
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
*/
|
|
166
|
+
export type AlwaysSpec<Ctx, To extends string = string> =
|
|
167
|
+
| To
|
|
168
|
+
| AlwaysConfig<Ctx, To>
|
|
169
|
+
| RuleList<AlwaysConfig<Ctx, To>>;
|
|
89
170
|
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
*/
|
|
175
|
+
export type OnConfig<Ctx, E extends EventObject, To extends string = string> = {
|
|
176
|
+
readonly [K in E["type"]]?: TransitionSpec<Ctx, E, Extract<E, { type: K }>, To>;
|
|
92
177
|
};
|
|
93
178
|
|
|
94
179
|
/**
|
|
@@ -101,11 +186,16 @@ export type StateHook<Ctx, E extends EventObject> = (
|
|
|
101
186
|
machine: MachineInstance<Ctx, E>,
|
|
102
187
|
) => void;
|
|
103
188
|
|
|
104
|
-
|
|
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
|
+
*/
|
|
194
|
+
export type UpdateHook<Ctx, E extends EventObject, To extends string = string> = (
|
|
105
195
|
ctx: Ctx,
|
|
106
196
|
dt: number,
|
|
107
197
|
machine: MachineInstance<Ctx, E>,
|
|
108
|
-
) =>
|
|
198
|
+
) => To | undefined;
|
|
109
199
|
|
|
110
200
|
/**
|
|
111
201
|
* Starts work that lives as long as the state. `finish` sends one event and is ignored after the
|
|
@@ -119,122 +209,203 @@ export type TaskStart<Ctx, E extends EventObject> = (
|
|
|
119
209
|
// biome-ignore lint/suspicious/noConfusingVoidType: `void` keeps an expression-body task returning a void call (`=> go.animate(...)`) valid.
|
|
120
210
|
) => (() => void) | void;
|
|
121
211
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
/** @noSelf */
|
|
212
|
+
/**
|
|
213
|
+
* A machine definition, returned by the second `defineMachine` call. Its `start` creates the
|
|
214
|
+
* instances that run.
|
|
215
|
+
* @noSelf
|
|
216
|
+
*/
|
|
146
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
|
+
*/
|
|
147
223
|
readonly start: (ctx: Ctx) => MachineInstance<Ctx, E, P>;
|
|
148
224
|
}
|
|
149
225
|
|
|
150
226
|
type PathDepth = [never, 0, 1, 2, 3];
|
|
151
227
|
|
|
152
|
-
type
|
|
153
|
-
|
|
154
|
-
|
|
228
|
+
type NextCount = [never, 2, 3, 4, 5, 6];
|
|
229
|
+
|
|
230
|
+
// The paths a started machine can be in. A node with no children is `unknown`.
|
|
231
|
+
type TreePathsBelow<T, D extends number> = {
|
|
232
|
+
[K in keyof T & string]:
|
|
233
|
+
| K
|
|
234
|
+
| `${K}/${D extends 1
|
|
235
|
+
? keyof T[K] extends never
|
|
236
|
+
? never
|
|
237
|
+
: string
|
|
238
|
+
: TreePathsBelow<T[K], PathDepth[D]>}`;
|
|
239
|
+
}[keyof T & string];
|
|
240
|
+
|
|
241
|
+
type TreePath<T> = `/${TreePathsBelow<T, 4>}`;
|
|
242
|
+
|
|
243
|
+
// The paths a config slot accepts. TypeScript checks a config once before it types the
|
|
244
|
+
// callbacks, and a level whose states hold only callbacks has no inferred children in that
|
|
245
|
+
// check, so a node with no inferred children accepts any path below it.
|
|
246
|
+
type SlotPathsBelow<T, D extends number> = unknown extends T
|
|
247
|
+
? string
|
|
248
|
+
: {
|
|
249
|
+
[K in keyof T & string]:
|
|
155
250
|
| K
|
|
156
|
-
| `${K}/${D extends 1
|
|
157
|
-
|
|
158
|
-
? string
|
|
159
|
-
: never
|
|
160
|
-
: PathsBelow<Children[K], PathDepth[D]>}`;
|
|
161
|
-
}[keyof Children & string]
|
|
162
|
-
: never;
|
|
251
|
+
| `${K}/${D extends 1 ? string : SlotPathsBelow<T[K], PathDepth[D]>}`;
|
|
252
|
+
}[keyof T & string];
|
|
163
253
|
|
|
164
|
-
|
|
254
|
+
type SlotPath<T> = `/${SlotPathsBelow<T, 4>}`;
|
|
165
255
|
|
|
166
|
-
type
|
|
256
|
+
type TreeOf<C> = C extends { readonly states: infer Children }
|
|
257
|
+
? { readonly [K in keyof Children]: TreeOf<Children[K]> }
|
|
258
|
+
: unknown;
|
|
167
259
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
*/
|
|
264
|
+
export type StatePath<C> = TreePath<TreeOf<C>>;
|
|
171
265
|
|
|
172
|
-
// The
|
|
173
|
-
//
|
|
174
|
-
type
|
|
175
|
-
?
|
|
266
|
+
// The levels of child states below a node. It counts tree levels, so above a parallel state
|
|
267
|
+
// it is an upper bound on the counts restoreDepth takes.
|
|
268
|
+
type TreeDepth<T, D extends number> = unknown extends T
|
|
269
|
+
? number
|
|
270
|
+
: [D] extends [never]
|
|
176
271
|
? number
|
|
177
|
-
:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
272
|
+
:
|
|
273
|
+
| 1
|
|
274
|
+
| ({
|
|
275
|
+
[K in keyof T]: unknown extends T[K] ? never : TreeDepth<T[K], PathDepth[D]>;
|
|
276
|
+
}[keyof T] extends infer N
|
|
277
|
+
? N extends number
|
|
278
|
+
? number extends N
|
|
279
|
+
? number
|
|
280
|
+
: NextCount[N]
|
|
281
|
+
: never
|
|
282
|
+
: never);
|
|
283
|
+
|
|
284
|
+
type AnyChild<Ctx, E extends EventObject, T, To extends string> = unknown extends T
|
|
285
|
+
? { readonly [name: string]: StateNode<Ctx, E, unknown, string, To> }
|
|
286
|
+
: unknown;
|
|
287
|
+
|
|
288
|
+
// `states` is the only slot that infers the tree; every other slot that mentions it is wrapped
|
|
289
|
+
// in NoInfer. `on`, `after` and `update` are wrapped whole so inference never reads them:
|
|
290
|
+
// without exactOptionalPropertyTypes, reading `on` asks for the paths while the tree is still
|
|
291
|
+
// being inferred, and a valid config fails as circular. `always` is wrapped only through `To`:
|
|
292
|
+
// NoInfer over a union that holds object types hides its string members from completion.
|
|
293
|
+
// No callback parameter mentions the tree: one that did would make TypeScript settle the tree
|
|
294
|
+
// at the first such callback, before the states beside it are read.
|
|
295
|
+
/** @noSelf */
|
|
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 `/`. */
|
|
298
|
+
readonly states?: {
|
|
299
|
+
readonly [K in keyof T]: StateNode<Ctx, E, T[K], `${Self}/${K & string}`, To>;
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
332
|
+
readonly update?: NoInfer<UpdateHook<Ctx, E, To>>;
|
|
333
|
+
/** Started on each entry; see `TaskStart`. */
|
|
334
|
+
readonly task?: TaskStart<Ctx, E>;
|
|
335
|
+
/** Renamed to `task`. */
|
|
336
|
+
readonly invoke?: never;
|
|
194
337
|
}
|
|
195
338
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
};
|
|
339
|
+
/** @noSelf */
|
|
340
|
+
interface CompoundState<Ctx, E extends EventObject, T, Self extends string, To extends string>
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
358
|
+
readonly restoreDepth?: NoInfer<TreeDepth<T, 4> | "all">;
|
|
359
|
+
}
|
|
229
360
|
|
|
230
|
-
|
|
231
|
-
|
|
361
|
+
/** @noSelf */
|
|
362
|
+
interface ParallelState<Ctx, E extends EventObject, T, Self extends string, To extends string>
|
|
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
|
+
*/
|
|
368
|
+
readonly type: "parallel";
|
|
369
|
+
readonly initial?: never;
|
|
370
|
+
readonly restoreDepth?: never;
|
|
232
371
|
}
|
|
233
372
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
373
|
+
type StateNode<Ctx, E extends EventObject, T, Self extends string, To extends string> =
|
|
374
|
+
| CompoundState<Ctx, E, T, Self, To>
|
|
375
|
+
| ParallelState<Ctx, E, T, Self, To>;
|
|
376
|
+
|
|
377
|
+
type RootState<Ctx, E extends EventObject, T> = CompoundState<
|
|
378
|
+
Ctx,
|
|
379
|
+
E,
|
|
380
|
+
T,
|
|
381
|
+
"",
|
|
382
|
+
NoInfer<SlotPath<T>>
|
|
383
|
+
> & {
|
|
384
|
+
readonly initial: string;
|
|
385
|
+
readonly states: object;
|
|
386
|
+
};
|
|
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
|
+
*/
|
|
393
|
+
export type StateConfig<Ctx, E extends EventObject> = StateNode<Ctx, E, unknown, string, string>;
|
|
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
|
+
*/
|
|
399
|
+
export type MachineConfig<Ctx, E extends EventObject> = CompoundState<
|
|
400
|
+
Ctx,
|
|
401
|
+
E,
|
|
402
|
+
unknown,
|
|
403
|
+
string,
|
|
404
|
+
string
|
|
405
|
+
> & {
|
|
406
|
+
readonly initial: string;
|
|
407
|
+
readonly states: { readonly [name: string]: StateConfig<Ctx, E> };
|
|
408
|
+
};
|
|
238
409
|
|
|
239
410
|
type Guard<Ctx, E extends EventObject> = (ctx: Ctx, event: E) => boolean;
|
|
240
411
|
|
|
@@ -550,7 +721,12 @@ function compileAlways<Ctx, E extends EventObject>(
|
|
|
550
721
|
): void {
|
|
551
722
|
const list: Transition<Ctx, E>[] = [];
|
|
552
723
|
compiled.always[index] = list;
|
|
553
|
-
|
|
724
|
+
// Read as an array type: the transpiler shifts an index to Lua's base 1 only on an array.
|
|
725
|
+
const spec = (compiled.configs[index] as StateConfig<Ctx, E>).always as
|
|
726
|
+
| string
|
|
727
|
+
| AlwaysConfig<Ctx>
|
|
728
|
+
| readonly AlwaysConfig<Ctx>[]
|
|
729
|
+
| undefined;
|
|
554
730
|
if (spec === undefined) {
|
|
555
731
|
return;
|
|
556
732
|
}
|
|
@@ -1411,8 +1587,15 @@ function define<Ctx, E extends EventObject>(
|
|
|
1411
1587
|
|
|
1412
1588
|
// Ctx and E are given explicitly and the config is inferred, so the config takes a second call.
|
|
1413
1589
|
// A key makes a later definition under it (a hot-reloaded module re-running) rebind live instances.
|
|
1590
|
+
/**
|
|
1591
|
+
* Defines a machine in two calls: `defineMachine<Ctx, E>()` fixes the context and event types and
|
|
1592
|
+
* takes an optional hot-reload key, and the second call takes the config. The config is checked
|
|
1593
|
+
* when that call runs, and a mistake throws, naming the state path. With a key, a later
|
|
1594
|
+
* definition under it puts its states into the machine already built, so every running instance
|
|
1595
|
+
* sees the edit; without one, each call builds a separate machine.
|
|
1596
|
+
*/
|
|
1414
1597
|
export function defineMachine<Ctx, E extends EventObject>(
|
|
1415
1598
|
key?: string,
|
|
1416
|
-
): <
|
|
1599
|
+
): <T>(config: RootState<Ctx, E, T>) => Machine<Ctx, E, TreePath<T>> {
|
|
1417
1600
|
return ((config: MachineConfig<Ctx, E>) => define(key, config)) as never;
|
|
1418
1601
|
}
|
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
|
-
|
|
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 =
|
|
3190
|
-
if (resolved ===
|
|
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,7 +3660,7 @@ 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,
|
|
@@ -3672,7 +3677,8 @@ function transpileVirtual(files, companionEmit) {
|
|
|
3672
3677
|
{ plugin: hsmLoweringPlugin },
|
|
3673
3678
|
{ plugin: typeApplicationErasurePlugin },
|
|
3674
3679
|
{ plugin: companionEmit.plugin }
|
|
3675
|
-
]
|
|
3680
|
+
],
|
|
3681
|
+
...overrides
|
|
3676
3682
|
});
|
|
3677
3683
|
}
|
|
3678
3684
|
function transpileProject(input) {
|
|
@@ -3697,7 +3703,7 @@ function compileHsmModules() {
|
|
|
3697
3703
|
nameByKey.set(key, name);
|
|
3698
3704
|
files[key] = readFileSync(path3.join(sourceDir, `${name}.ts`), "utf8");
|
|
3699
3705
|
}
|
|
3700
|
-
const result = transpileVirtual(files, createCompanionEmitPlugin());
|
|
3706
|
+
const result = transpileVirtual(files, createCompanionEmitPlugin(), { removeComments: true });
|
|
3701
3707
|
const problems = result.diagnostics.map((d) => `${d.file?.fileName ?? "(project)"}: ${flattenDiagnosticMessage(d.messageText)}`);
|
|
3702
3708
|
const lua = {};
|
|
3703
3709
|
for (const file of result.transpiledFiles) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defold-typescript/transpiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.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.
|
|
38
|
+
"@defold-typescript/types": "0.46.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"
|
package/src/companion-emit.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
343
|
-
if (resolved ===
|
|
360
|
+
const resolved = companionRequirePath(argument.value, paths);
|
|
361
|
+
if (resolved === argument.value) {
|
|
344
362
|
return statement;
|
|
345
363
|
}
|
|
346
364
|
return createVariableDeclarationStatement(
|
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 },
|
|
@@ -354,6 +355,7 @@ function transpileVirtual(
|
|
|
354
355
|
{ plugin: typeApplicationErasurePlugin },
|
|
355
356
|
{ plugin: companionEmit.plugin },
|
|
356
357
|
],
|
|
358
|
+
...overrides,
|
|
357
359
|
},
|
|
358
360
|
);
|
|
359
361
|
}
|
|
@@ -398,7 +400,9 @@ export function compileHsmModules(): Readonly<Record<string, string>> {
|
|
|
398
400
|
nameByKey.set(key, name);
|
|
399
401
|
files[key] = readFileSync(path.join(sourceDir, `${name}.ts`), "utf8");
|
|
400
402
|
}
|
|
401
|
-
|
|
403
|
+
// The source's doc comments are for the editor, which reads the declarations; the Lua a
|
|
404
|
+
// project gets carries none of them.
|
|
405
|
+
const result = transpileVirtual(files, createCompanionEmitPlugin(), { removeComments: true });
|
|
402
406
|
|
|
403
407
|
const problems = result.diagnostics.map(
|
|
404
408
|
(d) => `${d.file?.fileName ?? "(project)"}: ${flattenDiagnosticMessage(d.messageText)}`,
|