@defold-typescript/types 0.35.0 → 0.36.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.
package/src/emit-dts.ts CHANGED
@@ -222,7 +222,9 @@ export const OVERLOAD_COVERED_SKIPS = new Set([
222
222
  // is the TSTL non-string-key Lua-table idiom and resolves the same way the
223
223
  // already-emitted `LuaMultiReturn` does. The audit consults this map too, so the
224
224
  // gate and the emitted surface stay coupled; a slot not curated here still
225
- // surfaces under `recordTables` until a human adds it.
225
+ // surfaces under `recordTables` until a human adds it. Key and value are both
226
+ // `A | B` union tokens split per token, so a key whose doc names constants
227
+ // brands each of them rather than widening to `number`.
226
228
  export const MAPPING_TABLE_SLOTS: ReadonlyMap<string, { key: string; value: string }> = new Map([
227
229
  ["gui.clone_tree", { key: "hash", value: "node" }],
228
230
  ["gui.get_tree", { key: "hash", value: "node" }],
@@ -408,6 +410,18 @@ export function documentedConstantTokens(text: string, universe: ReadonlySet<str
408
410
  return [...out];
409
411
  }
410
412
 
413
+ // The constants a `table` slot's doc names as the table's *keys*, as opposed to
414
+ // the far commoner shape where a field's *value* is a constant
415
+ // (`physics.get_shape`'s `type`, `resource.create_texture`'s `format`). Upstream
416
+ // marks the difference in prose — render.clear reads "table with keys specifying
417
+ // which buffers to clear ... Available keys are:" — so a key mention alongside a
418
+ // named constant is the discriminator. Empty means the slot has no documented
419
+ // constant keys, which is true of every `table` slot but one today.
420
+ export function documentedConstantKeyTokens(doc: string, universe: ReadonlySet<string>): string[] {
421
+ if (!/\bkeys?\b/i.test(doc.replace(/<[^>]*>/g, ""))) return [];
422
+ return documentedConstantTokens(doc, universe);
423
+ }
424
+
411
425
  type SlotDocLookup = (slotKey: string) => string | undefined;
412
426
 
413
427
  function constantSlotTokens(
@@ -830,11 +844,20 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
830
844
  // render.predicate's `tags` is "table of tags ... can be of either hash or
831
845
  // string type", a homogeneous array whose element is the string|hash union.
832
846
  ["render.predicate:param:tags", { kind: "array", element: ["string", "hash"] }],
833
- // render.clear's `buffers` maps the numeric graphics.BUFFER_* constants to
834
- // their clear value: vector4 for the color buffer, number for depth/stencil.
835
- // The mapping value is the `number | vector4` union; the mapping string branch
836
- // splits the union token, so the value emits `number | Vector4`.
837
- ["render.clear:param:buffers", { kind: "mapping", key: "number", value: "number | vector4" }],
847
+ // render.clear's `buffers` maps the graphics.BUFFER_TYPE_* constants its doc
848
+ // names as keys to their clear value: vector4 for the color buffer, number for
849
+ // depth/stencil. Both sides are union tokens the mapping string branch splits,
850
+ // so the key emits the three branded constants and the value `number | Vector4`.
851
+ // The key is branded rather than `number`, which is the narrowing users migrate
852
+ // to via the authored `render.ClearBufferKey` alias.
853
+ [
854
+ "render.clear:param:buffers",
855
+ {
856
+ kind: "mapping",
857
+ key: "graphics.BUFFER_TYPE_COLOR0_BIT | graphics.BUFFER_TYPE_DEPTH_BIT | graphics.BUFFER_TYPE_STENCIL_BIT",
858
+ value: "number | vector4",
859
+ },
860
+ ],
838
861
  // go.on_input and gui.on_input share the same well-known InputAction shape
839
862
  // (the on_input function description lists the well-known fields: value,
840
863
  // pressed, released, repeated, x/y/screen_x/screen_y/dx/dy/screen_dx/screen_dy,
@@ -1883,10 +1906,25 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
1883
1906
  return `${lines.join("\n")}\n`;
1884
1907
  }
1885
1908
 
1909
+ /**
1910
+ * Every documented slot's rendered TS type, keyed `param|return:<position>:<raw
1911
+ * ref-doc name>`. The position is part of the key because a multi-return can
1912
+ * carry several unnamed slots (`push.schedule` renders two), which a name-only
1913
+ * key would collapse into one. Each value appears verbatim inside the symbol's
1914
+ * own `tsSignature`, so a consumer rendering a slot's type beside that signature
1915
+ * cannot contradict it.
1916
+ */
1917
+ export type SlotTypes = Readonly<Record<string, string>>;
1918
+
1919
+ function slotKey(kind: "param" | "return", position: number, name: string): string {
1920
+ return `${kind}:${position}:${name}`;
1921
+ }
1922
+
1886
1923
  /** One emitted symbol's identity paired with its rendered TS signature text. */
1887
1924
  export interface SymbolSignature {
1888
1925
  readonly identity: ApiSymbolIdentity;
1889
1926
  readonly tsSignature: string;
1927
+ readonly slotTypes: SlotTypes;
1890
1928
  }
1891
1929
 
1892
1930
  /**
@@ -1931,9 +1969,14 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1931
1969
  });
1932
1970
  const emitName = (name: string): string => (TS_RESERVED_NAMES.has(name) ? `_${name}` : name);
1933
1971
 
1972
+ // A member with no call list (constant, variable, property, typedef) documents
1973
+ // no slot, so it reports an empty map rather than being absent from the ledger.
1974
+ const NO_SLOTS: SlotTypes = {};
1975
+
1934
1976
  for (const fn of module.functions) {
1935
1977
  const prepared = prepareFunction(fn, prefix);
1936
1978
  if (prepared === null) continue;
1979
+ const slotTypes: Record<string, string> = {};
1937
1980
  out.push({
1938
1981
  identity: fnIdentity(fn),
1939
1982
  tsSignature: emitFunction(
@@ -1943,7 +1986,9 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1943
1986
  resolver,
1944
1987
  constantTokens,
1945
1988
  urlParameters,
1989
+ slotTypes,
1946
1990
  ),
1991
+ slotTypes,
1947
1992
  });
1948
1993
  }
1949
1994
 
@@ -1957,9 +2002,11 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1957
2002
  signature: "",
1958
2003
  },
1959
2004
  tsSignature: emitVariable(v, emitName(v.name), mapType),
2005
+ slotTypes: NO_SLOTS,
1960
2006
  });
1961
2007
  }
1962
2008
  for (const fn of nested.functions) {
2009
+ const slotTypes: Record<string, string> = {};
1963
2010
  out.push({
1964
2011
  identity: fnIdentity(fn.original),
1965
2012
  tsSignature: emitFunction(
@@ -1969,16 +2016,27 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1969
2016
  resolver,
1970
2017
  constantTokens,
1971
2018
  urlParameters,
2019
+ slotTypes,
1972
2020
  ),
2021
+ slotTypes,
1973
2022
  });
1974
2023
  }
1975
2024
 
1976
2025
  const handleGroups = collectHandleMethodGroups(module);
1977
2026
  for (const group of handleGroups.values()) {
1978
2027
  for (const prepared of group) {
2028
+ const slotTypes: Record<string, string> = {};
1979
2029
  out.push({
1980
2030
  identity: fnIdentity(prepared.original),
1981
- tsSignature: emitMethod(prepared, mapType, resolver, constantTokens, urlParameters),
2031
+ tsSignature: emitMethod(
2032
+ prepared,
2033
+ mapType,
2034
+ resolver,
2035
+ constantTokens,
2036
+ urlParameters,
2037
+ slotTypes,
2038
+ ),
2039
+ slotTypes,
1982
2040
  });
1983
2041
  }
1984
2042
  }
@@ -1989,6 +2047,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1989
2047
  out.push({
1990
2048
  identity: { namespace: module.namespace, kind: "CONSTANT", name: c.name, signature: "" },
1991
2049
  tsSignature: `const ${prepared.name}: ${brandType(prepared.fqn)};`,
2050
+ slotTypes: NO_SLOTS,
1992
2051
  });
1993
2052
  }
1994
2053
 
@@ -1998,6 +2057,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1998
2057
  out.push({
1999
2058
  identity: { namespace: module.namespace, kind: "VARIABLE", name: v.name, signature: "" },
2000
2059
  tsSignature: emitVariable(prepared, emitName(prepared.name), mapType),
2060
+ slotTypes: NO_SLOTS,
2001
2061
  });
2002
2062
  }
2003
2063
 
@@ -2005,6 +2065,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2005
2065
  out.push({
2006
2066
  identity: { namespace: module.namespace, kind: "PROPERTY", name: p.name, signature: "" },
2007
2067
  tsSignature: emitPropertyMember(p, mapType, module.namespace),
2068
+ slotTypes: NO_SLOTS,
2008
2069
  });
2009
2070
  }
2010
2071
 
@@ -2013,6 +2074,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2013
2074
  out.push({
2014
2075
  identity: { namespace: module.namespace, kind: "TYPEDEF", name: t.name, signature: "" },
2015
2076
  tsSignature: `type ${t.name} = Opaque<"${t.name}">;`,
2077
+ slotTypes: NO_SLOTS,
2016
2078
  });
2017
2079
  }
2018
2080
 
@@ -2208,6 +2270,7 @@ function memberSignature(
2208
2270
  resolver: TableDocResolver,
2209
2271
  constantTokens: ConstantSlotTokens,
2210
2272
  urlParameters: UrlParameterTable,
2273
+ slotSink?: Record<string, string>,
2211
2274
  ): string {
2212
2275
  const original = prepared.original.parameters;
2213
2276
  const elementName = prepared.original.name;
@@ -2216,22 +2279,19 @@ function memberSignature(
2216
2279
  const positional = (varargIndex === -1 ? original : original.slice(0, varargIndex)).map((p, i) =>
2217
2280
  emitParameter(p, i, i >= cutoff, mapType, resolver, constantTokens, elementName, urlParameters),
2218
2281
  );
2219
- const params = (
2282
+ const rest =
2220
2283
  varargIndex === -1
2221
- ? positional
2222
- : [
2223
- ...positional,
2224
- emitRestParameter(
2225
- original,
2226
- varargIndex,
2227
- mapType,
2228
- resolver,
2229
- constantTokens,
2230
- elementName,
2231
- urlParameters,
2232
- ),
2233
- ]
2234
- ).join(", ");
2284
+ ? null
2285
+ : emitRestParameter(
2286
+ original,
2287
+ varargIndex,
2288
+ mapType,
2289
+ resolver,
2290
+ constantTokens,
2291
+ elementName,
2292
+ urlParameters,
2293
+ );
2294
+ const params = [...positional.map((p) => p.text), ...(rest ? [rest.text] : [])].join(", ");
2235
2295
  const ret = emitReturn(
2236
2296
  prepared.original.returnValues,
2237
2297
  mapType,
@@ -2241,12 +2301,28 @@ function memberSignature(
2241
2301
  );
2242
2302
  const predicateToken = TYPE_PREDICATES.get(elementName);
2243
2303
  const soleParam = original[0];
2244
- if (
2304
+ const isPredicate =
2245
2305
  predicateToken !== undefined &&
2246
2306
  soleParam !== undefined &&
2247
2307
  original.length === 1 &&
2248
- ret.type === "boolean"
2249
- ) {
2308
+ ret.type === "boolean";
2309
+ if (slotSink !== undefined) {
2310
+ for (const [i, p] of positional.entries()) {
2311
+ const raw = original[i];
2312
+ if (raw) slotSink[slotKey("param", i, raw.name)] = p.ts;
2313
+ }
2314
+ for (const slot of rest?.slots ?? []) {
2315
+ slotSink[slotKey("param", slot.position, slot.name)] = slot.ts;
2316
+ }
2317
+ // A predicate renders `arg is T` in place of the mapped `boolean`, so its
2318
+ // return slot has no text in the signature to stand on and is not reported.
2319
+ if (!isPredicate) {
2320
+ for (const slot of ret.slots) {
2321
+ slotSink[slotKey("return", slot.position, slot.name)] = slot.ts;
2322
+ }
2323
+ }
2324
+ }
2325
+ if (isPredicate && soleParam !== undefined && predicateToken !== undefined) {
2250
2326
  return `${name}(${params}): ${safeParamName(soleParam.name, 0)} is ${mapType(predicateToken)};`;
2251
2327
  }
2252
2328
  return `${name}(${params}): ${ret.type};${ret.trailing}`;
@@ -2259,8 +2335,9 @@ function emitFunction(
2259
2335
  resolver: TableDocResolver,
2260
2336
  constantTokens: ConstantSlotTokens,
2261
2337
  urlParameters: UrlParameterTable,
2338
+ slotSink?: Record<string, string>,
2262
2339
  ): string {
2263
- return `function ${memberSignature(prepared, name, mapType, resolver, constantTokens, urlParameters)}`;
2340
+ return `function ${memberSignature(prepared, name, mapType, resolver, constantTokens, urlParameters, slotSink)}`;
2264
2341
  }
2265
2342
 
2266
2343
  // A colon-method member of a handle interface: identical signature machinery to a
@@ -2272,8 +2349,17 @@ function emitMethod(
2272
2349
  resolver: TableDocResolver,
2273
2350
  constantTokens: ConstantSlotTokens,
2274
2351
  urlParameters: UrlParameterTable,
2352
+ slotSink?: Record<string, string>,
2275
2353
  ): string {
2276
- return memberSignature(prepared, prepared.name, mapType, resolver, constantTokens, urlParameters);
2354
+ return memberSignature(
2355
+ prepared,
2356
+ prepared.name,
2357
+ mapType,
2358
+ resolver,
2359
+ constantTokens,
2360
+ urlParameters,
2361
+ slotSink,
2362
+ );
2277
2363
  }
2278
2364
 
2279
2365
  // Build the indented JSDoc lines for a function from its ref-doc prose. The
@@ -2444,21 +2530,29 @@ function emitRestParameter(
2444
2530
  constantTokens: ConstantSlotTokens,
2445
2531
  elementName: string,
2446
2532
  urlParameters: UrlParameterTable,
2447
- ): string {
2533
+ ): { text: string; slots: EmittedSlot[] } {
2448
2534
  const vararg = params[varargIndex];
2449
- if (vararg === undefined) return "";
2450
- const members = [
2451
- ...new Set(
2452
- params
2453
- .slice(varargIndex)
2454
- .map((p) =>
2455
- parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters),
2456
- ),
2457
- ),
2458
- ];
2535
+ if (vararg === undefined) return { text: "", slots: [] };
2536
+ const folded = params.slice(varargIndex).map((p, offset) => ({
2537
+ position: varargIndex + offset,
2538
+ name: p.name,
2539
+ ts: parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters),
2540
+ }));
2541
+ const members = [...new Set(folded.map((slot) => slot.ts))];
2459
2542
  const first = members[0] ?? "unknown";
2460
2543
  const element = members.length > 1 ? `(${members.join(" | ")})` : first;
2461
- return `...${varargParamName(vararg.name, varargIndex)}: ${element}[]`;
2544
+ return {
2545
+ text: `...${varargParamName(vararg.name, varargIndex)}: ${element}[]`,
2546
+ slots: folded,
2547
+ };
2548
+ }
2549
+
2550
+ // A slot's rendered type paired with the position and raw ref-doc name that
2551
+ // identify it, so the signature text and the per-slot map come out of one call.
2552
+ interface EmittedSlot {
2553
+ readonly position: number;
2554
+ readonly name: string;
2555
+ readonly ts: string;
2462
2556
  }
2463
2557
 
2464
2558
  function emitParameter(
@@ -2470,7 +2564,7 @@ function emitParameter(
2470
2564
  constantTokens: ConstantSlotTokens,
2471
2565
  elementName: string,
2472
2566
  urlParameters: UrlParameterTable,
2473
- ): string {
2567
+ ): { text: string; ts: string } {
2474
2568
  const name = safeParamName(p.name, index);
2475
2569
  const ts = parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters);
2476
2570
  // An interior doc-optional param (a required param follows, so the trailing-`?`
@@ -2478,7 +2572,7 @@ function emitParameter(
2478
2572
  // lowers `undefined` to `nil`, the faithful call. Trailing optionals keep the
2479
2573
  // `?` form; required params are untouched.
2480
2574
  const interiorOptional = !optional && isDocOptional(p, elementName) ? " | undefined" : "";
2481
- return `${name}${optional ? "?" : ""}: ${ts}${interiorOptional}`;
2575
+ return { text: `${name}${optional ? "?" : ""}: ${ts}${interiorOptional}`, ts };
2482
2576
  }
2483
2577
 
2484
2578
  function emitReturn(
@@ -2487,33 +2581,42 @@ function emitReturn(
2487
2581
  resolver: TableDocResolver,
2488
2582
  constantTokens: ConstantSlotTokens,
2489
2583
  elementName: string,
2490
- ): { type: string; trailing: string } {
2584
+ ): { type: string; trailing: string; slots: EmittedSlot[] } {
2585
+ // An authored whole-return override is not a per-slot render, so it reports no
2586
+ // slots and leaves the consumer on its token-derived fallback.
2491
2587
  const override = RETURN_TYPE_OVERRIDES.get(elementName);
2492
- if (override !== undefined) return { type: override, trailing: "" };
2493
- if (returnValues.length === 0) return { type: "void", trailing: "" };
2588
+ if (override !== undefined) return { type: override, trailing: "", slots: [] };
2589
+ if (returnValues.length === 0) return { type: "void", trailing: "", slots: [] };
2494
2590
  if (returnValues.length > 1) {
2495
2591
  // Defold multi-returns are positional and always present; each slot maps
2496
2592
  // straight through (unknown when the doc lists no type) into a tuple that
2497
2593
  // typescript-to-lua erases to `local a, b = fn()`.
2498
- const slots = returnValues.map((rv) =>
2499
- rv.types.length > 0
2500
- ? mapSlotUnion(
2501
- rv.types,
2502
- rv.doc,
2503
- mapType,
2504
- false,
2505
- resolver,
2506
- constantTokens,
2507
- elementName,
2508
- "return",
2509
- rv.name,
2510
- )
2511
- : "unknown",
2512
- );
2513
- return { type: `LuaMultiReturn<[${slots.join(", ")}]>`, trailing: "" };
2594
+ const emitted = returnValues.map((rv, index) => ({
2595
+ position: index,
2596
+ name: rv.name,
2597
+ ts:
2598
+ rv.types.length > 0
2599
+ ? mapSlotUnion(
2600
+ rv.types,
2601
+ rv.doc,
2602
+ mapType,
2603
+ false,
2604
+ resolver,
2605
+ constantTokens,
2606
+ elementName,
2607
+ "return",
2608
+ rv.name,
2609
+ )
2610
+ : "unknown",
2611
+ }));
2612
+ return {
2613
+ type: `LuaMultiReturn<[${emitted.map((slot) => slot.ts).join(", ")}]>`,
2614
+ trailing: "",
2615
+ slots: emitted,
2616
+ };
2514
2617
  }
2515
2618
  const first = returnValues[0];
2516
- if (!first) return { type: "void", trailing: "" };
2619
+ if (!first) return { type: "void", trailing: "", slots: [] };
2517
2620
  const ts =
2518
2621
  first.types.length > 0
2519
2622
  ? mapSlotUnion(
@@ -2528,7 +2631,7 @@ function emitReturn(
2528
2631
  first.name,
2529
2632
  )
2530
2633
  : "unknown";
2531
- return { type: ts, trailing: "" };
2634
+ return { type: ts, trailing: "", slots: [{ position: 0, name: first.name, ts }] };
2532
2635
  }
2533
2636
 
2534
2637
  function emitPropertyMember(
@@ -2606,7 +2709,14 @@ function mapSlotUnion(
2606
2709
  const nested = mapping.value as NestedMapping;
2607
2710
  value = `LuaMap<${mapType(nested.key)}, ${mapType(nested.value)}>`;
2608
2711
  }
2609
- ts = `LuaMap<${mapType(mapping.key)}, ${value}>`;
2712
+ // A mapping key may be a `A | B` union token too (render.clear's three
2713
+ // documented graphics.BUFFER_TYPE_* constants); split it the same way
2714
+ // the value above is split. A single-token key (no `|`) is unaffected.
2715
+ const key = unionFromTokens(
2716
+ mapping.key.split("|").map((token) => token.trim()),
2717
+ mapType,
2718
+ );
2719
+ ts = `LuaMap<${key}, ${value}>`;
2610
2720
  } else if (element !== undefined) {
2611
2721
  ts = arrayTypeFromTokens(element, mapType);
2612
2722
  } else if (curation?.kind === "object" || curation?.kind === "array-object") {
package/src/index.ts CHANGED
@@ -49,6 +49,7 @@ export {
49
49
  type EmitOptions,
50
50
  emitDeclarations,
51
51
  emitSymbolSignatures,
52
+ type SlotTypes,
52
53
  type SymbolSignature,
53
54
  TS_IDENTIFIER,
54
55
  TS_RESERVED_NAMES,
@@ -4,6 +4,23 @@ import type { Opaque } from "./core-types";
4
4
 
5
5
  declare global {
6
6
  namespace render {
7
+ /**
8
+ * The buffer keys `render.clear` accepts: the three `graphics.BUFFER_TYPE_*`
9
+ * constants the reference names. Declare the clear table with this alias —
10
+ * `LuaMap`'s iterator makes its key parameter invariant, so a
11
+ * `LuaMap<number, ...>` is not assignable however its entries were set.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const buffers = new LuaMap<render.ClearBufferKey, number | Vector4>();
16
+ * buffers.set(graphics.BUFFER_TYPE_COLOR0_BIT, vmath.vector4(0, 0, 0, 0));
17
+ * render.clear(buffers);
18
+ * ```
19
+ */
20
+ type ClearBufferKey =
21
+ | typeof graphics.BUFFER_TYPE_COLOR0_BIT
22
+ | typeof graphics.BUFFER_TYPE_DEPTH_BIT
23
+ | typeof graphics.BUFFER_TYPE_STENCIL_BIT;
7
24
  /**
8
25
  * Creates a new render target according to the supplied specification table.
9
26
  * The parameter table is keyed by buffer type, with one parameter table per