@defold-typescript/types 0.7.0 → 0.8.1

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.
@@ -26,7 +26,7 @@
26
26
  "collectionfactory": {
27
27
  "droppedElements": 0,
28
28
  "unknownTokens": [],
29
- "recordTables": 1,
29
+ "recordTables": 0,
30
30
  "multiReturn": 0,
31
31
  "droppedMembers": 0,
32
32
  "optionalAsRequired": 0
@@ -34,7 +34,7 @@
34
34
  "collectionproxy": {
35
35
  "droppedElements": 0,
36
36
  "unknownTokens": [],
37
- "recordTables": 1,
37
+ "recordTables": 0,
38
38
  "multiReturn": 0,
39
39
  "droppedMembers": 0,
40
40
  "optionalAsRequired": 0
@@ -42,7 +42,7 @@
42
42
  "crash": {
43
43
  "droppedElements": 0,
44
44
  "unknownTokens": [],
45
- "recordTables": 2,
45
+ "recordTables": 0,
46
46
  "multiReturn": 0,
47
47
  "droppedMembers": 0,
48
48
  "optionalAsRequired": 0
@@ -50,7 +50,7 @@
50
50
  "factory": {
51
51
  "droppedElements": 0,
52
52
  "unknownTokens": [],
53
- "recordTables": 1,
53
+ "recordTables": 0,
54
54
  "multiReturn": 0,
55
55
  "droppedMembers": 0,
56
56
  "optionalAsRequired": 0
@@ -58,9 +58,9 @@
58
58
  "go": {
59
59
  "droppedElements": 0,
60
60
  "unknownTokens": [],
61
- "recordTables": 2,
61
+ "recordTables": 0,
62
62
  "multiReturn": 0,
63
- "droppedMembers": 3,
63
+ "droppedMembers": 0,
64
64
  "optionalAsRequired": 0
65
65
  },
66
66
  "graphics": {
@@ -74,7 +74,15 @@
74
74
  "gui": {
75
75
  "droppedElements": 0,
76
76
  "unknownTokens": [],
77
- "recordTables": 2,
77
+ "recordTables": 0,
78
+ "multiReturn": 0,
79
+ "droppedMembers": 0,
80
+ "optionalAsRequired": 0
81
+ },
82
+ "html5": {
83
+ "droppedElements": 0,
84
+ "unknownTokens": [],
85
+ "recordTables": 0,
78
86
  "multiReturn": 0,
79
87
  "droppedMembers": 0,
80
88
  "optionalAsRequired": 0
@@ -82,7 +90,7 @@
82
90
  "http": {
83
91
  "droppedElements": 0,
84
92
  "unknownTokens": [],
85
- "recordTables": 1,
93
+ "recordTables": 0,
86
94
  "multiReturn": 0,
87
95
  "droppedMembers": 0,
88
96
  "optionalAsRequired": 0
@@ -148,7 +156,7 @@
148
156
  "unknownTokens": [],
149
157
  "recordTables": 0,
150
158
  "multiReturn": 0,
151
- "droppedMembers": 1,
159
+ "droppedMembers": 0,
152
160
  "optionalAsRequired": 0
153
161
  },
154
162
  "particlefx": {
@@ -162,7 +170,7 @@
162
170
  "physics": {
163
171
  "droppedElements": 0,
164
172
  "unknownTokens": [],
165
- "recordTables": 3,
173
+ "recordTables": 0,
166
174
  "multiReturn": 0,
167
175
  "droppedMembers": 0,
168
176
  "optionalAsRequired": 0
@@ -170,7 +178,7 @@
170
178
  "profiler": {
171
179
  "droppedElements": 0,
172
180
  "unknownTokens": [],
173
- "recordTables": 1,
181
+ "recordTables": 0,
174
182
  "multiReturn": 0,
175
183
  "droppedMembers": 0,
176
184
  "optionalAsRequired": 0
@@ -186,7 +194,7 @@
186
194
  "render": {
187
195
  "droppedElements": 0,
188
196
  "unknownTokens": [],
189
- "recordTables": 4,
197
+ "recordTables": 0,
190
198
  "multiReturn": 0,
191
199
  "droppedMembers": 0,
192
200
  "optionalAsRequired": 0
@@ -194,7 +202,7 @@
194
202
  "resource": {
195
203
  "droppedElements": 0,
196
204
  "unknownTokens": [],
197
- "recordTables": 3,
205
+ "recordTables": 0,
198
206
  "multiReturn": 0,
199
207
  "droppedMembers": 0,
200
208
  "optionalAsRequired": 0
@@ -202,7 +210,7 @@
202
210
  "socket": {
203
211
  "droppedElements": 0,
204
212
  "unknownTokens": [],
205
- "recordTables": 4,
213
+ "recordTables": 0,
206
214
  "multiReturn": 0,
207
215
  "droppedMembers": 0,
208
216
  "optionalAsRequired": 0
@@ -247,6 +255,14 @@
247
255
  "droppedMembers": 0,
248
256
  "optionalAsRequired": 0
249
257
  },
258
+ "types": {
259
+ "droppedElements": 0,
260
+ "unknownTokens": [],
261
+ "recordTables": 0,
262
+ "multiReturn": 0,
263
+ "droppedMembers": 0,
264
+ "optionalAsRequired": 0
265
+ },
250
266
  "vmath": {
251
267
  "droppedElements": 0,
252
268
  "unknownTokens": [],
@@ -258,7 +274,7 @@
258
274
  "webview": {
259
275
  "droppedElements": 0,
260
276
  "unknownTokens": [],
261
- "recordTables": 1,
277
+ "recordTables": 0,
262
278
  "multiReturn": 0,
263
279
  "droppedMembers": 0,
264
280
  "optionalAsRequired": 0
package/scripts/regen.ts CHANGED
@@ -9,6 +9,8 @@ import { wrapAsAmbientGlobal } from "../src/publish-dts";
9
9
  import {
10
10
  type DocSourceProvenance,
11
11
  type DownloadRefDoc,
12
+ type FetchChannelInfo,
13
+ type RefDocChannel,
12
14
  refDocCacheDir,
13
15
  resolveRefDoc,
14
16
  } from "./doc-source";
@@ -89,6 +91,8 @@ export interface ResolveTargetOptions {
89
91
  readonly readZip?: typeof readZip;
90
92
  readonly syncManifest?: readonly SyncManifestEntry[];
91
93
  readonly packageRoot?: string;
94
+ readonly channel?: RefDocChannel;
95
+ readonly fetchChannelInfo?: FetchChannelInfo;
92
96
  }
93
97
 
94
98
  // Source-aware module resolution: a `null`-source target reads committed
@@ -106,8 +110,10 @@ export async function resolveTargetModules(
106
110
  const { zip, provenance } = await resolveRefDoc({
107
111
  version: source.version,
108
112
  cacheDir: opts.cacheDir ?? refDocCacheDir(),
113
+ ...(opts.channel ? { channel: opts.channel } : {}),
109
114
  ...(opts.download ? { download: opts.download } : {}),
110
115
  ...(opts.readZip ? { readZip: opts.readZip } : {}),
116
+ ...(opts.fetchChannelInfo ? { fetchChannelInfo: opts.fetchChannelInfo } : {}),
111
117
  });
112
118
  const syncManifest = opts.syncManifest ?? SYNC_MANIFEST;
113
119
  return target.modules.map((module) => {
@@ -217,6 +223,12 @@ export const RESTRICTED_NAMESPACES: Readonly<Record<string, string>> = {
217
223
  render: "render_script",
218
224
  };
219
225
 
226
+ // The Lua standard library rides every per-kind subpath the same as the full
227
+ // entrypoint. Triple-slash directives must precede the first statement, so they
228
+ // lead the generated kind index.
229
+ const LUA_STDLIB_REFERENCES =
230
+ '/// <reference types="lua-types/5.1" />\n/// <reference types="lua-types/special/jit-only" />\n';
231
+
220
232
  const UNIVERSAL_EXTRA_IMPORTS: readonly string[] = [
221
233
  "../builtin-messages",
222
234
  "../../src/engine-globals",
@@ -247,7 +259,7 @@ export function generateKindIndex(kind: string): string {
247
259
  ...new Set([...universalNamespaces.sort(), ...[...UNIVERSAL_EXTRA_IMPORTS].sort()]),
248
260
  ].map((path) => `import "${path}";`);
249
261
  if (entry.restricted) lines.push(`import "../${entry.restricted}";`);
250
- return `${lines.join("\n")}\n\nexport { ${entry.factory} } from "../../src/lifecycle";\nexport type { ScriptProperties, ScriptProperty } from "../../src/lifecycle";\n`;
262
+ return `${LUA_STDLIB_REFERENCES}${lines.join("\n")}\n\nexport { ${entry.factory} } from "../../src/lifecycle";\nexport type { ScriptProperties, ScriptProperty } from "../../src/lifecycle";\n`;
251
263
  }
252
264
 
253
265
  export function generateVersionIndex(
@@ -30,6 +30,7 @@ export const SYNC_MANIFEST: readonly SyncManifestEntry[] = [
30
30
  entry("go", "doc/gameobject_script.cpp_doc.json"),
31
31
  entry("graphics", "doc/src-script_graphics.cpp_doc.json"),
32
32
  entry("gui", "doc/gui_script.cpp_doc.json"),
33
+ entry("html5", "doc/src-script_html5_js.cpp_doc.json"),
33
34
  entry("http", "doc/scripts-script_http.cpp_doc.json"),
34
35
  entry("image", "doc/scripts-script_image.cpp_doc.json"),
35
36
  entry("json", "doc/src-script_json.cpp_doc.json"),
@@ -50,6 +51,7 @@ export const SYNC_MANIFEST: readonly SyncManifestEntry[] = [
50
51
  entry("sys", "doc/src-script_sys.cpp_doc.json"),
51
52
  entry("tilemap", "doc/scripts-script_tilemap.cpp_doc.json"),
52
53
  entry("timer", "doc/src-script_timer.cpp_doc.json"),
54
+ entry("types", "doc/src-script_types.cpp_doc.json"),
53
55
  entry("vmath", "doc/src-script_vmath.cpp_doc.json"),
54
56
  entry("window", "doc/scripts-script_window.cpp_doc.json"),
55
57
  entry("zlib", "doc/src-script_zlib.cpp_doc.json"),
@@ -123,6 +125,9 @@ export function parseChecklistNamespaces(visionMarkdown: string): string[] {
123
125
  export interface ZipAccessor {
124
126
  has(entry: string): boolean;
125
127
  read(entry: string): string;
128
+ // Surfaces the same entry set `has` is built from, so the extension-archive
129
+ // resolver can hand the list to the pure `locateScriptApis` / `classifyExtension`.
130
+ entries(): string[];
126
131
  }
127
132
 
128
133
  export function readZip(path: string): ZipAccessor {
@@ -133,6 +138,7 @@ export function readZip(path: string): ZipAccessor {
133
138
  const entries = new Set(list.stdout.toString().split("\n").filter(Boolean));
134
139
  return {
135
140
  has: (name) => entries.has(name),
141
+ entries: () => [...entries],
136
142
  read: (name) => {
137
143
  const out = Bun.spawnSync(["unzip", "-p", path, name]);
138
144
  if (out.exitCode !== 0) {
package/src/emit-dts.ts CHANGED
@@ -82,6 +82,20 @@ export const TS_RESERVED_NAMES = new Set([
82
82
  "extends",
83
83
  ]);
84
84
 
85
+ // A parameter name that clears `TS_IDENTIFIER` but is a TS reserved word (e.g.
86
+ // `var` on the `types.is_*` guards) is illegal in parameter position (TS1390),
87
+ // unlike a member name which `TS_RESERVED_NAMES` recovers via an export alias.
88
+ // Parameter names in a `.d.ts` are non-binding, so a reserved word takes the
89
+ // idiomatic trailing-underscore escape (`var` -> `var_`, matching
90
+ // ts-defold-types so the parity audit stays a superset); a genuinely
91
+ // non-identifier token still falls back to the positional `arg<index>` form.
92
+ // The emitted signature and its `@param` tag share this name so the tag stays
93
+ // resolvable on hover.
94
+ function safeParamName(name: string, index: number): string {
95
+ if (!TS_IDENTIFIER.test(name)) return `arg${index}`;
96
+ return TS_RESERVED_NAMES.has(name) ? `${name}_` : name;
97
+ }
98
+
85
99
  // Element names whose `table` slot is a genuinely-arbitrary lua table by design.
86
100
  // Two kinds: the serialization/JSON passthrough functions (Defold-internal — the
87
101
  // engine round-trips an arbitrary lua value), and the platform/OS-sourced opaque
@@ -101,6 +115,55 @@ export const ARBITRARY_TABLE_SLOTS = new Set([
101
115
  "iac.set_listener",
102
116
  "push.get_scheduled",
103
117
  "push.get_all_scheduled",
118
+ // runtime-owned passthrough: the table is filled by user code at runtime, never
119
+ // by Defold's API contract. factory.create/collectionfactory.create `properties`
120
+ // are spawn-time overrides keyed by the spawned object's own script-property
121
+ // names; bare `on_message`'s receive `message` is the pre-hashed, user-routed
122
+ // payload (mitigated for authored code by isMessage/onMessage). No static shape
123
+ // exists, so `Record<string | number, unknown>` is faithful, not a loss.
124
+ "factory.create",
125
+ "collectionfactory.create",
126
+ "on_message",
127
+ // engine-formatted blob: the engine produces the value at runtime and no
128
+ // field list is documented, so the whole-slot `Record` is faithful, not a
129
+ // loss. crash.get_backtrace is "table containing the backtrace",
130
+ // crash.get_modules is "module table".
131
+ "crash.get_backtrace",
132
+ "crash.get_modules",
133
+ // platform-specific options: the doc is a dead backtick-prose cross-ref to a
134
+ // sibling whose function is absent from the fixture (webview.open is not
135
+ // documented), so no machine-readable shape exists.
136
+ "webview.open_raw",
137
+ // OS-resolver returns: the shape is set by the host resolver, not Defold's
138
+ // API contract; each fixture doc reads "a table with all information
139
+ // returned by the resolver". Mirrors the iac.set_listener / push.* platform-
140
+ // opaque precedent.
141
+ "socket.dns.tohostname",
142
+ "socket.dns.toip",
143
+ "socket.dns.getaddrinfo",
144
+ "socket.dns.getnameinfo",
145
+ // engine-built render-target tables: the "see the description" cross-refs
146
+ // are dead (the function's own description is empty) or carry a typed
147
+ // `transient: table` field whose sub-doc is prose-only, so the nested table
148
+ // is opaque to the field-list parser and the whole-slot `Record` is faithful.
149
+ "render.render_target",
150
+ "render.set_render_target",
151
+ ]);
152
+
153
+ // skipFunction FQNs (see `api-targets.json`'s `skipFunctions`) whose absence
154
+ // from the generated surface is *not* a fidelity loss — each is replaced by a
155
+ // hand-written, better-typed overload in the cited `*-overloads.d.ts`, so the
156
+ // final surface still ships the symbol, fully typed. The audit consults this
157
+ // set to keep the `droppedMembers` count honest; an uncovered skip (e.g. a new
158
+ // `skipFunctions` entry with no matching overload) still counts, so the gate
159
+ // stays a real signal. Mirrors the ARBITRARY_TABLE_SLOTS audit-honesty pattern.
160
+ export const OVERLOAD_COVERED_SKIPS = new Set([
161
+ // go-overloads.d.ts supplies the typed go.get / go.set / go.property shapes.
162
+ "go.get",
163
+ "go.set",
164
+ "go.property",
165
+ // msg-overloads.d.ts supplies the typed msg.post shape.
166
+ "msg.post",
104
167
  ]);
105
168
 
106
169
  // Element names whose `table` slot is a prose-only `a table mapping X to Y`
@@ -288,6 +351,175 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
288
351
  "tilemap.get_tiles:return:tiles",
289
352
  { kind: "mapping", key: "number", value: { key: "number", value: "number" } },
290
353
  ],
354
+ // resource.get_text_metrics's `metrics` return is an untyped name-only <ul>
355
+ // (width/height/max_ascent/max_descent, no <span class="type"> and no See
356
+ // cross-reference), so parseTableFields recovers nothing and the whole slot
357
+ // collapses to Record. The four fields are font metrics in pixels, i.e.
358
+ // `number`; as a return-table object curation they are required.
359
+ [
360
+ "resource.get_text_metrics:return:metrics",
361
+ {
362
+ kind: "object",
363
+ fields: [
364
+ { name: "width", types: ["number"] },
365
+ { name: "height", types: ["number"] },
366
+ { name: "max_ascent", types: ["number"] },
367
+ { name: "max_descent", types: ["number"] },
368
+ ],
369
+ },
370
+ ],
371
+ // profiler.view_recorded_frame's `frame_index` is a typed `<ul>` "specify one
372
+ // of the following": `distance` and `frame`, both numeric frame offsets. The
373
+ // <li> items carry no `<span class="type">`, so parseTableFields recovers
374
+ // nothing and the slot collapses to Record. As a param option bag both fields
375
+ // are optional (the caller supplies one), which the param-side `?` flag emits.
376
+ [
377
+ "profiler.view_recorded_frame:param:frame_index",
378
+ {
379
+ kind: "object",
380
+ fields: [
381
+ { name: "distance", types: ["number"] },
382
+ { name: "frame", types: ["number"] },
383
+ ],
384
+ },
385
+ ],
386
+ // http.request's `headers` is "optional table with custom headers", a
387
+ // string-keyed string map (no field list). Slot-path keying targets only
388
+ // `headers`; the sibling `options` table stays parser-recovered.
389
+ ["http.request:param:headers", { kind: "mapping", key: "string", value: "string" }],
390
+ // collectionproxy.get_resources returns "the resources, or an empty list", a
391
+ // homogeneous list of resource-path hashes (prose only, no field list).
392
+ ["collectionproxy.get_resources:return:resources", { kind: "array", element: "hash" }],
393
+ // render.predicate's `tags` is "table of tags ... can be of either hash or
394
+ // string type", a homogeneous array whose element is the string|hash union.
395
+ ["render.predicate:param:tags", { kind: "array", element: ["string", "hash"] }],
396
+ // render.clear's `buffers` maps the numeric graphics.BUFFER_* constants to
397
+ // their clear value: vector4 for the color buffer, number for depth/stencil.
398
+ // The mapping value is the `number | vector4` union; the mapping string branch
399
+ // splits the union token, so the value emits `number | Vector4`.
400
+ ["render.clear:param:buffers", { kind: "mapping", key: "number", value: "number | vector4" }],
401
+ // go.on_input and gui.on_input share the same well-known InputAction shape
402
+ // (the on_input function description lists the well-known fields: value,
403
+ // pressed, released, repeated, x/y/screen_x/screen_y/dx/dy/screen_dx/screen_dy,
404
+ // plus gamepad/touch/text multi-touch fields). The doc markup carries no
405
+ // field list of its own, so the shape is curated verbatim from the prose.
406
+ // Param-side optional fields: each input kind (mouse/key/text/gamepad) sets a
407
+ // different subset, so the call site always sees an `?` on every field.
408
+ // `touch` stays `table` (the nested multi-touch table has no machine-readable
409
+ // field list; mirrors the render.set_render_target.transient recursive case).
410
+ // The element is the bare `on_input` script hook (no namespace prefix), the
411
+ // same form the `on_message` arbitrary-table entry uses. The doc carries a
412
+ // well-formed touch input table, so `touch` is curated as a nested
413
+ // array-of-touch-records (the field has its own `<table>` listing id,
414
+ // pressed, released, tap_count, x, y, dx, dy, acc_x/y/z).
415
+ [
416
+ "on_input:param:action",
417
+ {
418
+ kind: "object",
419
+ fields: [
420
+ { name: "value", types: ["number"], optional: true },
421
+ { name: "pressed", types: ["boolean"], optional: true },
422
+ { name: "released", types: ["boolean"], optional: true },
423
+ { name: "repeated", types: ["boolean"], optional: true },
424
+ { name: "x", types: ["number"], optional: true },
425
+ { name: "y", types: ["number"], optional: true },
426
+ { name: "screen_x", types: ["number"], optional: true },
427
+ { name: "screen_y", types: ["number"], optional: true },
428
+ { name: "dx", types: ["number"], optional: true },
429
+ { name: "dy", types: ["number"], optional: true },
430
+ { name: "screen_dx", types: ["number"], optional: true },
431
+ { name: "screen_dy", types: ["number"], optional: true },
432
+ { name: "gamepad", types: ["number"], optional: true },
433
+ { name: "gamepad_axis", types: ["vector3"], optional: true },
434
+ {
435
+ name: "touch",
436
+ types: ["table"],
437
+ optional: true,
438
+ isList: true,
439
+ fields: [
440
+ { name: "id", types: ["number"] },
441
+ { name: "pressed", types: ["boolean"] },
442
+ { name: "released", types: ["boolean"] },
443
+ { name: "tap_count", types: ["number"] },
444
+ { name: "x", types: ["number"] },
445
+ { name: "y", types: ["number"] },
446
+ { name: "dx", types: ["number"] },
447
+ { name: "dy", types: ["number"] },
448
+ { name: "acc_x", types: ["number"] },
449
+ { name: "acc_y", types: ["number"] },
450
+ { name: "acc_z", types: ["number"] },
451
+ ],
452
+ },
453
+ { name: "text", types: ["string"], optional: true },
454
+ ],
455
+ },
456
+ ],
457
+ // physics.create_joint and physics.set_joint_properties both declare
458
+ // `properties` as a joint-type-specific table; the only universal field is
459
+ // `collide_connected` (parseTableFields already recovers it from the
460
+ // <span class="type">+<code> shape in the create_joint doc and the bare
461
+ // <code> shape in the set_joint_properties doc). Joint-type-specific fields
462
+ // stay deferred — they live in joint-type-specific docs the parser cannot
463
+ // see from a top-level param doc.
464
+ [
465
+ "physics.create_joint:param:properties",
466
+ {
467
+ kind: "object",
468
+ fields: [{ name: "collide_connected", types: ["boolean"], optional: true }],
469
+ },
470
+ ],
471
+ [
472
+ "physics.set_joint_properties:param:properties",
473
+ {
474
+ kind: "object",
475
+ fields: [{ name: "collide_connected", types: ["boolean"], optional: true }],
476
+ },
477
+ ],
478
+ // physics.raycast's `result` is "a list … see ray_cast_response for details";
479
+ // buildTableDocResolver resolves only FUNCTION elements so the cross-ref
480
+ // cannot fire (the message payload lives in messages_doc.json, not the
481
+ // FUNCTION slot pool). The shape is curated directly from the message
482
+ // catalog (the six ray_cast_response payload fields). The doc declares a
483
+ // `nil` branch ("If missed it returns <code>nil</code>"), so the existing
484
+ // nil-aware emit already projects the curated array onto the
485
+ // `array-object | nil` return type — no curation change needed there.
486
+ [
487
+ "physics.raycast:return:result",
488
+ {
489
+ kind: "array-object",
490
+ fields: [
491
+ { name: "fraction", types: ["number"] },
492
+ { name: "position", types: ["vector3"] },
493
+ { name: "normal", types: ["vector3"] },
494
+ { name: "id", types: ["hash"] },
495
+ { name: "group", types: ["hash"] },
496
+ { name: "request_id", types: ["number"] },
497
+ ],
498
+ },
499
+ ],
500
+ ]);
501
+
502
+ // resource.set_atlas's `table` param and resource.get_atlas's `data` return are
503
+ // flattened `<li><dl>` field lists whose `geometries` header is followed by the
504
+ // `table`-typed siblings `vertices`/`uvs`/`indices` — the grouping heuristic
505
+ // stops at the next `table` header, so `geometries` is left memberless and
506
+ // collapses to `Record`. The doc markup carries no signal that those number-list
507
+ // fields nest *under* `geometries` rather than beside it, so the nesting is
508
+ // hand-curated. Each geometry is the triangle data (the create-input form
509
+ // get_atlas mirrors via its cross-ref), and `vertices`/`uvs`/`indices` are
510
+ // brace-form number-lists, hence `numberList: true` (emitted `number[]`).
511
+ // Injected onto the already-parsed field list (every sibling stays
512
+ // parser-authoritative), so this is a nested-field curation, not a whole-slot
513
+ // object curation.
514
+ const ATLAS_GEOMETRY_MEMBERS: readonly TableField[] = [
515
+ { name: "vertices", types: ["table"], numberList: true },
516
+ { name: "uvs", types: ["table"], numberList: true },
517
+ { name: "indices", types: ["table"], numberList: true },
518
+ ];
519
+
520
+ export const NESTED_FIELD_CURATIONS: ReadonlyMap<string, readonly TableField[]> = new Map([
521
+ ["resource.set_atlas:param:table:geometries", ATLAS_GEOMETRY_MEMBERS],
522
+ ["resource.get_atlas:return:data:geometries", ATLAS_GEOMETRY_MEMBERS],
291
523
  ]);
292
524
 
293
525
  /**
@@ -415,6 +647,7 @@ export interface TableField {
415
647
  fields?: TableField[];
416
648
  isList?: boolean;
417
649
  numberList?: boolean;
650
+ optional?: boolean;
418
651
  }
419
652
 
420
653
  function parseUlFields(doc: string): TableField[] {
@@ -783,7 +1016,7 @@ function emitFunction(
783
1016
  // for a fully-undocumented function, leaving its emission byte-identical.
784
1017
  function functionDocLines(fn: ApiFunction, translations: TranslationStore): string[] {
785
1018
  const params = fn.parameters.map((p, index) => ({
786
- name: TS_IDENTIFIER.test(p.name) ? p.name : `arg${index}`,
1019
+ name: safeParamName(p.name, index),
787
1020
  doc: htmlToDocText(p.doc),
788
1021
  }));
789
1022
  const onlyReturn = fn.returnValues.length === 1 ? fn.returnValues[0] : undefined;
@@ -853,7 +1086,7 @@ function emitParameter(
853
1086
  resolver: TableDocResolver,
854
1087
  elementName: string,
855
1088
  ): string {
856
- const name = TS_IDENTIFIER.test(p.name) ? p.name : `arg${index}`;
1089
+ const name = safeParamName(p.name, index);
857
1090
  const concrete = p.types.filter((t) => t !== "nil");
858
1091
  const ts =
859
1092
  concrete.length > 0
@@ -937,7 +1170,13 @@ function mapSlotUnion(
937
1170
  if (mapping !== undefined) {
938
1171
  let value: string;
939
1172
  if (typeof mapping.value === "string") {
940
- value = mapType(mapping.value);
1173
+ // A mapping value may be a `T | U` union token (render.clear's
1174
+ // `number | vector4` clear value); split on `|` so each token maps
1175
+ // individually. A single-token value (no `|`) is unaffected.
1176
+ value = unionFromTokens(
1177
+ mapping.value.split("|").map((token) => token.trim()),
1178
+ mapType,
1179
+ );
941
1180
  } else if (Array.isArray(mapping.value)) {
942
1181
  value = inlineTableType(mapping.value, mapType, optionalFields);
943
1182
  } else {
@@ -951,8 +1190,9 @@ function mapSlotUnion(
951
1190
  const object = inlineTableType(curation.fields, mapType, optionalFields);
952
1191
  ts = curation.kind === "array-object" ? `${object}[]` : object;
953
1192
  } else {
954
- const fields = parseTableFields(doc, resolver);
955
- if (fields !== null) {
1193
+ const parsed = parseTableFields(doc, resolver);
1194
+ if (parsed !== null) {
1195
+ const fields = applyNestedFieldCurations(elementName, slotKind, slotName, parsed);
956
1196
  const object = inlineTableType(fields, mapType, optionalFields);
957
1197
  ts = isSlotLevelList(doc) ? `${object}[]` : object;
958
1198
  } else {
@@ -973,6 +1213,36 @@ function tableSlotKey(elementName: string, slotKind: "param" | "return", slotNam
973
1213
  return `${elementName}:${slotKind}:${slotName}`;
974
1214
  }
975
1215
 
1216
+ function nestedFieldKey(
1217
+ elementName: string,
1218
+ slotKind: "param" | "return",
1219
+ slotName: string,
1220
+ fieldName: string,
1221
+ ): string {
1222
+ return `${tableSlotKey(elementName, slotKind, slotName)}:${fieldName}`;
1223
+ }
1224
+
1225
+ // Inject the curated nested members of NESTED_FIELD_CURATIONS onto the matching
1226
+ // top-level field of an otherwise parser-recovered slot, leaving every sibling
1227
+ // parser-authoritative. A field the parser already gave nested fields is left
1228
+ // alone (parser won). Returns a new array; never mutates the parser's result.
1229
+ export function applyNestedFieldCurations(
1230
+ elementName: string,
1231
+ slotKind: "param" | "return" | undefined,
1232
+ slotName: string | undefined,
1233
+ fields: readonly TableField[],
1234
+ ): TableField[] {
1235
+ if (slotKind === undefined || slotName === undefined) return [...fields];
1236
+ return fields.map((field) => {
1237
+ if (field.fields !== undefined) return field;
1238
+ const curated = NESTED_FIELD_CURATIONS.get(
1239
+ nestedFieldKey(elementName, slotKind, slotName, field.name),
1240
+ );
1241
+ if (curated === undefined) return field;
1242
+ return { ...field, fields: [...curated], isList: true };
1243
+ });
1244
+ }
1245
+
976
1246
  function arrayTypeFromTokens(
977
1247
  element: string | readonly string[],
978
1248
  mapType: (t: string) => string,
@@ -27,8 +27,16 @@ declare global {
27
27
  * @example
28
28
  * ```ts
29
29
  * const position = go.get("#sprite", "position");
30
+ * // Name the target component to read its catalogued property type. The
31
+ * // empty call applies the type argument, then the inner call infers the key:
32
+ * const animation = go.get<sprite.properties>()("#sprite", "animation"); // Hash
30
33
  * ```
31
34
  */
35
+ function get<P>(): <K extends keyof P>(
36
+ url: string | Hash | Url,
37
+ property: K,
38
+ options?: GoPropertyOptions,
39
+ ) => P[K];
32
40
  function get<K extends keyof go.properties>(
33
41
  url: string | Hash | Url,
34
42
  property: K,
@@ -52,8 +60,17 @@ declare global {
52
60
  * @example
53
61
  * ```ts
54
62
  * go.set("#sprite", "tint", vmath.vector4(1, 0, 0, 1));
63
+ * // Name the target component to gate the value to its property type. The
64
+ * // empty call applies the type argument, then the inner call infers the key:
65
+ * go.set<sprite.properties>()("#sprite", "playback_rate", 2);
55
66
  * ```
56
67
  */
68
+ function set<P>(): <K extends keyof P>(
69
+ url: string | Hash | Url,
70
+ property: K,
71
+ value: P[K],
72
+ options?: GoPropertyOptions,
73
+ ) => void;
57
74
  function set<K extends keyof go.properties>(
58
75
  url: string | Hash | Url,
59
76
  property: K,
package/src/lifecycle.ts CHANGED
@@ -120,27 +120,52 @@ export type RenderScriptHooks<TSelf, TInitState = TSelf> = Omit<
120
120
  // callbacks see (`NoInfer`-wrapped inside the hook set), and `TInitState` what
121
121
  // `init` returns. `ScriptHooks` itself stays callback-only so the
122
122
  // `SCRIPT_HOOK_NAMES` drift pin remains valid.
123
- export type ScriptHooksWithProperties<TProps, TSelf, TInitState> = ScriptHooks<
124
- TSelf,
125
- TInitState
123
+ //
124
+ // `init` is overridden to receive `self: NoInfer<TProps>` — Defold applies the
125
+ // declared property values to `self` before `init` runs, so init-time setup can
126
+ // read them. `self` is *only* the property channel (`TProps`), not the merged
127
+ // `TSelf`: the return is still the sole `TInitState` inference site, and
128
+ // `NoInfer<TProps>` keeps `self` from competing with `properties` as a second
129
+ // `TProps` inference site (the non-circularity the no-`self` `init` originally
130
+ // bought).
131
+ export type ScriptHooksWithProperties<TProps, TSelf, TInitState> = Omit<
132
+ ScriptHooks<TSelf, TInitState>,
133
+ "init"
126
134
  > & {
135
+ init?(self: NoInfer<TProps>): TInitState;
127
136
  properties?: TProps;
128
137
  };
129
138
 
130
- export type GuiScriptHooksWithProperties<TProps, TSelf, TInitState> = GuiScriptHooks<
131
- TSelf,
132
- TInitState
139
+ export type GuiScriptHooksWithProperties<TProps, TSelf, TInitState> = Omit<
140
+ GuiScriptHooks<TSelf, TInitState>,
141
+ "init"
133
142
  > & {
143
+ init?(self: NoInfer<TProps>): TInitState;
134
144
  properties?: TProps;
135
145
  };
136
146
 
137
- export type RenderScriptHooksWithProperties<TProps, TSelf, TInitState> = RenderScriptHooks<
138
- TSelf,
139
- TInitState
147
+ export type RenderScriptHooksWithProperties<TProps, TSelf, TInitState> = Omit<
148
+ RenderScriptHooks<TSelf, TInitState>,
149
+ "init"
140
150
  > & {
151
+ init?(self: NoInfer<TProps>): TInitState;
141
152
  properties?: TProps;
142
153
  };
143
154
 
155
+ /**
156
+ * Extract a script module's declared property channel (`TProps`) as a nameable
157
+ * type. A script declares its editor properties with the value-keyed
158
+ * `properties` field of `defineScript`; another module reads that shape with
159
+ * `ScriptPropertiesOf<typeof script>` and names it as the `P` generic of
160
+ * `go.get`/`go.set` to read or tune those properties cross-script by URL (e.g.
161
+ * `go.get<ScriptPropertiesOf<typeof enemy>>()("/enemy#controller", "speed")`).
162
+ *
163
+ * It keeps one source of truth: the extracted shape is the same `TProps` the
164
+ * owning script's `self` exposes, so there is no second hand-maintained
165
+ * interface to drift.
166
+ */
167
+ export type ScriptPropertiesOf<T extends { properties?: object }> = NonNullable<T["properties"]>;
168
+
144
169
  /**
145
170
  * Type a `.script` component's hook table. At runtime this is an identity
146
171
  * function — it returns `hooks` unchanged; its only job is typing. It infers