@defold-typescript/types 0.5.4 → 0.6.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 (59) hide show
  1. package/api-targets.json +1 -1
  2. package/generated/b2d.d.ts +3 -0
  3. package/generated/buffer.d.ts +44 -38
  4. package/generated/builtin-messages.d.ts +1 -1
  5. package/generated/camera.d.ts +3 -0
  6. package/generated/collectionfactory.d.ts +47 -40
  7. package/generated/collectionproxy.d.ts +23 -18
  8. package/generated/crash.d.ts +3 -0
  9. package/generated/factory.d.ts +32 -24
  10. package/generated/go.d.ts +293 -293
  11. package/generated/graphics.d.ts +3 -0
  12. package/generated/gui.d.ts +303 -283
  13. package/generated/http.d.ts +26 -16
  14. package/generated/iac.d.ts +3 -0
  15. package/generated/iap.d.ts +6 -3
  16. package/generated/image.d.ts +30 -26
  17. package/generated/json.d.ts +36 -32
  18. package/generated/kinds/gui-script.d.ts +7 -5
  19. package/generated/kinds/render-script.d.ts +7 -5
  20. package/generated/kinds/script.d.ts +7 -5
  21. package/generated/label.d.ts +16 -9
  22. package/generated/liveupdate.d.ts +29 -26
  23. package/generated/model.d.ts +57 -45
  24. package/generated/msg.d.ts +12 -9
  25. package/generated/particlefx.d.ts +50 -34
  26. package/generated/physics.d.ts +153 -133
  27. package/generated/profiler.d.ts +45 -41
  28. package/generated/push.d.ts +5 -2
  29. package/generated/render.d.ts +410 -349
  30. package/generated/resource.d.ts +619 -572
  31. package/generated/socket.d.ts +49 -33
  32. package/generated/sound.d.ts +83 -72
  33. package/generated/sprite.d.ts +36 -32
  34. package/generated/sys.d.ts +198 -189
  35. package/generated/tilemap.d.ts +43 -39
  36. package/generated/timer.d.ts +42 -36
  37. package/generated/vmath.d.ts +254 -229
  38. package/generated/webview.d.ts +3 -0
  39. package/generated/window.d.ts +23 -17
  40. package/generated/zlib.d.ts +15 -12
  41. package/index.d.ts +3 -1
  42. package/package.json +6 -2
  43. package/scripts/example-store-io.ts +18 -0
  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 +23 -10
  48. package/src/core-types.ts +14 -0
  49. package/src/doc-comment.ts +2 -1
  50. package/src/emit-dts.ts +238 -18
  51. package/src/engine-globals.d.ts +2 -0
  52. package/src/example-store.ts +44 -0
  53. package/src/go-overloads.d.ts +73 -0
  54. package/src/index.ts +5 -0
  55. package/src/lifecycle.ts +157 -16
  56. package/src/message-dispatch.d.ts +21 -0
  57. package/src/message-guard.d.ts +19 -0
  58. package/src/msg-overloads.d.ts +20 -0
  59. package/src/publish-dts.ts +1 -1
@@ -1,5 +1,8 @@
1
1
  /** @noSelfInFile */
2
2
  declare global {
3
+ /**
4
+ * Functions and constants for interacting with webview APIs
5
+ */
3
6
  namespace webview {
4
7
  /**
5
8
  * Creates a webview instance. It can show HTML pages as well as evaluate Javascript. The view remains hidden until the first call. There can exist a maximum of 4 webviews at the same time.
@@ -2,6 +2,10 @@
2
2
  import type { Opaque } from "../src/core-types";
3
3
 
4
4
  declare global {
5
+ /**
6
+ * Functions and constants to access the window, window event listeners
7
+ * and screen dimming.
8
+ */
5
9
  namespace window {
6
10
  /**
7
11
  * Dimming mode is used to control whether or not a mobile device should dim the screen after a period without user interaction.
@@ -114,24 +118,26 @@ declare global {
114
118
  * - number `width`: The width of a resize event. nil otherwise.
115
119
  * - number `height`: The height of a resize event. nil otherwise.
116
120
  * @example
117
- * ```lua
118
- * function window_callback(self, event, data)
119
- * if event == window.WINDOW_EVENT_FOCUS_LOST then
120
- * print("window.WINDOW_EVENT_FOCUS_LOST")
121
- * elseif event == window.WINDOW_EVENT_FOCUS_GAINED then
122
- * print("window.WINDOW_EVENT_FOCUS_GAINED")
123
- * elseif event == window.WINDOW_EVENT_ICONFIED then
124
- * print("window.WINDOW_EVENT_ICONFIED")
125
- * elseif event == window.WINDOW_EVENT_DEICONIFIED then
126
- * print("window.WINDOW_EVENT_DEICONIFIED")
127
- * elseif event == window.WINDOW_EVENT_RESIZED then
128
- * print("Window resized: ", data.width, data.height)
129
- * end
130
- * end
121
+ * ```ts
122
+ * function window_callback(self, event, data) {
123
+ * if (event === window.WINDOW_EVENT_FOCUS_LOST) {
124
+ * print("window.WINDOW_EVENT_FOCUS_LOST");
125
+ * } else if (event === window.WINDOW_EVENT_FOCUS_GAINED) {
126
+ * print("window.WINDOW_EVENT_FOCUS_GAINED");
127
+ * } else if (event === window.WINDOW_EVENT_ICONFIED) {
128
+ * print("window.WINDOW_EVENT_ICONFIED");
129
+ * } else if (event === window.WINDOW_EVENT_DEICONIFIED) {
130
+ * print("window.WINDOW_EVENT_DEICONIFIED");
131
+ * } else if (event === window.WINDOW_EVENT_RESIZED) {
132
+ * print("Window resized: ", data.width, data.height);
133
+ * }
134
+ * }
131
135
  *
132
- * function init(self)
133
- * window.set_listener(window_callback)
134
- * end
136
+ * export default defineScript({
137
+ * init() {
138
+ * window.set_listener(window_callback);
139
+ * },
140
+ * });
135
141
  * ```
136
142
  */
137
143
  function set_listener(callback?: (self: unknown, event: unknown, data: unknown) => void): void;
@@ -1,5 +1,8 @@
1
1
  /** @noSelfInFile */
2
2
  declare global {
3
+ /**
4
+ * Functions for compression and decompression of string buffers.
5
+ */
3
6
  namespace zlib {
4
7
  /**
5
8
  * A lua error is raised is on error
@@ -7,14 +10,14 @@ declare global {
7
10
  * @param buf - buffer to deflate
8
11
  * @returns deflated buffer
9
12
  * @example
10
- * ```lua
11
- * local data = "This is a string with uncompressed data."
12
- * local compressed_data = zlib.deflate(data)
13
- * local s = ""
14
- * for c in string.gmatch(compressed_data, ".") do
15
- * s = s .. '\\' .. string.byte(c)
16
- * end
17
- * print(s) --> \120\94\11\201\200\44\86\0\162\68\133\226\146\162 ...
13
+ * ```ts
14
+ * const data = "This is a string with uncompressed data.";
15
+ * const compressed_data = zlib.deflate(data);
16
+ * let s = "";
17
+ * for (const c of compressed_data) {
18
+ * s = s + "\\" + c.charCodeAt(0);
19
+ * }
20
+ * print(s); //> \120\94\11\201\200\44\86\0\162\68\133\226\146\162 ...
18
21
  * ```
19
22
  */
20
23
  function deflate(buf: string): string;
@@ -24,10 +27,10 @@ declare global {
24
27
  * @param buf - buffer to inflate
25
28
  * @returns inflated buffer
26
29
  * @example
27
- * ```lua
28
- * local data = "\120\94\11\201\200\44\86\0\162\68\133\226\146\162\204\188\116\133\242\204\146\12\133\210\188\228\252\220\130\162\212\226\226\212\20\133\148\196\146\68\61\0\44\67\14\201"
29
- * local uncompressed_data = zlib.inflate(data)
30
- * print(uncompressed_data) --> This is a string with uncompressed data.
30
+ * ```ts
31
+ * const data = "\120\94\11\201\200\44\86\0\162\68\133\226\146\162\204\188\116\133\242\204\146\12\133\210\188\228\252\220\130\162\212\226\226\212\20\133\148\196\146\68\61\0\44\67\14\201";
32
+ * const uncompressed_data = zlib.inflate(data);
33
+ * print(uncompressed_data); //> This is a string with uncompressed data.
31
34
  * ```
32
35
  */
33
36
  function inflate(buf: string): string;
package/index.d.ts CHANGED
@@ -2,7 +2,6 @@ import "./generated/builtin-messages";
2
2
  import "./src/msg-overloads";
3
3
  import "./src/message-guard";
4
4
  import "./src/message-dispatch";
5
- import "./src/go-overloads";
6
5
  import "./src/engine-globals";
7
6
  import "./generated/b2d";
8
7
  import "./generated/buffer";
@@ -12,6 +11,7 @@ import "./generated/collectionproxy";
12
11
  import "./generated/crash";
13
12
  import "./generated/factory";
14
13
  import "./generated/go";
14
+ import "./src/go-overloads";
15
15
  import "./generated/graphics";
16
16
  import "./generated/gui";
17
17
  import "./generated/http";
@@ -64,5 +64,7 @@ export {
64
64
  SCRIPT_HOOK_NAMES,
65
65
  type ScriptHookName,
66
66
  type ScriptHooks,
67
+ type ScriptProperties,
68
+ type ScriptProperty,
67
69
  } from "./src/lifecycle";
68
70
  export { type WrapOptions, wrapAsAmbientGlobal } from "./src/publish-dts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/types",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "TypeScript types for the Defold engine's Lua APIs.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,6 +20,9 @@
20
20
  "./render-script": {
21
21
  "types": "./generated/kinds/render-script.d.ts"
22
22
  },
23
+ "./core-types": {
24
+ "types": "./src/core-types.ts"
25
+ },
23
26
  "./package.json": "./package.json"
24
27
  },
25
28
  "files": [
@@ -39,7 +42,8 @@
39
42
  "build": "tsc -p tsconfig.build.json",
40
43
  "typecheck": "tsc -p tsconfig.json --noEmit",
41
44
  "regen": "bun scripts/regen.ts",
42
- "sync-api-docs": "bun scripts/sync-api-docs.ts"
45
+ "sync-api-docs": "bun scripts/sync-api-docs.ts",
46
+ "ref-doc-delta": "bun scripts/ref-doc-delta.ts --target defold-1.9.8 --namespace label --present label.get_text --absent label.set_text"
43
47
  },
44
48
  "devDependencies": {
45
49
  "@typescript-to-lua/language-extensions": "1.19.0"
@@ -0,0 +1,18 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import type { TranslationStore } from "../src/example-store";
4
+
5
+ // Build-time only: lives under `scripts/` (never reachable from the shipped
6
+ // `src/index.ts` graph) so its `node:fs` import cannot leak into a consumer's
7
+ // typecheck. `src/example-store.ts` stays pure for exactly that reason.
8
+ const TRANSLATIONS_PATH = resolve(import.meta.dir, "..", "examples", "translations.json");
9
+
10
+ export function loadTranslations(path: string = TRANSLATIONS_PATH): TranslationStore {
11
+ let raw: string;
12
+ try {
13
+ raw = readFileSync(path, "utf8");
14
+ } catch {
15
+ return {};
16
+ }
17
+ return JSON.parse(raw) as TranslationStore;
18
+ }
@@ -5,8 +5,11 @@ import {
5
5
  buildTableDocResolver,
6
6
  HOMOGENEOUS_ARRAY_SLOTS,
7
7
  MAPPING_TABLE_SLOTS,
8
+ type NestedMapping,
8
9
  parseTableFields,
9
10
  recoverCallbackSignature,
11
+ TABLE_SLOT_CURATIONS,
12
+ type TableSlotCuration,
10
13
  TS_IDENTIFIER,
11
14
  } from "../src/emit-dts";
12
15
  import { parseMessagesDoc } from "../src/emit-messages";
@@ -113,6 +116,7 @@ function auditEntry(
113
116
  arbitraryTable = false,
114
117
  mappingSlot?: { key: string; value: string },
115
118
  homogeneousElement?: string | readonly string[],
119
+ tableSlotCuration?: TableSlotCuration,
116
120
  ) => {
117
121
  for (const token of types) {
118
122
  // A `table` slot whose doc carries a parseable `<dl>` field list is
@@ -127,6 +131,29 @@ function auditEntry(
127
131
  // the curated key/value tokens — not a `Record`, so don't count it.
128
132
  // Feed the curated tokens back through considerTypes so an unmapped one
129
133
  // still surfaces under unknownTokens (none today: hash/node/vector3 map).
134
+ if (tableSlotCuration?.kind === "mapping") {
135
+ // A single-token value feeds straight back; an object-valued mapping
136
+ // (`LuaMap<K, { … }>`) feeds the key plus each curated field type, the
137
+ // same way the object branch does; a nested-mapping value
138
+ // (`LuaMap<K, LuaMap<K, V>>`) feeds the outer key plus the inner
139
+ // key/value tokens — so an unmapped token in any arm still surfaces.
140
+ if (typeof tableSlotCuration.value === "string") {
141
+ considerTypes([tableSlotCuration.key, tableSlotCuration.value]);
142
+ } else if (Array.isArray(tableSlotCuration.value)) {
143
+ considerTypes([tableSlotCuration.key]);
144
+ for (const field of tableSlotCuration.value) {
145
+ if (field.fields !== undefined) {
146
+ for (const nested of field.fields) considerTypes(nested.types);
147
+ } else if (field.numberList !== true) {
148
+ considerTypes(field.types);
149
+ }
150
+ }
151
+ } else {
152
+ const nested = tableSlotCuration.value as NestedMapping;
153
+ considerTypes([tableSlotCuration.key, nested.key, nested.value]);
154
+ }
155
+ continue;
156
+ }
130
157
  if (mappingSlot !== undefined) {
131
158
  considerTypes([mappingSlot.key, mappingSlot.value]);
132
159
  continue;
@@ -136,6 +163,24 @@ function auditEntry(
136
163
  // `Record`, so don't count it. Feed the curated token(s) back through
137
164
  // considerTypes so an unmapped one still surfaces under unknownTokens
138
165
  // (none today: number/hash/string/url map).
166
+ if (tableSlotCuration?.kind === "array") {
167
+ considerTypes(
168
+ typeof tableSlotCuration.element === "string"
169
+ ? [tableSlotCuration.element]
170
+ : tableSlotCuration.element,
171
+ );
172
+ continue;
173
+ }
174
+ if (tableSlotCuration?.kind === "object" || tableSlotCuration?.kind === "array-object") {
175
+ for (const field of tableSlotCuration.fields) {
176
+ if (field.fields !== undefined) {
177
+ for (const nested of field.fields) considerTypes(nested.types);
178
+ } else if (field.numberList !== true) {
179
+ considerTypes(field.types);
180
+ }
181
+ }
182
+ continue;
183
+ }
139
184
  if (homogeneousElement !== undefined) {
140
185
  considerTypes(
141
186
  typeof homogeneousElement === "string" ? [homogeneousElement] : homogeneousElement,
@@ -236,26 +281,37 @@ function auditEntry(
236
281
  const homogeneousElement =
237
282
  typeof element.name === "string" ? HOMOGENEOUS_ARRAY_SLOTS.get(element.name) : undefined;
238
283
  params.forEach((param, index) => {
284
+ const tableSlotCuration =
285
+ typeof element.name === "string" && typeof param.name === "string"
286
+ ? TABLE_SLOT_CURATIONS.get(tableSlotKey(element.name, "param", param.name))
287
+ : undefined;
239
288
  considerTypes(
240
289
  stringArray(param.types),
241
290
  docString(param.doc),
242
291
  arbitraryTable,
243
292
  mappingSlot,
244
293
  homogeneousElement,
294
+ tableSlotCuration,
245
295
  );
246
296
  // Residual: a doc-optional param the emitter cannot mark `?` because a
247
297
  // required param follows it. The trailing-run cutoff must match
248
298
  // emit-dts so the gate and the emitted surface agree.
249
299
  if (isDocOptional(param) && index < cutoff) optionalAsRequired += 1;
250
300
  });
251
- for (const ret of returns)
301
+ for (const ret of returns) {
302
+ const tableSlotCuration =
303
+ typeof element.name === "string" && typeof ret.name === "string"
304
+ ? TABLE_SLOT_CURATIONS.get(tableSlotKey(element.name, "return", ret.name))
305
+ : undefined;
252
306
  considerTypes(
253
307
  stringArray(ret.types),
254
308
  docString(ret.doc),
255
309
  arbitraryTable,
256
310
  mappingSlot,
257
311
  homogeneousElement,
312
+ tableSlotCuration,
258
313
  );
314
+ }
259
315
  }
260
316
 
261
317
  return {
@@ -274,6 +330,10 @@ function stripNamespace(name: string): string {
274
330
  return index === -1 ? name : name.slice(index + 1);
275
331
  }
276
332
 
333
+ function tableSlotKey(elementName: string, slotKind: "param" | "return", slotName: string): string {
334
+ return `${elementName}:${slotKind}:${slotName}`;
335
+ }
336
+
277
337
  export function buildFidelityReport(
278
338
  manifest: readonly ModuleManifestEntry[] = MODULE_MANIFEST,
279
339
  ): Record<string, FidelityEntry> {
@@ -26,7 +26,7 @@
26
26
  "collectionfactory": {
27
27
  "droppedElements": 0,
28
28
  "unknownTokens": [],
29
- "recordTables": 2,
29
+ "recordTables": 1,
30
30
  "multiReturn": 0,
31
31
  "droppedMembers": 0,
32
32
  "optionalAsRequired": 0
@@ -60,7 +60,7 @@
60
60
  "unknownTokens": [],
61
61
  "recordTables": 2,
62
62
  "multiReturn": 0,
63
- "droppedMembers": 2,
63
+ "droppedMembers": 3,
64
64
  "optionalAsRequired": 0
65
65
  },
66
66
  "graphics": {
@@ -90,7 +90,7 @@
90
90
  "iac": {
91
91
  "droppedElements": 0,
92
92
  "unknownTokens": [],
93
- "recordTables": 1,
93
+ "recordTables": 0,
94
94
  "multiReturn": 0,
95
95
  "droppedMembers": 0,
96
96
  "optionalAsRequired": 0
@@ -98,7 +98,7 @@
98
98
  "iap": {
99
99
  "droppedElements": 0,
100
100
  "unknownTokens": [],
101
- "recordTables": 3,
101
+ "recordTables": 0,
102
102
  "multiReturn": 0,
103
103
  "droppedMembers": 0,
104
104
  "optionalAsRequired": 0
@@ -130,7 +130,7 @@
130
130
  "liveupdate": {
131
131
  "droppedElements": 0,
132
132
  "unknownTokens": [],
133
- "recordTables": 1,
133
+ "recordTables": 0,
134
134
  "multiReturn": 0,
135
135
  "droppedMembers": 0,
136
136
  "optionalAsRequired": 0
@@ -138,7 +138,7 @@
138
138
  "model": {
139
139
  "droppedElements": 0,
140
140
  "unknownTokens": [],
141
- "recordTables": 2,
141
+ "recordTables": 0,
142
142
  "multiReturn": 0,
143
143
  "droppedMembers": 0,
144
144
  "optionalAsRequired": 0
@@ -162,7 +162,7 @@
162
162
  "physics": {
163
163
  "droppedElements": 0,
164
164
  "unknownTokens": [],
165
- "recordTables": 5,
165
+ "recordTables": 3,
166
166
  "multiReturn": 0,
167
167
  "droppedMembers": 0,
168
168
  "optionalAsRequired": 0
@@ -178,7 +178,7 @@
178
178
  "push": {
179
179
  "droppedElements": 0,
180
180
  "unknownTokens": [],
181
- "recordTables": 4,
181
+ "recordTables": 0,
182
182
  "multiReturn": 0,
183
183
  "droppedMembers": 0,
184
184
  "optionalAsRequired": 0
@@ -202,7 +202,7 @@
202
202
  "socket": {
203
203
  "droppedElements": 0,
204
204
  "unknownTokens": [],
205
- "recordTables": 8,
205
+ "recordTables": 4,
206
206
  "multiReturn": 0,
207
207
  "droppedMembers": 0,
208
208
  "optionalAsRequired": 0
@@ -234,7 +234,7 @@
234
234
  "tilemap": {
235
235
  "droppedElements": 0,
236
236
  "unknownTokens": [],
237
- "recordTables": 2,
237
+ "recordTables": 0,
238
238
  "multiReturn": 0,
239
239
  "droppedMembers": 0,
240
240
  "optionalAsRequired": 0
@@ -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
@@ -4,8 +4,15 @@ import messagesDoc from "../fixtures/messages_doc.json" with { type: "json" };
4
4
  import { parseDefoldApiDoc } from "../src/api-doc";
5
5
  import { emitDeclarations } from "../src/emit-dts";
6
6
  import { emitBuiltinMessages, parseMessagesDoc } from "../src/emit-messages";
7
+ import type { TranslationStore } from "../src/example-store";
7
8
  import { wrapAsAmbientGlobal } from "../src/publish-dts";
8
- import { type DownloadRefDoc, refDocCacheDir, resolveRefDoc } from "./doc-source";
9
+ import {
10
+ type DocSourceProvenance,
11
+ type DownloadRefDoc,
12
+ refDocCacheDir,
13
+ resolveRefDoc,
14
+ } from "./doc-source";
15
+ import { loadTranslations } from "./example-store-io";
9
16
  import { type readZip, SYNC_MANIFEST, type SyncManifestEntry } from "./sync-api-docs";
10
17
 
11
18
  export interface ApiTargetModule {
@@ -73,6 +80,7 @@ export interface ModuleManifestEntry {
73
80
  readonly outFile: string;
74
81
  readonly skipFunctions?: readonly string[];
75
82
  readonly importsFrom?: string;
83
+ readonly sourceProvenance?: DocSourceProvenance;
76
84
  }
77
85
 
78
86
  export interface ResolveTargetOptions {
@@ -95,7 +103,7 @@ export async function resolveTargetModules(
95
103
  if (source == null) {
96
104
  return loadTargetModules(target, opts.packageRoot);
97
105
  }
98
- const { zip } = await resolveRefDoc({
106
+ const { zip, provenance } = await resolveRefDoc({
99
107
  version: source.version,
100
108
  cacheDir: opts.cacheDir ?? refDocCacheDir(),
101
109
  ...(opts.download ? { download: opts.download } : {}),
@@ -114,6 +122,7 @@ export async function resolveTargetModules(
114
122
  doc: JSON.parse(zip.read(sync.zipEntry)),
115
123
  outFile: module.outFile,
116
124
  importsFrom: target.coreTypesImport,
125
+ sourceProvenance: provenance,
117
126
  };
118
127
  return module.skipFunctions ? { ...entry, skipFunctions: module.skipFunctions } : entry;
119
128
  });
@@ -159,6 +168,7 @@ export function collectConstantFqns(
159
168
 
160
169
  export interface GenerateOptions {
161
170
  knownConstantFqns?: ReadonlySet<string>;
171
+ translations?: TranslationStore;
162
172
  }
163
173
 
164
174
  export function generateModuleDeclaration(
@@ -178,7 +188,8 @@ export function generateModuleDeclaration(
178
188
  return true;
179
189
  });
180
190
  const knownConstantFqns = options?.knownConstantFqns ?? collectConstantFqns();
181
- const emitted = emitDeclarations(module, { knownConstantFqns });
191
+ const translations = options?.translations ?? loadTranslations();
192
+ const emitted = emitDeclarations(module, { knownConstantFqns, translations });
182
193
  const contents = wrapAsAmbientGlobal({
183
194
  namespace: module.namespace,
184
195
  emitted,
@@ -208,6 +219,7 @@ export const RESTRICTED_NAMESPACES: Readonly<Record<string, string>> = {
208
219
 
209
220
  const UNIVERSAL_EXTRA_IMPORTS: readonly string[] = [
210
221
  "../builtin-messages",
222
+ "../../src/engine-globals",
211
223
  "../../src/msg-overloads",
212
224
  "../../src/message-guard",
213
225
  "../../src/go-overloads",
@@ -216,12 +228,13 @@ const UNIVERSAL_EXTRA_IMPORTS: readonly string[] = [
216
228
  export interface KindManifestEntry {
217
229
  readonly kind: string;
218
230
  readonly restricted?: string;
231
+ readonly factory: string;
219
232
  }
220
233
 
221
234
  export const KIND_MODULE_MANIFEST: readonly KindManifestEntry[] = [
222
- { kind: "script" },
223
- { kind: "gui-script", restricted: "gui" },
224
- { 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" },
225
238
  ];
226
239
 
227
240
  export function generateKindIndex(kind: string): string {
@@ -230,11 +243,11 @@ export function generateKindIndex(kind: string): string {
230
243
  const universalNamespaces = MODULE_MANIFEST.filter(
231
244
  (m) => !Object.hasOwn(RESTRICTED_NAMESPACES, m.namespace),
232
245
  ).map((m) => `../${m.outFile.replace(/\.d\.ts$/, "")}`);
233
- const lines = [...UNIVERSAL_EXTRA_IMPORTS, ...universalNamespaces]
234
- .sort()
235
- .map((path) => `import "${path}";`);
246
+ const lines = [
247
+ ...new Set([...universalNamespaces.sort(), ...[...UNIVERSAL_EXTRA_IMPORTS].sort()]),
248
+ ].map((path) => `import "${path}";`);
236
249
  if (entry.restricted) lines.push(`import "../${entry.restricted}";`);
237
- 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`;
238
251
  }
239
252
 
240
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
  }
@@ -73,6 +73,7 @@ export interface DocCommentParts {
73
73
  params?: { name: string; doc: string }[];
74
74
  returns?: string;
75
75
  example?: string;
76
+ exampleLang?: "lua" | "ts";
76
77
  }
77
78
 
78
79
  /**
@@ -115,7 +116,7 @@ export function renderDocComment(parts: DocCommentParts): string[] {
115
116
  }
116
117
  if (example !== "") {
117
118
  lines.push(" * @example");
118
- lines.push(" * ```lua");
119
+ lines.push(` * \`\`\`${parts.exampleLang ?? "lua"}`);
119
120
  for (const line of example.split("\n")) {
120
121
  lines.push(line === "" ? " *" : ` * ${line}`);
121
122
  }