@defold-typescript/types 0.5.4 → 0.5.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/types",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "TypeScript types for the Defold engine's Lua APIs.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
+ }
package/scripts/regen.ts CHANGED
@@ -4,8 +4,10 @@ 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
9
  import { type DownloadRefDoc, refDocCacheDir, resolveRefDoc } from "./doc-source";
10
+ import { loadTranslations } from "./example-store-io";
9
11
  import { type readZip, SYNC_MANIFEST, type SyncManifestEntry } from "./sync-api-docs";
10
12
 
11
13
  export interface ApiTargetModule {
@@ -159,6 +161,7 @@ export function collectConstantFqns(
159
161
 
160
162
  export interface GenerateOptions {
161
163
  knownConstantFqns?: ReadonlySet<string>;
164
+ translations?: TranslationStore;
162
165
  }
163
166
 
164
167
  export function generateModuleDeclaration(
@@ -178,7 +181,8 @@ export function generateModuleDeclaration(
178
181
  return true;
179
182
  });
180
183
  const knownConstantFqns = options?.knownConstantFqns ?? collectConstantFqns();
181
- const emitted = emitDeclarations(module, { knownConstantFqns });
184
+ const translations = options?.translations ?? loadTranslations();
185
+ const emitted = emitDeclarations(module, { knownConstantFqns, translations });
182
186
  const contents = wrapAsAmbientGlobal({
183
187
  namespace: module.namespace,
184
188
  emitted,
@@ -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
  }
package/src/emit-dts.ts CHANGED
@@ -13,6 +13,8 @@ import {
13
13
  htmlToDocText,
14
14
  renderDocComment,
15
15
  } from "./doc-comment";
16
+ import type { TranslationStore } from "./example-store";
17
+ import { hashExampleSource, lookupTranslation } from "./example-store";
16
18
 
17
19
  export interface EmitOptions {
18
20
  mapType?: (defoldType: string) => string;
@@ -21,6 +23,12 @@ export interface EmitOptions {
21
23
  // brands to the same FQN-keyed type its owning module's `const` emits,
22
24
  // instead of widening to `unknown`.
23
25
  knownConstantFqns?: ReadonlySet<string>;
26
+ // Hand-authored TypeScript `@example` translations keyed by element FQN.
27
+ // Defaults to an empty store (every example stays on its Lua fallback,
28
+ // byte-identical output); the build layer (`regen`) passes the loaded
29
+ // `examples/translations.json`. Loading lives in `scripts/example-store-io.ts`
30
+ // so this module stays node-free for downstream consumers.
31
+ translations?: TranslationStore;
24
32
  }
25
33
 
26
34
  export const TS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
@@ -427,6 +435,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
427
435
 
428
436
  const constantFqns = new Set(module.constants.map((c) => c.name));
429
437
  const knownConstantFqns = options?.knownConstantFqns;
438
+ const translations = options?.translations ?? {};
430
439
  const baseMapType = options?.mapType ?? defaultMapType;
431
440
  const mapType = (token: string): string =>
432
441
  constantFqns.has(token) || knownConstantFqns?.has(token)
@@ -502,7 +511,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
502
511
  for (const fn of functions) {
503
512
  const reserved = TS_RESERVED_NAMES.has(fn.name);
504
513
  const emitName = aliasName(fn.name, aliases);
505
- for (const docLine of functionDocLines(fn.original)) lines.push(docLine);
514
+ for (const docLine of functionDocLines(fn.original, translations)) lines.push(docLine);
506
515
  const line = emitFunction(fn, emitName, mapType, resolver);
507
516
  lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
508
517
  }
@@ -614,18 +623,23 @@ function emitFunction(
614
623
  // fallback applies to non-identifier names, matching `emitParameter`) so the tag
615
624
  // resolves on hover; a single documented return becomes `@returns`. Returns `[]`
616
625
  // for a fully-undocumented function, leaving its emission byte-identical.
617
- function functionDocLines(fn: ApiFunction): string[] {
626
+ function functionDocLines(fn: ApiFunction, translations: TranslationStore): string[] {
618
627
  const params = fn.parameters.map((p, index) => ({
619
628
  name: TS_IDENTIFIER.test(p.name) ? p.name : `arg${index}`,
620
629
  doc: htmlToDocText(p.doc),
621
630
  }));
622
631
  const onlyReturn = fn.returnValues.length === 1 ? fn.returnValues[0] : undefined;
623
- const example = htmlToCodeText(fn.examples ?? "");
632
+ const lua = htmlToCodeText(fn.examples ?? "");
633
+ // A hand-authored TS translation pinned to this exact Lua flips the fence to
634
+ // ```ts; any hash mismatch (or absent translation) keeps the Lua fallback.
635
+ const ts = lua === "" ? null : lookupTranslation(translations, fn.name, hashExampleSource(lua));
636
+ const exampleParts: Pick<DocCommentParts, "example" | "exampleLang"> =
637
+ ts !== null ? { example: ts, exampleLang: "ts" } : lua !== "" ? { example: lua } : {};
624
638
  const parts: DocCommentParts = {
625
639
  summary: htmlToDocText(summaryFor(fn.brief, fn.description)),
626
640
  params,
627
641
  ...(onlyReturn ? { returns: htmlToDocText(onlyReturn.doc) } : {}),
628
- ...(example !== "" ? { example } : {}),
642
+ ...exampleParts,
629
643
  };
630
644
  return indentDocLines(parts, INDENT);
631
645
  }
@@ -646,7 +660,7 @@ function indentDocLines(parts: DocCommentParts, indent: string): string[] {
646
660
  // Prefer the full `description`; fall back to the one-line `brief` when prose is
647
661
  // absent. Shared by every documented member kind so the summary source is
648
662
  // consistent across functions, constants, variables, and properties.
649
- function summaryFor(brief: string, description: string): string {
663
+ export function summaryFor(brief: string, description: string): string {
650
664
  return description.trim() !== "" ? description : brief;
651
665
  }
652
666
 
@@ -0,0 +1,44 @@
1
+ // A hand-authored TypeScript translation of one element's ref-doc `@example`,
2
+ // pinned by a hash of the exact source Lua it replaces. A ref-doc re-pin that
3
+ // changes the source Lua flips the hash, so a stale translation stops matching
4
+ // (drift guard) and the emit falls back to the Lua body.
5
+ export interface Translation {
6
+ sourceHash: string;
7
+ ts: string;
8
+ }
9
+
10
+ export type TranslationStore = Record<string, Translation>;
11
+
12
+ const FNV_OFFSET_BASIS = 0xcbf29ce484222325n;
13
+ const FNV_PRIME = 0x100000001b3n;
14
+ const U64_MASK = 0xffffffffffffffffn;
15
+
16
+ // A pure, dependency-free FNV-1a 64-bit hash over the source's UTF-16 code
17
+ // units, returned as zero-padded hex. Deliberately node- and Bun-free: this
18
+ // module is reachable from `index.ts` (via `emit-dts`), so a `node:crypto` or
19
+ // ambient-`Bun` reference here would fail type-checking in every downstream
20
+ // consumer that compiles the shipped `src/` graph.
21
+ //
22
+ // The input is the already-normalized post-`htmlToCodeText` string (per-line
23
+ // trailing whitespace and surrounding blank lines stripped), so the hash is
24
+ // independent of trailing whitespace in the original ref-doc HTML.
25
+ export function hashExampleSource(source: string): string {
26
+ let hash = FNV_OFFSET_BASIS;
27
+ for (let i = 0; i < source.length; i++) {
28
+ hash = ((hash ^ BigInt(source.charCodeAt(i))) * FNV_PRIME) & U64_MASK;
29
+ }
30
+ return hash.toString(16).padStart(16, "0");
31
+ }
32
+
33
+ // Return the stored TypeScript body only when the FQN exists and its pinned
34
+ // `sourceHash` matches the source we are about to emit; any mismatch returns
35
+ // `null` so the caller keeps the Lua fallback.
36
+ export function lookupTranslation(
37
+ store: TranslationStore,
38
+ fqn: string,
39
+ sourceHash: string,
40
+ ): string | null {
41
+ const entry = store[fqn];
42
+ if (!entry || entry.sourceHash !== sourceHash) return null;
43
+ return entry.ts;
44
+ }
@@ -9,6 +9,21 @@ declare global {
9
9
  keys?: Record<string | number, unknown>;
10
10
  }
11
11
 
12
+ /**
13
+ * gets a named property of the specified game object or component
14
+ *
15
+ * @param url - url of the game object or component having the property
16
+ * @param property - id of the property to retrieve
17
+ * @param options - optional options table
18
+ * - index number index into array property (1 based)
19
+ * - key hash name of internal property
20
+ * - keys table array of internal component resources identified by key (e.g. a particle fx emitter, see examples below)
21
+ * @returns the value of the specified property
22
+ * @example
23
+ * ```ts
24
+ * const position = go.get("#sprite", "position");
25
+ * ```
26
+ */
12
27
  function get<K extends keyof go.properties>(
13
28
  url: string | Hash | Url,
14
29
  property: K,
@@ -19,6 +34,21 @@ declare global {
19
34
  property: string | Hash,
20
35
  options?: GoPropertyOptions,
21
36
  ): number | boolean | Hash | Url | Vector3 | Vector4 | Quaternion | Opaque<"resource">;
37
+ /**
38
+ * sets a named property of the specified game object or component, or a material constant
39
+ *
40
+ * @param url - url of the game object or component having the property
41
+ * @param property - id of the property to set
42
+ * @param value - the value to set
43
+ * @param options - optional options table
44
+ * - index integer index into array property (1 based)
45
+ * - key hash name of internal property
46
+ * - keys table array of internal component resources identified by key (e.g. a particle fx emitter, see examples below)
47
+ * @example
48
+ * ```ts
49
+ * go.set("#sprite", "tint", vmath.vector4(1, 0, 0, 1));
50
+ * ```
51
+ */
22
52
  function set<K extends keyof go.properties>(
23
53
  url: string | Hash | Url,
24
54
  property: K,
@@ -10,6 +10,27 @@ declare global {
10
10
  // The transpiler lowers the call to a flat `message_id == hash("...")`
11
11
  // if/elseif chain (message-dispatch-lowering.ts), keeping this package free of
12
12
  // runtime Lua.
13
+ /**
14
+ * Discriminated-union dispatcher for `on_message`: takes a record of per-message
15
+ * handlers keyed by builtin message id, each receiving its `BuiltinMessages`
16
+ * payload already narrowed, and returns the `on_message` handler that routes to
17
+ * them. Reads as `on_message: onMessage<Self>({ ... })`; `self` threads via the
18
+ * explicit type argument, mirroring `defineScript<Self>`. The transpiler lowers
19
+ * it to a flat `if/elseif message_id == hash("...")` chain.
20
+ *
21
+ * @param handlers - a partial map from builtin message id to its handler; each handler's `message` is narrowed to that id's payload.
22
+ * @returns the `on_message` lifecycle handler that dispatches to the matching entry.
23
+ * @example
24
+ * ```ts
25
+ * defineScript({
26
+ * on_message: onMessage({
27
+ * contact_point_response(self, message, sender) {
28
+ * print(message.other_group);
29
+ * },
30
+ * }),
31
+ * });
32
+ * ```
33
+ */
13
34
  function onMessage<TSelf = Record<never, never>>(
14
35
  handlers: Partial<{
15
36
  [K in BuiltinMessageId]: (self: TSelf, message: BuiltinMessages[K], sender: Url) => void;
@@ -8,6 +8,25 @@ declare global {
8
8
  // untyped `message` record narrow to its `BuiltinMessages` payload. The
9
9
  // transpiler lowers the call to `message_id == hash("...")`
10
10
  // (message-guard-lowering.ts), keeping this package free of runtime Lua.
11
+ /**
12
+ * Type guard for an `on_message` handler: narrows the untyped `message` record
13
+ * to its `BuiltinMessages` payload when `message_id` matches a known builtin
14
+ * message id. The engine delivers `message_id` as a pre-hashed `Hash`, so this
15
+ * guard re-introduces the literal a discriminated union would otherwise need.
16
+ *
17
+ * @param message_id - the hashed message id `on_message` received.
18
+ * @param message - the untyped message record `on_message` received.
19
+ * @param expected - the builtin message id to test against (e.g. `"contact_point_response"`).
20
+ * @returns `true` when `message_id` matches `expected`, narrowing `message` to that payload.
21
+ * @example
22
+ * ```ts
23
+ * function on_message(this: void, message_id: Hash, message: object, sender: Url) {
24
+ * if (isMessage(message_id, message, "contact_point_response")) {
25
+ * print(message.other_group);
26
+ * }
27
+ * }
28
+ * ```
29
+ */
11
30
  function isMessage<K extends BuiltinMessageId>(
12
31
  message_id: Hash,
13
32
  message: Record<string | number, unknown>,
@@ -7,6 +7,26 @@ type MsgPostPayload<K> = K extends BuiltinMessageId
7
7
 
8
8
  declare global {
9
9
  namespace msg {
10
+ /**
11
+ * Post a message to a receiving URL. The most common case is to send messages
12
+ * to a component. If the component part of the receiver is omitted, the message
13
+ * is broadcast to all components in the game object.
14
+ * The following receiver shorthands are available:
15
+ * - `"."` the current game object
16
+ * - `"#"` the current component
17
+ * There is a 2 kilobyte limit to the message parameter table size.
18
+ *
19
+ * @param receiver - The receiver must be a string in URL-format, a URL object or a hashed string.
20
+ * @param message_id - The id must be a string or a hashed string.
21
+ * @param message - a lua table with message parameters to send.
22
+ * @example
23
+ * ```ts
24
+ * msg.post("#collisionobject", "apply_force", {
25
+ * force: vmath.vector3(0, 1000, 0),
26
+ * position: go.get_world_position(),
27
+ * });
28
+ * ```
29
+ */
10
30
  function post<K extends string>(
11
31
  receiver: string | Url | Hash,
12
32
  message_id: K,