@defold-typescript/types 0.5.5 → 0.7.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 (54) hide show
  1. package/README.md +2 -2
  2. package/api-targets.json +1 -1
  3. package/generated/b2d.d.ts +3 -0
  4. package/generated/buffer.d.ts +44 -38
  5. package/generated/builtin-messages.d.ts +1 -1
  6. package/generated/camera.d.ts +3 -0
  7. package/generated/collectionfactory.d.ts +47 -40
  8. package/generated/collectionproxy.d.ts +23 -18
  9. package/generated/crash.d.ts +3 -0
  10. package/generated/factory.d.ts +32 -24
  11. package/generated/go.d.ts +123 -124
  12. package/generated/graphics.d.ts +3 -0
  13. package/generated/gui.d.ts +303 -283
  14. package/generated/http.d.ts +26 -16
  15. package/generated/iac.d.ts +3 -0
  16. package/generated/iap.d.ts +6 -3
  17. package/generated/image.d.ts +30 -26
  18. package/generated/json.d.ts +36 -32
  19. package/generated/kinds/gui-script.d.ts +7 -5
  20. package/generated/kinds/render-script.d.ts +7 -5
  21. package/generated/kinds/script.d.ts +7 -5
  22. package/generated/label.d.ts +16 -9
  23. package/generated/liveupdate.d.ts +29 -26
  24. package/generated/model.d.ts +57 -45
  25. package/generated/msg.d.ts +3 -0
  26. package/generated/particlefx.d.ts +50 -34
  27. package/generated/physics.d.ts +153 -133
  28. package/generated/profiler.d.ts +45 -41
  29. package/generated/push.d.ts +5 -2
  30. package/generated/render.d.ts +410 -349
  31. package/generated/resource.d.ts +619 -572
  32. package/generated/socket.d.ts +49 -33
  33. package/generated/sound.d.ts +83 -72
  34. package/generated/sprite.d.ts +3 -0
  35. package/generated/sys.d.ts +198 -189
  36. package/generated/tilemap.d.ts +43 -39
  37. package/generated/timer.d.ts +42 -36
  38. package/generated/vmath.d.ts +22 -0
  39. package/generated/webview.d.ts +3 -0
  40. package/generated/window.d.ts +23 -17
  41. package/generated/zlib.d.ts +15 -12
  42. package/index.d.ts +3 -1
  43. package/package.json +13 -2
  44. package/scripts/fidelity-audit.ts +61 -1
  45. package/scripts/fidelity-baseline.json +10 -10
  46. package/scripts/ref-doc-delta.ts +143 -0
  47. package/scripts/regen.ts +18 -9
  48. package/src/core-types.ts +14 -0
  49. package/src/emit-dts.ts +219 -13
  50. package/src/engine-globals.d.ts +2 -0
  51. package/src/go-overloads.d.ts +43 -0
  52. package/src/index.ts +5 -0
  53. package/src/lifecycle.ts +157 -16
  54. package/src/publish-dts.ts +1 -1
@@ -0,0 +1,143 @@
1
+ import { parseDefoldApiDoc } from "../src/api-doc";
2
+ import {
3
+ type ApiTarget,
4
+ loadApiTargets,
5
+ type ResolveTargetOptions,
6
+ resolveTargetModules,
7
+ } from "./regen";
8
+
9
+ export interface RefDocDeltaArgs {
10
+ target: string;
11
+ namespace: string;
12
+ present: string[];
13
+ absent: string[];
14
+ json: boolean;
15
+ }
16
+
17
+ export interface RefDocDeltaReport {
18
+ ok: boolean;
19
+ targetId: string;
20
+ version: string;
21
+ namespace: string;
22
+ provenance: string | null;
23
+ missingPresent: string[];
24
+ unexpectedPresent: string[];
25
+ }
26
+
27
+ export interface VerifyRefDocDeltaInput {
28
+ target: ApiTarget;
29
+ namespace: string;
30
+ present: readonly string[];
31
+ absent: readonly string[];
32
+ resolveOpts?: ResolveTargetOptions;
33
+ }
34
+
35
+ export function collectElementNames(doc: unknown): Set<string> {
36
+ const module = parseDefoldApiDoc(doc);
37
+ const names = new Set<string>([module.namespace]);
38
+ for (const collection of [
39
+ module.functions,
40
+ module.constants,
41
+ module.variables,
42
+ module.properties,
43
+ ]) {
44
+ for (const item of collection) {
45
+ if (item.name.length > 0) names.add(item.name);
46
+ }
47
+ }
48
+ return names;
49
+ }
50
+
51
+ export function parseRefDocDeltaArgs(argv: readonly string[]): RefDocDeltaArgs {
52
+ const args: RefDocDeltaArgs = { target: "", namespace: "", present: [], absent: [], json: false };
53
+ for (let i = 0; i < argv.length; i++) {
54
+ const arg = argv[i];
55
+ if (arg === "--json") {
56
+ args.json = true;
57
+ continue;
58
+ }
59
+ if (arg === "--target" || arg === "--namespace" || arg === "--present" || arg === "--absent") {
60
+ const value = argv[++i];
61
+ if (!value) throw new Error(`${arg} requires a value`);
62
+ if (arg === "--target") args.target = value;
63
+ else if (arg === "--namespace") args.namespace = value;
64
+ else if (arg === "--present") args.present.push(value);
65
+ else args.absent.push(value);
66
+ continue;
67
+ }
68
+ throw new Error(`unknown argument: ${arg}`);
69
+ }
70
+ if (!args.target) throw new Error("--target is required");
71
+ if (!args.namespace) throw new Error("--namespace is required");
72
+ if (args.present.length === 0 && args.absent.length === 0) {
73
+ throw new Error("at least one --present or --absent assertion is required");
74
+ }
75
+ return args;
76
+ }
77
+
78
+ export function selectRefDocDeltaTarget(
79
+ targets: readonly ApiTarget[],
80
+ targetId: string,
81
+ ): ApiTarget {
82
+ const target = targets.find((item) => item.id === targetId);
83
+ if (!target) throw new Error(`target "${targetId}" not found`);
84
+ if ((target.source ?? null)?.kind !== "ref-doc") {
85
+ throw new Error(`target "${targetId}" is not ref-doc sourced`);
86
+ }
87
+ return target;
88
+ }
89
+
90
+ export async function verifyRefDocDelta(input: VerifyRefDocDeltaInput): Promise<RefDocDeltaReport> {
91
+ const source = input.target.source ?? null;
92
+ if (source?.kind !== "ref-doc") {
93
+ throw new Error(`target "${input.target.id}" is not ref-doc sourced`);
94
+ }
95
+ const modules = await resolveTargetModules(input.target, input.resolveOpts);
96
+ const module = modules.find((entry) => entry.namespace === input.namespace);
97
+ if (!module) {
98
+ throw new Error(`target "${input.target.id}" has no namespace "${input.namespace}"`);
99
+ }
100
+ const names = collectElementNames(module.doc);
101
+ const missingPresent = input.present.filter((name) => !names.has(name));
102
+ const unexpectedPresent = input.absent.filter((name) => names.has(name));
103
+ return {
104
+ ok: missingPresent.length === 0 && unexpectedPresent.length === 0,
105
+ targetId: input.target.id,
106
+ version: source.version,
107
+ namespace: input.namespace,
108
+ provenance: module.sourceProvenance ?? null,
109
+ missingPresent,
110
+ unexpectedPresent,
111
+ };
112
+ }
113
+
114
+ function renderPlainReport(report: RefDocDeltaReport): string {
115
+ const lines = [
116
+ `${report.ok ? "ok" : "drift"}: ${report.targetId}/${report.namespace} (${report.version}, ${report.provenance ?? "unknown"})`,
117
+ ];
118
+ if (report.missingPresent.length > 0) {
119
+ lines.push(`missing expected present: ${report.missingPresent.join(", ")}`);
120
+ }
121
+ if (report.unexpectedPresent.length > 0) {
122
+ lines.push(`unexpected expected absent: ${report.unexpectedPresent.join(", ")}`);
123
+ }
124
+ return lines.join("\n");
125
+ }
126
+
127
+ if (import.meta.main) {
128
+ try {
129
+ const args = parseRefDocDeltaArgs(process.argv.slice(2));
130
+ const target = selectRefDocDeltaTarget(loadApiTargets(), args.target);
131
+ const report = await verifyRefDocDelta({
132
+ target,
133
+ namespace: args.namespace,
134
+ present: args.present,
135
+ absent: args.absent,
136
+ });
137
+ console.log(args.json ? JSON.stringify(report, null, 2) : renderPlainReport(report));
138
+ if (!report.ok) process.exitCode = 1;
139
+ } catch (error) {
140
+ console.error(error instanceof Error ? error.message : String(error));
141
+ process.exitCode = 1;
142
+ }
143
+ }
package/scripts/regen.ts CHANGED
@@ -6,7 +6,12 @@ import { emitDeclarations } from "../src/emit-dts";
6
6
  import { emitBuiltinMessages, parseMessagesDoc } from "../src/emit-messages";
7
7
  import type { TranslationStore } from "../src/example-store";
8
8
  import { wrapAsAmbientGlobal } from "../src/publish-dts";
9
- import { type DownloadRefDoc, refDocCacheDir, resolveRefDoc } from "./doc-source";
9
+ import {
10
+ type DocSourceProvenance,
11
+ type DownloadRefDoc,
12
+ refDocCacheDir,
13
+ resolveRefDoc,
14
+ } from "./doc-source";
10
15
  import { loadTranslations } from "./example-store-io";
11
16
  import { type readZip, SYNC_MANIFEST, type SyncManifestEntry } from "./sync-api-docs";
12
17
 
@@ -75,6 +80,7 @@ export interface ModuleManifestEntry {
75
80
  readonly outFile: string;
76
81
  readonly skipFunctions?: readonly string[];
77
82
  readonly importsFrom?: string;
83
+ readonly sourceProvenance?: DocSourceProvenance;
78
84
  }
79
85
 
80
86
  export interface ResolveTargetOptions {
@@ -97,7 +103,7 @@ export async function resolveTargetModules(
97
103
  if (source == null) {
98
104
  return loadTargetModules(target, opts.packageRoot);
99
105
  }
100
- const { zip } = await resolveRefDoc({
106
+ const { zip, provenance } = await resolveRefDoc({
101
107
  version: source.version,
102
108
  cacheDir: opts.cacheDir ?? refDocCacheDir(),
103
109
  ...(opts.download ? { download: opts.download } : {}),
@@ -116,6 +122,7 @@ export async function resolveTargetModules(
116
122
  doc: JSON.parse(zip.read(sync.zipEntry)),
117
123
  outFile: module.outFile,
118
124
  importsFrom: target.coreTypesImport,
125
+ sourceProvenance: provenance,
119
126
  };
120
127
  return module.skipFunctions ? { ...entry, skipFunctions: module.skipFunctions } : entry;
121
128
  });
@@ -212,6 +219,7 @@ export const RESTRICTED_NAMESPACES: Readonly<Record<string, string>> = {
212
219
 
213
220
  const UNIVERSAL_EXTRA_IMPORTS: readonly string[] = [
214
221
  "../builtin-messages",
222
+ "../../src/engine-globals",
215
223
  "../../src/msg-overloads",
216
224
  "../../src/message-guard",
217
225
  "../../src/go-overloads",
@@ -220,12 +228,13 @@ const UNIVERSAL_EXTRA_IMPORTS: readonly string[] = [
220
228
  export interface KindManifestEntry {
221
229
  readonly kind: string;
222
230
  readonly restricted?: string;
231
+ readonly factory: string;
223
232
  }
224
233
 
225
234
  export const KIND_MODULE_MANIFEST: readonly KindManifestEntry[] = [
226
- { kind: "script" },
227
- { kind: "gui-script", restricted: "gui" },
228
- { kind: "render-script", restricted: "render" },
235
+ { kind: "script", factory: "defineScript" },
236
+ { kind: "gui-script", restricted: "gui", factory: "defineGuiScript" },
237
+ { kind: "render-script", restricted: "render", factory: "defineRenderScript" },
229
238
  ];
230
239
 
231
240
  export function generateKindIndex(kind: string): string {
@@ -234,11 +243,11 @@ export function generateKindIndex(kind: string): string {
234
243
  const universalNamespaces = MODULE_MANIFEST.filter(
235
244
  (m) => !Object.hasOwn(RESTRICTED_NAMESPACES, m.namespace),
236
245
  ).map((m) => `../${m.outFile.replace(/\.d\.ts$/, "")}`);
237
- const lines = [...UNIVERSAL_EXTRA_IMPORTS, ...universalNamespaces]
238
- .sort()
239
- .map((path) => `import "${path}";`);
246
+ const lines = [
247
+ ...new Set([...universalNamespaces.sort(), ...[...UNIVERSAL_EXTRA_IMPORTS].sort()]),
248
+ ].map((path) => `import "${path}";`);
240
249
  if (entry.restricted) lines.push(`import "../${entry.restricted}";`);
241
- return `${lines.join("\n")}\n\nexport {};\n`;
250
+ return `${lines.join("\n")}\n\nexport { ${entry.factory} } from "../../src/lifecycle";\nexport type { ScriptProperties, ScriptProperty } from "../../src/lifecycle";\n`;
242
251
  }
243
252
 
244
253
  export function generateVersionIndex(
package/src/core-types.ts CHANGED
@@ -78,6 +78,20 @@ export interface Hash {
78
78
  }
79
79
 
80
80
  declare const OpaqueBrand: unique symbol;
81
+ /**
82
+ * A nominal handle to an engine value you may hold and pass back to the API but
83
+ * must never inspect or construct — a Defold `node`, `texture`, `render_target`,
84
+ * `userdata`, etc. The `Name` parameter mints a distinct, mutually-incompatible
85
+ * brand per kind, so TypeScript's structural typing can't silently swap a
86
+ * `render_target` for a `constant`, and a plain object can't stand in for either.
87
+ *
88
+ * @remarks
89
+ * The brand is a phantom `unique symbol` property: it exists only in the type
90
+ * system (erased at transpile, never present at runtime). Because the symbol is
91
+ * not exported, consumer code cannot fabricate an `Opaque` — the engine API is
92
+ * the only source. Contrast with a `LuaTable` alias, which says the opposite:
93
+ * "inspect freely, the shape just isn't modeled."
94
+ */
81
95
  export interface Opaque<Name extends string> {
82
96
  readonly [OpaqueBrand]: Name;
83
97
  }
package/src/emit-dts.ts CHANGED
@@ -82,8 +82,11 @@ export const TS_RESERVED_NAMES = new Set([
82
82
  "extends",
83
83
  ]);
84
84
 
85
- // Element names whose `table` slot is a genuinely-arbitrary lua table by design
86
- // the serialization/JSON passthrough functions. Their emitted
85
+ // Element names whose `table` slot is a genuinely-arbitrary lua table by design.
86
+ // Two kinds: the serialization/JSON passthrough functions (Defold-internal — the
87
+ // engine round-trips an arbitrary lua value), and the platform/OS-sourced opaque
88
+ // blobs (external — the shape is set by the host OS or invoking app, not by
89
+ // Defold, so there is no documented field list). Their emitted
87
90
  // `Record<string | number, unknown>` is the faithful "any lua table" type, not a
88
91
  // `recordTables` fidelity loss, so the audit consults this set to avoid counting
89
92
  // them. A new ref-doc function with an opaque table must be added here
@@ -95,6 +98,9 @@ export const ARBITRARY_TABLE_SLOTS = new Set([
95
98
  "sys.load",
96
99
  "sys.serialize",
97
100
  "sys.deserialize",
101
+ "iac.set_listener",
102
+ "push.get_scheduled",
103
+ "push.get_all_scheduled",
98
104
  ]);
99
105
 
100
106
  // Element names whose `table` slot is a prose-only `a table mapping X to Y`
@@ -131,6 +137,157 @@ export const HOMOGENEOUS_ARRAY_SLOTS: ReadonlyMap<string, string | readonly stri
131
137
  ["sound.get_groups", "hash"],
132
138
  ["iap.list", "string"],
133
139
  ["go.delete", ["string", "hash", "url"]],
140
+ // push.register's `notifications` is a prose-only array of push.NOTIFICATION_*
141
+ // bitmask constants (the ref example sums NOTIFICATION_BADGE/SOUND/ALERT). The
142
+ // vendored push_doc.json fixture declares no NOTIFICATION_* constant elements, so
143
+ // no brand exists to reference; `number` is the faithful element token for these
144
+ // numeric constants, mirroring the vmath.vector/buffer.* number entries.
145
+ ["push.register", "number"],
146
+ ]);
147
+
148
+ // A `mapping` curation whose value is itself a single-level mapping, emitted
149
+ // `LuaMap<Kouter, LuaMap<Kinner, Vinner>>` — the nested-row-map shape
150
+ // (`tilemap.get_tiles` → `tiles[row][col]`).
151
+ export type NestedMapping = { key: string; value: string };
152
+
153
+ export type TableSlotCuration =
154
+ | { kind: "mapping"; key: string; value: string | readonly TableField[] | NestedMapping }
155
+ | { kind: "array"; element: string | readonly string[] }
156
+ | { kind: "object"; fields: readonly TableField[] }
157
+ | { kind: "array-object"; fields: readonly TableField[] };
158
+
159
+ const SOCKET_HANDLE_TOKENS = ["client", "master", "unconnected"] as const;
160
+
161
+ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new Map([
162
+ ["collectionfactory.create:return:ids", { kind: "mapping", key: "hash", value: "hash" }],
163
+ // iap.finish and iap.acknowledge take the same Defold IAP transaction object —
164
+ // the table handed to the iap.set_listener callback. The ref-doc fixture
165
+ // describes it in prose only (no field list), so the shape is curated from the
166
+ // Defold iap reference. As a param-side object curation every field emits `?`,
167
+ // which is faithful: original_trans/signature/user_id are platform-specific.
168
+ [
169
+ "iap.finish:param:transaction",
170
+ {
171
+ kind: "object",
172
+ fields: [
173
+ { name: "ident", types: ["string"] },
174
+ { name: "state", types: ["number"] },
175
+ { name: "trans_ident", types: ["string"] },
176
+ { name: "date", types: ["string"] },
177
+ { name: "original_trans", types: ["string"] },
178
+ { name: "receipt", types: ["string"] },
179
+ { name: "signature", types: ["string"] },
180
+ { name: "user_id", types: ["string"] },
181
+ ],
182
+ },
183
+ ],
184
+ [
185
+ "iap.acknowledge:param:transaction",
186
+ {
187
+ kind: "object",
188
+ fields: [
189
+ { name: "ident", types: ["string"] },
190
+ { name: "state", types: ["number"] },
191
+ { name: "trans_ident", types: ["string"] },
192
+ { name: "date", types: ["string"] },
193
+ { name: "original_trans", types: ["string"] },
194
+ { name: "receipt", types: ["string"] },
195
+ { name: "signature", types: ["string"] },
196
+ { name: "user_id", types: ["string"] },
197
+ ],
198
+ },
199
+ ],
200
+ // iap.buy's `options` is documented in the fixture as prose only ("optional
201
+ // parameters as properties"), so the field set is curated from the Defold iap
202
+ // reference: a Facebook-only custom `request_id` and a Google-Play-only
203
+ // subscription-offer `token`. Both are platform-specific, hence param-side
204
+ // optional fields.
205
+ [
206
+ "iap.buy:param:options",
207
+ {
208
+ kind: "object",
209
+ fields: [
210
+ { name: "request_id", types: ["string"] },
211
+ { name: "token", types: ["string"] },
212
+ ],
213
+ },
214
+ ],
215
+ [
216
+ "liveupdate.get_mounts:return:mounts",
217
+ {
218
+ kind: "array-object",
219
+ fields: [
220
+ { name: "name", types: ["string"] },
221
+ { name: "uri", types: ["string"] },
222
+ { name: "priority", types: ["number"] },
223
+ ],
224
+ },
225
+ ],
226
+ [
227
+ "model.get_aabb:return:aabb",
228
+ {
229
+ kind: "object",
230
+ fields: [
231
+ { name: "min", types: ["vector3"] },
232
+ { name: "max", types: ["vector3"] },
233
+ ],
234
+ },
235
+ ],
236
+ [
237
+ "model.get_mesh_aabb:return:aabb",
238
+ {
239
+ kind: "mapping",
240
+ key: "hash",
241
+ value: [
242
+ { name: "min", types: ["vector3"] },
243
+ { name: "max", types: ["vector3"] },
244
+ ],
245
+ },
246
+ ],
247
+ ["physics.raycast:param:groups", { kind: "array", element: "hash" }],
248
+ ["physics.raycast_async:param:groups", { kind: "array", element: "hash" }],
249
+ // push.schedule's `notification_settings` is documented in the fixture as prose
250
+ // only ("Table with notification and platform specific fields"), so the field
251
+ // set is curated from the Defold push reference: an iOS `action`, an iOS
252
+ // `badge_count`, and an Android `priority`. Each is platform-specific, hence
253
+ // param-side optional fields.
254
+ [
255
+ "push.schedule:param:notification_settings",
256
+ {
257
+ kind: "object",
258
+ fields: [
259
+ { name: "action", types: ["string"] },
260
+ { name: "badge_count", types: ["number"] },
261
+ { name: "priority", types: ["number"] },
262
+ ],
263
+ },
264
+ ],
265
+ ["socket.select:param:recvt", { kind: "array", element: SOCKET_HANDLE_TOKENS }],
266
+ ["socket.select:param:sendt", { kind: "array", element: SOCKET_HANDLE_TOKENS }],
267
+ ["socket.select:return:sockets_r", { kind: "array", element: SOCKET_HANDLE_TOKENS }],
268
+ ["socket.select:return:sockets_w", { kind: "array", element: SOCKET_HANDLE_TOKENS }],
269
+ [
270
+ "tilemap.get_tile_info:return:tile_info",
271
+ {
272
+ kind: "object",
273
+ fields: [
274
+ { name: "index", types: ["number"] },
275
+ { name: "h_flip", types: ["boolean"] },
276
+ { name: "v_flip", types: ["boolean"] },
277
+ { name: "rotate_90", types: ["boolean"] },
278
+ ],
279
+ },
280
+ ],
281
+ // tilemap.get_tiles returns a sparse table of rows iterated `tiles[row][col]`,
282
+ // the keys being tile positions offset by tilemap.get_bounds() (not a dense
283
+ // 0..n run). The faithful shape is the nested LuaMap idiom
284
+ // `LuaMap<number, LuaMap<number, number>>` — the keys are plain `number` tile
285
+ // positions, not a branded `Hash`, and a non-string-keyed Lua table is `LuaMap`,
286
+ // never a `Record` (which would imply a dense index object).
287
+ [
288
+ "tilemap.get_tiles:return:tiles",
289
+ { kind: "mapping", key: "number", value: { key: "number", value: "number" } },
290
+ ],
134
291
  ]);
135
292
 
136
293
  /**
@@ -482,6 +639,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
482
639
  const decl = hasAliases ? "export " : "";
483
640
 
484
641
  const lines: string[] = [];
642
+ for (const docLine of namespaceDocLines(module)) lines.push(docLine);
485
643
  lines.push(`declare namespace ${module.namespace} {`);
486
644
 
487
645
  for (const t of typedefs) {
@@ -653,6 +811,15 @@ function summaryDocLines(brief: string, description: string, indent: string): st
653
811
  return indentDocLines({ summary: htmlToDocText(summaryFor(brief, description)) }, indent);
654
812
  }
655
813
 
814
+ function namespaceDocLines(module: ApiModule): string[] {
815
+ const official = summaryFor(module.brief, module.description).trim();
816
+ const summary =
817
+ official.length > 0
818
+ ? htmlToDocText(official)
819
+ : `(synthesized)\nDefold \`${module.namespace}\` API namespace.`;
820
+ return indentDocLines({ summary }, "");
821
+ }
822
+
656
823
  function indentDocLines(parts: DocCommentParts, indent: string): string[] {
657
824
  return renderDocComment(parts).map((line) => `${indent}${line}`);
658
825
  }
@@ -690,7 +857,7 @@ function emitParameter(
690
857
  const concrete = p.types.filter((t) => t !== "nil");
691
858
  const ts =
692
859
  concrete.length > 0
693
- ? mapSlotUnion(concrete, p.doc, mapType, true, resolver, elementName)
860
+ ? mapSlotUnion(concrete, p.doc, mapType, true, resolver, elementName, "param", p.name)
694
861
  : "unknown";
695
862
  return `${name}${optional ? "?" : ""}: ${ts}`;
696
863
  }
@@ -708,7 +875,7 @@ function emitReturn(
708
875
  // typescript-to-lua erases to `local a, b = fn()`.
709
876
  const slots = returnValues.map((rv) =>
710
877
  rv.types.length > 0
711
- ? mapSlotUnion(rv.types, rv.doc, mapType, false, resolver, elementName)
878
+ ? mapSlotUnion(rv.types, rv.doc, mapType, false, resolver, elementName, "return", rv.name)
712
879
  : "unknown",
713
880
  );
714
881
  return { type: `LuaMultiReturn<[${slots.join(", ")}]>`, trailing: "" };
@@ -717,7 +884,16 @@ function emitReturn(
717
884
  if (!first) return { type: "void", trailing: "" };
718
885
  const ts =
719
886
  first.types.length > 0
720
- ? mapSlotUnion(first.types, first.doc, mapType, false, resolver, elementName)
887
+ ? mapSlotUnion(
888
+ first.types,
889
+ first.doc,
890
+ mapType,
891
+ false,
892
+ resolver,
893
+ elementName,
894
+ "return",
895
+ first.name,
896
+ )
721
897
  : "unknown";
722
898
  return { type: ts, trailing: "" };
723
899
  }
@@ -742,22 +918,38 @@ function mapSlotUnion(
742
918
  optionalFields: boolean,
743
919
  resolver: TableDocResolver,
744
920
  elementName: string,
921
+ slotKind?: "param" | "return",
922
+ slotName?: string,
745
923
  ): string {
746
924
  const mapped: string[] = [];
747
925
  const seen = new Set<string>();
748
926
  for (const token of types) {
749
927
  let ts: string;
750
928
  if (token === "table") {
751
- const mapping = MAPPING_TABLE_SLOTS.get(elementName);
752
- const element = HOMOGENEOUS_ARRAY_SLOTS.get(elementName);
929
+ const curation =
930
+ slotKind !== undefined && slotName !== undefined
931
+ ? TABLE_SLOT_CURATIONS.get(tableSlotKey(elementName, slotKind, slotName))
932
+ : undefined;
933
+ const mapping =
934
+ curation?.kind === "mapping" ? curation : MAPPING_TABLE_SLOTS.get(elementName);
935
+ const element =
936
+ curation?.kind === "array" ? curation.element : HOMOGENEOUS_ARRAY_SLOTS.get(elementName);
753
937
  if (mapping !== undefined) {
754
- ts = `LuaMap<${mapType(mapping.key)}, ${mapType(mapping.value)}>`;
938
+ let value: string;
939
+ if (typeof mapping.value === "string") {
940
+ value = mapType(mapping.value);
941
+ } else if (Array.isArray(mapping.value)) {
942
+ value = inlineTableType(mapping.value, mapType, optionalFields);
943
+ } else {
944
+ const nested = mapping.value as NestedMapping;
945
+ value = `LuaMap<${mapType(nested.key)}, ${mapType(nested.value)}>`;
946
+ }
947
+ ts = `LuaMap<${mapType(mapping.key)}, ${value}>`;
755
948
  } else if (element !== undefined) {
756
- const tokens = typeof element === "string" ? [element] : element;
757
- ts =
758
- tokens.length > 1
759
- ? `(${unionFromTokens(tokens, mapType)})[]`
760
- : `${mapType(tokens[0] as string)}[]`;
949
+ ts = arrayTypeFromTokens(element, mapType);
950
+ } else if (curation?.kind === "object" || curation?.kind === "array-object") {
951
+ const object = inlineTableType(curation.fields, mapType, optionalFields);
952
+ ts = curation.kind === "array-object" ? `${object}[]` : object;
761
953
  } else {
762
954
  const fields = parseTableFields(doc, resolver);
763
955
  if (fields !== null) {
@@ -777,6 +969,20 @@ function mapSlotUnion(
777
969
  return mapped.join(" | ");
778
970
  }
779
971
 
972
+ function tableSlotKey(elementName: string, slotKind: "param" | "return", slotName: string): string {
973
+ return `${elementName}:${slotKind}:${slotName}`;
974
+ }
975
+
976
+ function arrayTypeFromTokens(
977
+ element: string | readonly string[],
978
+ mapType: (t: string) => string,
979
+ ): string {
980
+ const tokens = typeof element === "string" ? [element] : element;
981
+ return tokens.length > 1
982
+ ? `(${unionFromTokens(tokens, mapType)})[]`
983
+ : `${mapType(tokens[0] as string)}[]`;
984
+ }
985
+
780
986
  export function inlineTableType(
781
987
  fields: readonly TableField[],
782
988
  mapType: (t: string) => string,
@@ -1,7 +1,9 @@
1
+ /** @noSelfInFile */
1
2
  import type * as Core from "./core-types";
2
3
 
3
4
  declare global {
4
5
  type Hash = Core.Hash;
6
+ function hash(s: string): Core.Hash;
5
7
  type Opaque<Name extends string> = Core.Opaque<Name>;
6
8
  type Url = Core.Url;
7
9
  type Vector = Core.Vector;
@@ -1,6 +1,11 @@
1
1
  /** @noSelfInFile */
2
+
2
3
  import type { Hash, Opaque, Quaternion, Url, Vector3, Vector4 } from "./core-types";
3
4
 
5
+ interface ScriptProperty<TValue> {
6
+ readonly __defoldScriptProperty: TValue;
7
+ }
8
+
4
9
  declare global {
5
10
  namespace go {
6
11
  interface GoPropertyOptions {
@@ -61,5 +66,43 @@ declare global {
61
66
  value: number | boolean | Hash | Url | Vector3 | Vector4 | Quaternion | Opaque<"resource">,
62
67
  options?: GoPropertyOptions,
63
68
  ): void;
69
+ /**
70
+ * Registers a Defold editor script property (deprecated escape hatch).
71
+ *
72
+ * @deprecated Don't call `go.property` yourself. Declare the property in
73
+ * `defineScript({ properties })` — that is the only form that types it onto
74
+ * `self`; the transpiler emits the `go.property(...)` registration for you.
75
+ * A direct call still registers, but `self.<name>` stays untyped.
76
+ *
77
+ * @param name - editor property id to register.
78
+ * @param value - default value for the registered property.
79
+ * @returns a phantom descriptor used only for TypeScript self typing.
80
+ * @example
81
+ * ```ts
82
+ * // Declare it as a field — the key is the name, the value is the default:
83
+ * export default defineScript({
84
+ * properties: { speed: 450 },
85
+ * update(self) {
86
+ * self.speed; // number
87
+ * },
88
+ * });
89
+ * // The transpiler emits, at chunk scope: go.property("speed", 450)
90
+ * ```
91
+ */
92
+ function property(name: string, value: number): ScriptProperty<number>;
93
+ /** @deprecated Declare booleans via the `defineScript({ properties })` field. */
94
+ function property(name: string, value: boolean): ScriptProperty<boolean>;
95
+ /** @deprecated Declare hashes via the `defineScript({ properties })` field. */
96
+ function property(name: string, value: Hash): ScriptProperty<Hash>;
97
+ /** @deprecated Declare URLs via the `defineScript({ properties })` field. */
98
+ function property(name: string, value: Url): ScriptProperty<Url>;
99
+ /** @deprecated Declare vector3s via the `defineScript({ properties })` field. */
100
+ function property(name: string, value: Vector3): ScriptProperty<Vector3>;
101
+ /** @deprecated Declare vector4s via the `defineScript({ properties })` field. */
102
+ function property(name: string, value: Vector4): ScriptProperty<Vector4>;
103
+ /** @deprecated Declare quaternions via the `defineScript({ properties })` field. */
104
+ function property(name: string, value: Quaternion): ScriptProperty<Quaternion>;
105
+ /** @deprecated Declare resources via the `defineScript({ properties })` field. */
106
+ function property(name: string, value: Opaque<"resource">): ScriptProperty<Opaque<"resource">>;
64
107
  }
65
108
  }
package/src/index.ts CHANGED
@@ -17,11 +17,16 @@ export {
17
17
  defineRenderScript,
18
18
  defineScript,
19
19
  type GuiScriptHooks,
20
+ type GuiScriptHooksWithProperties,
20
21
  type InputAction,
21
22
  type InputTouch,
22
23
  type RenderScriptHooks,
24
+ type RenderScriptHooksWithProperties,
23
25
  SCRIPT_HOOK_NAMES,
24
26
  type ScriptHookName,
25
27
  type ScriptHooks,
28
+ type ScriptHooksWithProperties,
29
+ type ScriptProperties,
30
+ type ScriptProperty,
26
31
  } from "./lifecycle";
27
32
  export { type WrapOptions, wrapAsAmbientGlobal } from "./publish-dts";