@defold-typescript/types 0.36.0 → 0.38.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.
@@ -2,6 +2,44 @@
2
2
  import type { Hash, Url } from "./core-types";
3
3
 
4
4
  declare global {
5
+ /**
6
+ * A `Url` that also names the messages its receiver declares. `__receives` is
7
+ * type-only and never exists at runtime; the value is an ordinary `Url`.
8
+ */
9
+ type ReceiverUrl<M> = Url & { readonly __receives?: M };
10
+
11
+ /**
12
+ * The script-local messages a script declares by handling them: each
13
+ * annotated `onMessage` handler key outside the built-in and `CustomMessages`
14
+ * catalogs, mapped to its payload. Read it off the script module's default
15
+ * export and pass it to `msg.url<M>` to get a receiver-typed address.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * type WaveMessages = ScriptMessages<typeof import("./wave").default>;
20
+ * ```
21
+ */
22
+ type ScriptMessages<T> = T extends { on_message?: infer D }
23
+ ? NonNullable<D> extends { readonly __messages?: infer L }
24
+ ? L extends object
25
+ ? L
26
+ : Record<never, never>
27
+ : Record<never, never>
28
+ : Record<never, never>;
29
+
30
+ /**
31
+ * The messages a `msg.post` receiver declares: the `M` of a
32
+ * {@link ReceiverUrl}, or the handled messages of the script a scene address
33
+ * names in `SceneComponentAddresses`. Empty for every other receiver.
34
+ */
35
+ type ReceiverMessages<R> = R extends { readonly __receives?: infer M }
36
+ ? M extends object
37
+ ? M
38
+ : Record<never, never>
39
+ : R extends keyof SceneComponentAddresses
40
+ ? ScriptMessages<SceneComponentAddresses[R]>
41
+ : Record<never, never>;
42
+
5
43
  namespace msg {
6
44
  /**
7
45
  * Post a message to a receiving URL. The most common case is to send messages
@@ -25,11 +63,31 @@ declare global {
25
63
  * position: go.get_world_position(),
26
64
  * });
27
65
  * ```
66
+ *
67
+ * @remarks
68
+ * The `(string & {})` term keeps every declared message id as a completion
69
+ * while still accepting any other string, so no existing call is rejected.
70
+ *
71
+ * A receiver from `msg.url<M>` also offers and checks the ids in `M`, and a
72
+ * scene address whose generated value is a script module offers and checks
73
+ * that script's handled messages. A built-in id keeps its built-in payload,
74
+ * and any other id stays open.
75
+ *
76
+ * Both receiver routes live in this one signature on purpose: a separate,
77
+ * stricter overload ahead of it would let a mismatched payload fall through
78
+ * to the open one and compile anyway.
28
79
  */
29
- function post<K extends string>(
30
- receiver: SceneAddress | Url | Hash,
80
+ function post<
81
+ R extends SceneAddress | Hash | Url,
82
+ K extends (keyof ReceiverMessages<R> & string) | MessageId | (string & {}),
83
+ >(
84
+ receiver: R,
31
85
  message_id: K,
32
- message?: MessagePayload<K>,
86
+ message?: K extends BuiltinMessageId
87
+ ? MessagePayload<K>
88
+ : K extends keyof ReceiverMessages<R>
89
+ ? ReceiverMessages<R>[K]
90
+ : MessagePayload<K>,
33
91
  ): void;
34
92
  function post(
35
93
  receiver: SceneAddress | Url | Hash,
@@ -60,6 +118,11 @@ declare global {
60
118
  * component (`msg.url("#main")`). The `socket:` prefix only crosses
61
119
  * into a collection-proxy-loaded world.
62
120
  *
121
+ * Pass the receiver's message map as `M` (for example
122
+ * `msg.url<ScriptMessages<typeof import("./wave").default>>("/logic#wave")`)
123
+ * to get a receiver-typed address: `msg.post` then completes and checks
124
+ * those ids. The value is still an ordinary `Url`.
125
+ *
63
126
  * @example
64
127
  * ```ts
65
128
  * // No-arg, then take the current game object's URL.
@@ -91,5 +154,12 @@ declare global {
91
154
  path: string | Hash,
92
155
  fragment: string | Hash,
93
156
  ): Url;
157
+ function url<M extends object>(): ReceiverUrl<M>;
158
+ function url<M extends object>(urlstring: SceneAddress | undefined): ReceiverUrl<M>;
159
+ function url<M extends object>(
160
+ socket: string | Hash | undefined,
161
+ path: string | Hash,
162
+ fragment: string | Hash,
163
+ ): ReceiverUrl<M>;
94
164
  }
95
165
  }
@@ -0,0 +1,306 @@
1
+ // Provenance helpers for the two return-side correction tables. Both read the
2
+ // vendored ref-doc as text rather than through `parseDefoldApiDoc`, because the
3
+ // evidence a return-side correction rests on lives in the slot's HTML prose and
4
+ // the parser discards it.
5
+
6
+ import type { ReturnFieldOptionalityCorrection, ReturnTypeCorrection } from "./emit-dts";
7
+ import type { ProvenanceSurface } from "./optional-correction-provenance";
8
+
9
+ const NAMED_ENTITIES: Record<string, string> = {
10
+ "&lt;": "<",
11
+ "&gt;": ">",
12
+ "&quot;": '"',
13
+ "&#39;": "'",
14
+ "&apos;": "'",
15
+ };
16
+
17
+ function decodeEntities(text: string): string {
18
+ let out = text;
19
+ for (const [entity, char] of Object.entries(NAMED_ENTITIES)) {
20
+ out = out.split(entity).join(char);
21
+ }
22
+ out = out.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)));
23
+ // `&amp;` last so an already-decoded `&` is never re-interpreted.
24
+ return out.split("&amp;").join("&");
25
+ }
26
+
27
+ /**
28
+ * Decode a ref-doc prose fragment to the plain text a correction's `evidence`
29
+ * string is matched against.
30
+ *
31
+ * Unlike `htmlToDocText`, a `<span class="icon-X">` marker survives as `[X]`.
32
+ * Upstream states some field-presence gates only through that marker —
33
+ * `user_agent` carries `icon-html5` and no availability sentence at all — so
34
+ * stripping it would leave those corrections pinned to prose that says nothing
35
+ * about presence, and an upstream fix removing the gate would not red them.
36
+ */
37
+ export function decodeSlotProse(html: string): string {
38
+ const withIcons = html.replace(
39
+ /<span class="icon-([a-z0-9-]+)"><\/span>/gi,
40
+ (_, platform: string) => `[${platform}]`,
41
+ );
42
+ return decodeEntities(withIcons.replace(/<[^>]+>/g, ""))
43
+ .replace(/[ \t]+/g, " ")
44
+ .replace(/ *\n */g, "\n")
45
+ .trim();
46
+ }
47
+
48
+ /**
49
+ * The decoded prose of one `<dd>` in a slot doc's `<dl>`, keyed by the `<dt>`
50
+ * field name. Returns `undefined` when the slot doc has no entry for the field,
51
+ * which is the signal that upstream renamed or dropped it.
52
+ */
53
+ export function fieldProse(slotDoc: string, field: string): string | undefined {
54
+ const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
55
+ const match = new RegExp(`<dt><code>${escaped}</code></dt>\\s*<dd>([\\s\\S]*?)</dd>`, "i").exec(
56
+ slotDoc,
57
+ );
58
+ return match?.[1] === undefined ? undefined : decodeSlotProse(match[1]);
59
+ }
60
+
61
+ /**
62
+ * The `<span class="type">` tokens upstream declares for one `<dd>`, split the
63
+ * way the table-field parser splits them. An absent field yields `undefined`;
64
+ * a field with no type span yields an empty array.
65
+ */
66
+ export function fieldTypeTokens(slotDoc: string, field: string): readonly string[] | undefined {
67
+ const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
68
+ const match = new RegExp(`<dt><code>${escaped}</code></dt>\\s*<dd>([\\s\\S]*?)</dd>`, "i").exec(
69
+ slotDoc,
70
+ );
71
+ if (match?.[1] === undefined) return undefined;
72
+ const span = /<span class="type">([^<]*)<\/span>/.exec(match[1]);
73
+ if (span?.[1] === undefined) return [];
74
+ return span[1]
75
+ .split("|")
76
+ .map((token) => token.trim())
77
+ .filter((token) => token.length > 0);
78
+ }
79
+
80
+ interface RawSlot {
81
+ readonly name?: string;
82
+ readonly doc?: string;
83
+ readonly types?: readonly string[];
84
+ }
85
+
86
+ function isRecord(value: unknown): value is Record<string, unknown> {
87
+ return typeof value === "object" && value !== null && !Array.isArray(value);
88
+ }
89
+
90
+ // Every `returnvalues` entry of every FUNCTION in the surface named `element`,
91
+ // unfiltered by slot: one target can declare the element more than once.
92
+ function returnSlots(doc: unknown, element: string): RawSlot[] {
93
+ if (!isRecord(doc) || !Array.isArray(doc.elements)) return [];
94
+ return doc.elements.flatMap((candidate) =>
95
+ isRecord(candidate) &&
96
+ candidate.type === "FUNCTION" &&
97
+ candidate.name === element &&
98
+ Array.isArray(candidate.returnvalues)
99
+ ? candidate.returnvalues.filter(isRecord)
100
+ : [],
101
+ );
102
+ }
103
+
104
+ // The first `returnvalues` entry of each declaration of `element`, one per
105
+ // declaration so a target that declares the element twice is folded, not sliced.
106
+ function firstReturnSlots(doc: unknown, element: string): RawSlot[] {
107
+ if (!isRecord(doc) || !Array.isArray(doc.elements)) return [];
108
+ return doc.elements.flatMap((candidate) => {
109
+ if (
110
+ !isRecord(candidate) ||
111
+ candidate.type !== "FUNCTION" ||
112
+ candidate.name !== element ||
113
+ !Array.isArray(candidate.returnvalues)
114
+ ) {
115
+ return [];
116
+ }
117
+ const first = candidate.returnvalues[0];
118
+ return isRecord(first) ? [first] : [];
119
+ });
120
+ }
121
+
122
+ function namespaceOf(element: string): string {
123
+ return element.slice(0, element.lastIndexOf("."));
124
+ }
125
+
126
+ // A sighted target lands in exactly one bucket, strongest signal first. A
127
+ // dropped field outranks a needed one because `applyFieldOptionalityCorrections`
128
+ // throws for that target, so regen fails there whatever the other declarations
129
+ // say; a needed one outranks a resolved one so an overload that still shows the
130
+ // defect keeps the correction alive, matching `correctionProvenance`.
131
+ type FieldVerdict = "missing" | "needed" | "resolved";
132
+
133
+ const FIELD_VERDICT_RANK: Record<FieldVerdict, number> = {
134
+ missing: 2,
135
+ needed: 1,
136
+ resolved: 0,
137
+ };
138
+
139
+ export interface ReturnFieldProvenance {
140
+ readonly key: string;
141
+ readonly sightedIn: readonly string[];
142
+ readonly neededBy: readonly string[];
143
+ readonly resolvedIn: readonly string[];
144
+ readonly missingFieldIn: readonly string[];
145
+ readonly typeDrift: readonly string[];
146
+ }
147
+
148
+ // Upstream sets no structural optionality flag on a return *field* — the parser
149
+ // never populates `TableField.optional`, only the curation and correction passes
150
+ // do — so there is no `isMarkedOptional` analogue to read here. "Resolved" is
151
+ // therefore prose- and token-shaped: the evidence sentence is gone from the
152
+ // field's `<dd>`, or its type span now declares `nil` itself.
153
+ //
154
+ // `missingFieldIn` is deliberately not a resolution. A field upstream dropped
155
+ // from a slot the target still declares makes the correction stale, and
156
+ // `applyFieldOptionalityCorrections` throws rather than no-op for it, so that
157
+ // target needs a human decision instead of a silent deletion.
158
+ export function fieldCorrectionProvenance(
159
+ entries: readonly [string, ReturnFieldOptionalityCorrection][],
160
+ surfaces: readonly ProvenanceSurface[],
161
+ ): ReturnFieldProvenance[] {
162
+ return entries.map(([key, correction]) => {
163
+ const parts = key.split(":");
164
+ if (parts.length !== 4) {
165
+ throw new Error(`fieldCorrectionProvenance: malformed key ${key}`);
166
+ }
167
+ const [element, , slotName, field] = parts as [string, string, string, string];
168
+ const namespace = namespaceOf(element);
169
+
170
+ const verdicts = new Map<string, FieldVerdict>();
171
+ const drift = new Map<string, Set<string>>();
172
+ for (const surface of surfaces) {
173
+ if (surface.namespace !== namespace) continue;
174
+ for (const slot of returnSlots(surface.doc, element)) {
175
+ if (slot.name !== slotName) continue;
176
+ const slotDoc = slot.doc ?? "";
177
+ const prose = fieldProse(slotDoc, field);
178
+ const tokens = fieldTypeTokens(slotDoc, field);
179
+
180
+ let verdict: FieldVerdict;
181
+ if (prose === undefined) {
182
+ verdict = "missing";
183
+ } else if (!prose.includes(correction.evidence) || (tokens ?? []).includes("nil")) {
184
+ verdict = "resolved";
185
+ } else {
186
+ verdict = "needed";
187
+ if ((tokens ?? []).join("|") !== correction.upstream.join("|")) {
188
+ const messages = drift.get(surface.target) ?? new Set<string>();
189
+ drift.set(surface.target, messages);
190
+ messages.add(
191
+ `${surface.target}: pinned ${correction.upstream.join("|")}, found ${tokens?.join("|") ?? "no field"}`,
192
+ );
193
+ }
194
+ }
195
+
196
+ const seen = verdicts.get(surface.target);
197
+ if (seen === undefined || FIELD_VERDICT_RANK[verdict] > FIELD_VERDICT_RANK[seen]) {
198
+ verdicts.set(surface.target, verdict);
199
+ }
200
+ }
201
+ }
202
+
203
+ const sightedIn = [...verdicts.keys()].sort();
204
+ const inBucket = (verdict: FieldVerdict) =>
205
+ sightedIn.filter((target) => verdicts.get(target) === verdict);
206
+ const neededBy = inBucket("needed");
207
+ return {
208
+ key,
209
+ sightedIn,
210
+ neededBy,
211
+ resolvedIn: inBucket("resolved"),
212
+ missingFieldIn: inBucket("missing"),
213
+ typeDrift: neededBy.flatMap((target) => [...(drift.get(target) ?? [])]).sort(),
214
+ };
215
+ });
216
+ }
217
+
218
+ export interface ReturnTypeProvenance {
219
+ readonly key: string;
220
+ readonly sightedIn: readonly string[];
221
+ readonly neededBy: readonly string[];
222
+ readonly resolvedIn: readonly string[];
223
+ readonly driftedIn: readonly string[];
224
+ readonly typeDrift: readonly string[];
225
+ }
226
+
227
+ // Same ranking argument as `FIELD_VERDICT_RANK`: `emitReturn` applies the
228
+ // correction to every declaration of the FQN in a target, so one drifted
229
+ // declaration makes that target's emit wrong whatever its siblings say.
230
+ type ReturnVerdict = "drift" | "needed" | "resolved";
231
+
232
+ const RETURN_VERDICT_RANK: Record<ReturnVerdict, number> = {
233
+ drift: 2,
234
+ needed: 1,
235
+ resolved: 0,
236
+ };
237
+
238
+ function baseTokens(tokens: readonly string[]): string {
239
+ return tokens.filter((token) => token !== "nil").join("|");
240
+ }
241
+
242
+ // The return-type class has the same two-sided evidence: the declared tokens
243
+ // still say what the correction contradicts, and the slot prose still states the
244
+ // contradiction. A target that fixes either half no longer needs the entry.
245
+ //
246
+ // Unlike the field lane, a changed base type is drift here rather than a
247
+ // resolution. A field correction only flips `optional` and leaves the type
248
+ // parser-authoritative, so a retyped field still emits what upstream declares; a
249
+ // return correction replaces the whole emitted type, so a base upstream no longer
250
+ // declares means the correction now overwrites a type nobody verified — a
251
+ // decision for a human, not a deletion and not a silent bank.
252
+ export function returnCorrectionProvenance(
253
+ entries: readonly [string, ReturnTypeCorrection][],
254
+ surfaces: readonly ProvenanceSurface[],
255
+ ): ReturnTypeProvenance[] {
256
+ return entries.map(([key, correction]) => {
257
+ const namespace = namespaceOf(key);
258
+ const pinned = baseTokens(correction.upstream);
259
+
260
+ const verdicts = new Map<string, ReturnVerdict>();
261
+ const drift = new Map<string, Set<string>>();
262
+ for (const surface of surfaces) {
263
+ if (surface.namespace !== namespace) continue;
264
+ // The correction rewrites the first return only, so only that slot is
265
+ // evidence; a declaration with no returns at all sights nothing.
266
+ for (const slot of firstReturnSlots(surface.doc, key)) {
267
+ const tokens = slot.types ?? [];
268
+
269
+ let verdict: ReturnVerdict;
270
+ if (baseTokens(tokens) !== pinned) {
271
+ verdict = "drift";
272
+ const messages = drift.get(surface.target) ?? new Set<string>();
273
+ drift.set(surface.target, messages);
274
+ messages.add(
275
+ `${surface.target}: pinned ${correction.upstream.join("|")}, found ${tokens.join("|")}`,
276
+ );
277
+ } else if (
278
+ tokens.includes("nil") ||
279
+ !decodeSlotProse(slot.doc ?? "").includes(correction.reason)
280
+ ) {
281
+ verdict = "resolved";
282
+ } else {
283
+ verdict = "needed";
284
+ }
285
+
286
+ const seen = verdicts.get(surface.target);
287
+ if (seen === undefined || RETURN_VERDICT_RANK[verdict] > RETURN_VERDICT_RANK[seen]) {
288
+ verdicts.set(surface.target, verdict);
289
+ }
290
+ }
291
+ }
292
+
293
+ const sightedIn = [...verdicts.keys()].sort();
294
+ const inBucket = (verdict: ReturnVerdict) =>
295
+ sightedIn.filter((target) => verdicts.get(target) === verdict);
296
+ const driftedIn = inBucket("drift");
297
+ return {
298
+ key,
299
+ sightedIn,
300
+ neededBy: inBucket("needed"),
301
+ resolvedIn: inBucket("resolved"),
302
+ driftedIn,
303
+ typeDrift: driftedIn.flatMap((target) => [...(drift.get(target) ?? [])]).sort(),
304
+ };
305
+ });
306
+ }