@defold-typescript/types 0.4.2 → 0.5.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.
@@ -1,13 +1,63 @@
1
1
  /** @noSelfInFile */
2
2
  declare global {
3
3
  namespace webview {
4
+ /**
5
+ * 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.
6
+ * On iOS, the callback will never get a `webview.CALLBACK_RESULT_EVAL_ERROR`, due to the iOS SDK implementation."
7
+ *
8
+ * @param callback - A callback which receives info about finished requests taking the following parameters:
9
+ */
4
10
  function create(callback: (...args: unknown[]) => unknown): void;
11
+ /**
12
+ * Destroys an instance of a webview.
13
+ *
14
+ * @param webview_id - The webview id (returned by the `webview.create()` call)
15
+ */
5
16
  function destroy(webview_id: number): void;
17
+ /**
18
+ * Evaluates JavaScript within the context of the currently loaded page (if any). Once the request is done, the callback (registered in `webview.create()`) is invoked. The callback will get the result in the `data["result"]` field.
19
+ *
20
+ * @param webview_id - The webview id
21
+ * @param code - The JavaScript code to evaluate
22
+ */
6
23
  function eval(webview_id: number, code: string): void;
24
+ /**
25
+ * Returns the visibility state of the webview.
26
+ *
27
+ * @param webview_id - The webview id
28
+ */
7
29
  function is_visible(webview_id: number): void;
30
+ /**
31
+ * Opens a web page in the webview, using HTML data. Once the request is done, the callback (registered in `webview.create()`) is invoked.
32
+ *
33
+ * @param webview_id - The webview id
34
+ * @param html - The HTML data to display
35
+ * @param options - A table of options for the request. See `webview.open()`
36
+ */
8
37
  function open_raw(webview_id: number, html: string, options: Record<string | number, unknown>): void;
38
+ /**
39
+ * Sets the position and size of the webview
40
+ *
41
+ * @param webview_id - The webview id
42
+ * @param x - The x position of the webview
43
+ * @param y - The y position of the webview
44
+ * @param width - The width of the webview (-1 to match screen width)
45
+ * @param height - The height of the webview (-1 to match screen height)
46
+ */
9
47
  function set_position(webview_id: number, x: number, y: number, width: number, height: number): void;
48
+ /**
49
+ * Set transparency of webview background
50
+ *
51
+ * @param webview_id - The webview id
52
+ * @param transparent - If `true`, the webview background becomes transparent, otherwise opaque.
53
+ */
10
54
  function set_transparent(webview_id: number, transparent: boolean): void;
55
+ /**
56
+ * Shows or hides a webview
57
+ *
58
+ * @param webview_id - The webview id
59
+ * @param visible - If `0`, hides the webview. If non zero, shows the view
60
+ */
11
61
  function set_visible(webview_id: number, visible: number): void;
12
62
  }
13
63
  }
@@ -3,24 +3,165 @@ import type { Opaque } from "../src/core-types";
3
3
 
4
4
  declare global {
5
5
  namespace window {
6
+ /**
7
+ * Dimming mode is used to control whether or not a mobile device should dim the screen after a period without user interaction.
8
+ */
6
9
  const DIMMING_OFF: number & { readonly __brand: "window.DIMMING_OFF" };
10
+ /**
11
+ * Dimming mode is used to control whether or not a mobile device should dim the screen after a period without user interaction.
12
+ */
7
13
  const DIMMING_ON: number & { readonly __brand: "window.DIMMING_ON" };
14
+ /**
15
+ * Dimming mode is used to control whether or not a mobile device should dim the screen after a period without user interaction.
16
+ * This mode indicates that the dim mode can't be determined, or that the platform doesn't support dimming.
17
+ */
8
18
  const DIMMING_UNKNOWN: number & { readonly __brand: "window.DIMMING_UNKNOWN" };
19
+ /**
20
+ * This event is sent to a window event listener when the game window or app screen is
21
+ * restored after being iconified.
22
+ */
9
23
  const WINDOW_EVENT_DEICONIFIED: number & { readonly __brand: "window.WINDOW_EVENT_DEICONIFIED" };
24
+ /**
25
+ * This event is sent to a window event listener when the game window or app screen has
26
+ * gained focus.
27
+ * This event is also sent at game startup and the engine gives focus to the game.
28
+ */
10
29
  const WINDOW_EVENT_FOCUS_GAINED: number & { readonly __brand: "window.WINDOW_EVENT_FOCUS_GAINED" };
30
+ /**
31
+ * This event is sent to a window event listener when the game window or app screen has lost focus.
32
+ */
11
33
  const WINDOW_EVENT_FOCUS_LOST: number & { readonly __brand: "window.WINDOW_EVENT_FOCUS_LOST" };
34
+ /**
35
+ * This event is sent to a window event listener when the game window or app screen is
36
+ * iconified (reduced to an application icon in a toolbar, application tray or similar).
37
+ */
12
38
  const WINDOW_EVENT_ICONFIED: number & { readonly __brand: "window.WINDOW_EVENT_ICONFIED" };
39
+ /**
40
+ * This event is sent to a window event listener when the game window or app screen is resized.
41
+ * The new size is passed along in the data field to the event listener.
42
+ */
13
43
  const WINDOW_EVENT_RESIZED: number & { readonly __brand: "window.WINDOW_EVENT_RESIZED" };
44
+ /**
45
+ * Returns the current dimming mode set on a mobile device.
46
+ * The dimming mode specifies whether or not a mobile device should dim the screen after a period without user interaction.
47
+ * On platforms that does not support dimming, `window.DIMMING_UNKNOWN` is always returned.
48
+ *
49
+ * @returns The mode for screen dimming
50
+ - `window.DIMMING_UNKNOWN`
51
+ - `window.DIMMING_ON`
52
+ - `window.DIMMING_OFF`
53
+ */
14
54
  function get_dim_mode(): Opaque<"constant">;
55
+ /**
56
+ * This returns the content scale of the current display.
57
+ *
58
+ * @returns The display scale
59
+ */
15
60
  function get_display_scale(): number;
61
+ /**
62
+ * This returns the current lock state of the mouse cursor
63
+ *
64
+ * @returns The lock state
65
+ */
16
66
  function get_mouse_lock(): boolean;
67
+ /**
68
+ * This returns the safe area rectangle (x, y, width, height) and the inset
69
+ * values relative to the window edges. On platforms without a safe area,
70
+ * this returns the full window size and zero insets.
71
+ *
72
+ * @returns safe area data
73
+ `safe_area`
74
+ table table containing these keys:
75
+ - number `x`
76
+ - number `y`
77
+ - number `width`
78
+ - number `height`
79
+ - number `inset_left`
80
+ - number `inset_top`
81
+ - number `inset_right`
82
+ - number `inset_bottom`
83
+ */
17
84
  function get_safe_area(): { safe_area: { x: number; y: number; width: number; height: number; inset_left: number; inset_top: number; inset_right: number; inset_bottom: number } };
85
+ /**
86
+ * This returns the current window size (width and height).
87
+ */
18
88
  function get_size(): LuaMultiReturn<[number, number]>;
89
+ /**
90
+ * Sets the dimming mode on a mobile device.
91
+ * The dimming mode specifies whether or not a mobile device should dim the screen after a period without user interaction. The dimming mode will only affect the mobile device while the game is in focus on the device, but not when the game is running in the background.
92
+ * This function has no effect on platforms that does not support dimming.
93
+ *
94
+ * @param mode - The mode for screen dimming
95
+ - `window.DIMMING_ON`
96
+ - `window.DIMMING_OFF`
97
+ */
19
98
  function set_dim_mode(mode: Opaque<"constant">): void;
99
+ /**
100
+ * Sets a window event listener. Only one window event listener can be set at a time.
101
+ *
102
+ * @param callback - A callback which receives info about window events. Pass an empty function or `nil` if you no longer wish to receive callbacks.
103
+ `self`
104
+ object The calling script
105
+ `event`
106
+ constant The type of event. Can be one of these:
107
+ - `window.WINDOW_EVENT_FOCUS_LOST`
108
+ - `window.WINDOW_EVENT_FOCUS_GAINED`
109
+ - `window.WINDOW_EVENT_RESIZED`
110
+ - `window.WINDOW_EVENT_ICONIFIED`
111
+ - `window.WINDOW_EVENT_DEICONIFIED`
112
+ `data`
113
+ table The callback value `data` is a table which currently holds these values
114
+ - number `width`: The width of a resize event. nil otherwise.
115
+ - number `height`: The height of a resize event. nil otherwise.
116
+ * @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
131
+ *
132
+ * function init(self)
133
+ * window.set_listener(window_callback)
134
+ * end
135
+ * ```
136
+ */
20
137
  function set_listener(callback?: (self: unknown, event: unknown, data: unknown) => void): void;
138
+ /**
139
+ * Set the locking state for current mouse cursor on a PC platform.
140
+ * This function locks or unlocks the mouse cursor to the center point of the window. While the cursor is locked,
141
+ * mouse position updates will still be sent to the scripts as usual.
142
+ *
143
+ * @param flag - The lock state for the mouse cursor
144
+ */
21
145
  function set_mouse_lock(flag: boolean): void;
146
+ /**
147
+ * Sets the window position.
148
+ *
149
+ * @param x - Horizontal position of window
150
+ * @param y - Vertical position of window
151
+ */
22
152
  function set_position(x: number, y: number): void;
153
+ /**
154
+ * Sets the window size. Works on desktop platforms only.
155
+ *
156
+ * @param width - Width of window
157
+ * @param height - Height of window
158
+ */
23
159
  function set_size(width: number, height: number): void;
160
+ /**
161
+ * Sets the window title. Works on desktop platforms.
162
+ *
163
+ * @param title - The title, encoded as UTF-8
164
+ */
24
165
  function set_title(title: string): void;
25
166
  }
26
167
  }
@@ -1,7 +1,35 @@
1
1
  /** @noSelfInFile */
2
2
  declare global {
3
3
  namespace zlib {
4
+ /**
5
+ * A lua error is raised is on error
6
+ *
7
+ * @param buf - buffer to deflate
8
+ * @returns deflated buffer
9
+ * @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 ...
18
+ * ```
19
+ */
4
20
  function deflate(buf: string): string;
21
+ /**
22
+ * A lua error is raised is on error
23
+ *
24
+ * @param buf - buffer to inflate
25
+ * @returns inflated buffer
26
+ * @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.
31
+ * ```
32
+ */
5
33
  function inflate(buf: string): string;
6
34
  }
7
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/types",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "TypeScript types for the Defold engine's Lua APIs.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/api-doc.ts CHANGED
@@ -12,6 +12,8 @@ export interface ApiModule {
12
12
  export interface ApiProperty {
13
13
  name: string;
14
14
  types: string[];
15
+ brief: string;
16
+ description: string;
15
17
  }
16
18
 
17
19
  export interface ApiTypedef {
@@ -30,6 +32,7 @@ export interface ApiFunction {
30
32
  description: string;
31
33
  parameters: ApiParameter[];
32
34
  returnValues: ApiParameter[];
35
+ examples?: string;
33
36
  }
34
37
 
35
38
  export interface ApiParameter {
@@ -99,7 +102,12 @@ function parseProperty(element: Record<string, unknown>): ApiProperty {
99
102
  .map((t) => t.trim())
100
103
  .filter((t) => t.length > 0)
101
104
  : [];
102
- return { name: stringOr(element.name, ""), types };
105
+ return {
106
+ name: stringOr(element.name, ""),
107
+ types,
108
+ brief,
109
+ description: stringOr(element.description, ""),
110
+ };
103
111
  }
104
112
 
105
113
  function parseConstant(element: Record<string, unknown>): ApiConstant {
@@ -117,6 +125,7 @@ function parseFunction(element: Record<string, unknown>): ApiFunction {
117
125
  description: stringOr(element.description, ""),
118
126
  parameters: parseParameterList(element.parameters),
119
127
  returnValues: parseParameterList(element.returnvalues),
128
+ examples: stringOr(element.examples, ""),
120
129
  };
121
130
  }
122
131
 
@@ -0,0 +1,119 @@
1
+ const NAMED_ENTITIES: Record<string, string> = {
2
+ "&lt;": "<",
3
+ "&gt;": ">",
4
+ "&quot;": '"',
5
+ "&#39;": "'",
6
+ "&apos;": "'",
7
+ };
8
+
9
+ function decodeEntities(text: string): string {
10
+ let out = text;
11
+ for (const [entity, char] of Object.entries(NAMED_ENTITIES)) {
12
+ out = out.split(entity).join(char);
13
+ }
14
+ out = out.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)));
15
+ // `&amp;` last so an already-decoded `&` is never re-interpreted.
16
+ return out.split("&amp;").join("&");
17
+ }
18
+
19
+ /**
20
+ * Convert a ref-doc HTML fragment to clean Markdown/plain text suitable for a
21
+ * JSDoc comment body. Pure and free of any `ApiModule` dependency so the emit
22
+ * slices and any future surface can reuse it.
23
+ */
24
+ export function htmlToDocText(html: string): string {
25
+ let text = html
26
+ .replace(/<code>([\s\S]*?)<\/code>/gi, "`$1`")
27
+ .replace(/<(?:em|i)>([\s\S]*?)<\/(?:em|i)>/gi, "*$1*")
28
+ .replace(/<a\b[^>]*>([\s\S]*?)<\/a>/gi, "$1")
29
+ .replace(/<li>/gi, "\n- ")
30
+ .replace(/<\/li>/gi, "")
31
+ .replace(/<br\s*\/?>/gi, "\n")
32
+ .replace(/<\/?pre>/gi, "\n")
33
+ .replace(/<[^>]+>/g, "");
34
+
35
+ text = decodeEntities(text);
36
+
37
+ // Collapse horizontal whitespace runs, trim around newlines, drop blank
38
+ // runs, then trim the whole string — preserving single newlines from lists
39
+ // and `<br>`.
40
+ text = text
41
+ .replace(/[ \t]+/g, " ")
42
+ .replace(/ *\n */g, "\n")
43
+ .replace(/\n{2,}/g, "\n")
44
+ .trim();
45
+
46
+ // A literal `*/` would close the JSDoc comment early; escape it.
47
+ return text.split("*/").join("*\\/");
48
+ }
49
+
50
+ /**
51
+ * Convert a syntax-highlighted ref-doc code fragment (the `examples` field's
52
+ * `<div class="codehilite">…</div>` markup) to plain source. Unlike
53
+ * `htmlToDocText` this preserves line structure and indentation — collapsing or
54
+ * trimming per line would make the sample unreadable — stripping only highlight
55
+ * markup, per-line trailing whitespace, and surrounding blank lines, and folding
56
+ * runs of blank lines to one. Returns `""` for empty / whitespace-only input.
57
+ */
58
+ export function htmlToCodeText(html: string): string {
59
+ let text = html.replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "");
60
+ text = decodeEntities(text);
61
+
62
+ const lines = text.split("\n").map((line) => line.replace(/[ \t]+$/g, ""));
63
+ while (lines.length > 0 && lines[0] === "") lines.shift();
64
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
65
+ const collapsed = lines.join("\n").replace(/\n{3,}/g, "\n\n");
66
+
67
+ // A literal `*/` inside sample code would close the JSDoc comment early.
68
+ return collapsed.split("*/").join("*\\/");
69
+ }
70
+
71
+ export interface DocCommentParts {
72
+ summary: string;
73
+ params?: { name: string; doc: string }[];
74
+ returns?: string;
75
+ example?: string;
76
+ }
77
+
78
+ /**
79
+ * Build the JSDoc line array (no indentation) for the given parts. Returns `[]`
80
+ * when there is nothing to document so the caller can emit nothing.
81
+ */
82
+ export function renderDocComment(parts: DocCommentParts): string[] {
83
+ const summaryLines = parts.summary.trim() === "" ? [] : parts.summary.split("\n");
84
+ const params = (parts.params ?? []).filter((p) => p.doc.trim() !== "");
85
+ const returns = parts.returns?.trim() ? parts.returns : "";
86
+ const example = parts.example?.trim() ? parts.example : "";
87
+
88
+ if (summaryLines.length === 0 && params.length === 0 && returns === "" && example === "") {
89
+ return [];
90
+ }
91
+
92
+ const lines = ["/**"];
93
+ for (const line of summaryLines) {
94
+ lines.push(` * ${line}`);
95
+ }
96
+
97
+ const hasTags = params.length > 0 || returns !== "" || example !== "";
98
+ if (summaryLines.length > 0 && hasTags) {
99
+ lines.push(" *");
100
+ }
101
+
102
+ for (const param of params) {
103
+ lines.push(` * @param ${param.name} - ${param.doc}`);
104
+ }
105
+ if (returns !== "") {
106
+ lines.push(` * @returns ${returns}`);
107
+ }
108
+ if (example !== "") {
109
+ lines.push(" * @example");
110
+ lines.push(" * ```lua");
111
+ for (const line of example.split("\n")) {
112
+ lines.push(line === "" ? " *" : ` * ${line}`);
113
+ }
114
+ lines.push(" * ```");
115
+ }
116
+
117
+ lines.push(" */");
118
+ return lines;
119
+ }
package/src/emit-dts.ts CHANGED
@@ -7,6 +7,12 @@ import type {
7
7
  ApiVariable,
8
8
  } from "./api-doc";
9
9
  import { DEFOLD_TYPE_MAP } from "./core-types";
10
+ import {
11
+ type DocCommentParts,
12
+ htmlToCodeText,
13
+ htmlToDocText,
14
+ renderDocComment,
15
+ } from "./doc-comment";
10
16
 
11
17
  export interface EmitOptions {
12
18
  mapType?: (defoldType: string) => string;
@@ -473,6 +479,9 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
473
479
  lines.push(`${INDENT}${decl}type ${t.name} = Opaque<"${t.name}">;`);
474
480
  }
475
481
  for (const c of constants) {
482
+ for (const docLine of summaryDocLines(c.original.brief, c.original.description, INDENT)) {
483
+ lines.push(docLine);
484
+ }
476
485
  lines.push(`${INDENT}${decl}const ${c.name}: ${brandType(c.fqn)};`);
477
486
  }
478
487
  const aliases: { internal: string; public: string }[] = [];
@@ -483,11 +492,17 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
483
492
  const reserved = TS_RESERVED_NAMES.has(v.name);
484
493
  const emitName = aliasName(v.name, aliases);
485
494
  const line = emitVariable(v, emitName, mapType);
486
- if (line !== null) lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
495
+ if (line !== null) {
496
+ for (const docLine of summaryDocLines(v.original.brief, v.original.description, INDENT)) {
497
+ lines.push(docLine);
498
+ }
499
+ lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
500
+ }
487
501
  }
488
502
  for (const fn of functions) {
489
503
  const reserved = TS_RESERVED_NAMES.has(fn.name);
490
504
  const emitName = aliasName(fn.name, aliases);
505
+ for (const docLine of functionDocLines(fn.original)) lines.push(docLine);
491
506
  const line = emitFunction(fn, emitName, mapType, resolver);
492
507
  lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
493
508
  }
@@ -500,6 +515,9 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
500
515
  const members = [...module.properties].sort((a, b) => a.name.localeCompare(b.name));
501
516
  lines.push(`${INDENT}${decl}interface properties {`);
502
517
  for (const p of members) {
518
+ for (const docLine of summaryDocLines(p.brief, p.description, `${INDENT}${INDENT}`)) {
519
+ lines.push(docLine);
520
+ }
503
521
  lines.push(`${INDENT}${INDENT}${emitPropertyMember(p, mapType)}`);
504
522
  }
505
523
  lines.push(`${INDENT}}`);
@@ -512,6 +530,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
512
530
  interface PreparedConstant {
513
531
  name: string;
514
532
  fqn: string;
533
+ original: ApiConstant;
515
534
  }
516
535
 
517
536
  interface PreparedFunction {
@@ -533,7 +552,7 @@ function prepareFunction(fn: ApiFunction, prefix: string): PreparedFunction | nu
533
552
  function prepareConstant(c: ApiConstant, prefix: string): PreparedConstant | null {
534
553
  const stripped = stripPrefix(c.name, prefix);
535
554
  if (!TS_IDENTIFIER.test(stripped)) return null;
536
- return { name: stripped, fqn: c.name };
555
+ return { name: stripped, fqn: c.name, original: c };
537
556
  }
538
557
 
539
558
  function brandType(fqn: string): string {
@@ -589,6 +608,48 @@ function emitFunction(
589
608
  return `function ${name}(${params}): ${ret.type};${ret.trailing}`;
590
609
  }
591
610
 
611
+ // Build the indented JSDoc lines for a function from its ref-doc prose. The
612
+ // summary prefers the full `description`, falling back to the one-line `brief`;
613
+ // each `@param` name is the parameter's *emitted* name (the `arg<index>`
614
+ // fallback applies to non-identifier names, matching `emitParameter`) so the tag
615
+ // resolves on hover; a single documented return becomes `@returns`. Returns `[]`
616
+ // for a fully-undocumented function, leaving its emission byte-identical.
617
+ function functionDocLines(fn: ApiFunction): string[] {
618
+ const params = fn.parameters.map((p, index) => ({
619
+ name: TS_IDENTIFIER.test(p.name) ? p.name : `arg${index}`,
620
+ doc: htmlToDocText(p.doc),
621
+ }));
622
+ const onlyReturn = fn.returnValues.length === 1 ? fn.returnValues[0] : undefined;
623
+ const example = htmlToCodeText(fn.examples ?? "");
624
+ const parts: DocCommentParts = {
625
+ summary: htmlToDocText(summaryFor(fn.brief, fn.description)),
626
+ params,
627
+ ...(onlyReturn ? { returns: htmlToDocText(onlyReturn.doc) } : {}),
628
+ ...(example !== "" ? { example } : {}),
629
+ };
630
+ return indentDocLines(parts, INDENT);
631
+ }
632
+
633
+ // Summary-only doc lines for a member that carries no params or returns
634
+ // (constants, variables, `properties` members). Reuses the same renderer and
635
+ // summary precedence as functions; returns `[]` for an undocumented member so
636
+ // its emission stays byte-identical. `indent` matches the member's own
637
+ // indentation depth (one level inside the namespace, two inside `properties`).
638
+ function summaryDocLines(brief: string, description: string, indent: string): string[] {
639
+ return indentDocLines({ summary: htmlToDocText(summaryFor(brief, description)) }, indent);
640
+ }
641
+
642
+ function indentDocLines(parts: DocCommentParts, indent: string): string[] {
643
+ return renderDocComment(parts).map((line) => `${indent}${line}`);
644
+ }
645
+
646
+ // Prefer the full `description`; fall back to the one-line `brief` when prose is
647
+ // absent. Shared by every documented member kind so the summary source is
648
+ // consistent across functions, constants, variables, and properties.
649
+ function summaryFor(brief: string, description: string): string {
650
+ return description.trim() !== "" ? description : brief;
651
+ }
652
+
592
653
  function isDocOptional(p: ApiParameter): boolean {
593
654
  return p.isOptional || p.types.includes("nil");
594
655
  }
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export {
10
10
  type Vector3,
11
11
  type Vector4,
12
12
  } from "./core-types";
13
+ export { type DocCommentParts, htmlToDocText, renderDocComment } from "./doc-comment";
13
14
  export { type EmitOptions, emitDeclarations } from "./emit-dts";
14
15
  export {
15
16
  defineGuiScript,
package/src/lifecycle.ts CHANGED
@@ -1,9 +1,5 @@
1
1
  import type { Hash, Url } from "./core-types";
2
2
 
3
- type OnMessagePayload<K> = K extends BuiltinMessageId
4
- ? BuiltinMessages[K]
5
- : Record<string | number, unknown>;
6
-
7
3
  export interface InputTouch {
8
4
  id?: number;
9
5
  pressed?: boolean;
@@ -46,10 +42,15 @@ export interface InputAction {
46
42
  export interface ScriptHooks<TSelf> {
47
43
  init?(self: TSelf): void;
48
44
  update?(self: TSelf, dt: number): void;
49
- on_message?<K extends string>(
45
+ fixed_update?(self: TSelf, dt: number): void;
46
+ late_update?(self: TSelf, dt: number): void;
47
+ // Defold delivers message_id as a pre-hashed `hash`, so handlers must compare
48
+ // it against `hash("...")` constants — a string literal never matches. Sender-
49
+ // side payload narrowing by message id lives on `msg.post` (msg-overloads.d.ts).
50
+ on_message?(
50
51
  self: TSelf,
51
- message_id: K,
52
- message: OnMessagePayload<K>,
52
+ message_id: Hash,
53
+ message: Record<string | number, unknown>,
53
54
  sender: Url,
54
55
  ): void;
55
56
  on_input?(
@@ -62,6 +63,26 @@ export interface ScriptHooks<TSelf> {
62
63
  on_reload?(self: TSelf): void;
63
64
  }
64
65
 
66
+ export const SCRIPT_HOOK_NAMES = [
67
+ "init",
68
+ "update",
69
+ "fixed_update",
70
+ "late_update",
71
+ "on_message",
72
+ "on_input",
73
+ "final",
74
+ "on_reload",
75
+ ] as const;
76
+
77
+ export type ScriptHookName = (typeof SCRIPT_HOOK_NAMES)[number];
78
+
79
+ type Equal<A, B> =
80
+ (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
81
+
82
+ // drift-pin: SCRIPT_HOOK_NAMES must list exactly the ScriptHooks members
83
+ const _hookNamesPinnedToInterface: Equal<ScriptHookName, keyof ScriptHooks<unknown>> = true;
84
+ void _hookNamesPinnedToInterface;
85
+
65
86
  export type GuiScriptHooks<TSelf> = ScriptHooks<TSelf>;
66
87
 
67
88
  export type RenderScriptHooks<TSelf> = Omit<ScriptHooks<TSelf>, "on_input">;