@defold-typescript/types 0.9.0 → 0.10.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/index.d.ts CHANGED
@@ -34,6 +34,7 @@ import "./generated/push";
34
34
  import "./generated/render";
35
35
  import "./generated/resource";
36
36
  import "./generated/socket";
37
+ import "./src/socket-types";
37
38
  import "./generated/sound";
38
39
  import "./generated/sprite";
39
40
  import "./generated/sys";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/types",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "TypeScript types for the Defold engine's Lua APIs.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,6 +30,9 @@
30
30
  "./core-types": {
31
31
  "types": "./src/core-types.ts"
32
32
  },
33
+ "./lifecycle": {
34
+ "types": "./src/lifecycle.ts"
35
+ },
33
36
  "./timers": {
34
37
  "types": "./src/timers.d.ts"
35
38
  },
@@ -4,6 +4,7 @@ import {
4
4
  ARBITRARY_TABLE_SLOTS,
5
5
  applyNestedFieldCurations,
6
6
  buildTableDocResolver,
7
+ HANDLE_METHOD_LOCAL,
7
8
  HOMOGENEOUS_ARRAY_SLOTS,
8
9
  MAPPING_TABLE_SLOTS,
9
10
  type NestedMapping,
@@ -76,6 +77,26 @@ function trailingOptionalCutoff(params: readonly Record<string, unknown>[]): num
76
77
  return cutoff;
77
78
  }
78
79
 
80
+ // Colon `<receiver>:<method>` FUNCTION elements are emitted as method-bearing
81
+ // interfaces (see emit-dts collectHandleMethodGroups). Before that recovery they
82
+ // emitted nothing yet were never counted as a droppedMembers loss — a blind spot
83
+ // that let `socket` read `droppedMembers: 0` over an incomplete surface. Count
84
+ // them honestly: with emission on, only a malformed colon name (non-identifier
85
+ // segment) is still a loss; with emission off, every colon method is dropped.
86
+ export function countDroppedHandleMethods(doc: unknown, namespace: string, emit = true): number {
87
+ const prefix = `${namespace}.`;
88
+ let dropped = 0;
89
+ for (const element of elementsOf(doc)) {
90
+ if (element.type !== "FUNCTION" || typeof element.name !== "string") continue;
91
+ const local = element.name.startsWith(prefix)
92
+ ? element.name.slice(prefix.length)
93
+ : element.name;
94
+ if (!local.includes(":")) continue;
95
+ if (!emit || !HANDLE_METHOD_LOCAL.test(local)) dropped += 1;
96
+ }
97
+ return dropped;
98
+ }
99
+
79
100
  function auditEntry(
80
101
  entry: ModuleManifestEntry,
81
102
  knownConstantFqns: ReadonlySet<string>,
@@ -356,9 +377,11 @@ function auditEntry(
356
377
  unknownTokens: [...unknown].sort(),
357
378
  recordTables,
358
379
  multiReturn,
359
- droppedMembers: generateModuleDeclaration(entry, {
360
- knownConstantFqns: NO_KNOWN_CONSTANTS,
361
- }).dropped.filter((name) => !OVERLOAD_COVERED_SKIPS.has(name)).length,
380
+ droppedMembers:
381
+ generateModuleDeclaration(entry, {
382
+ knownConstantFqns: NO_KNOWN_CONSTANTS,
383
+ }).dropped.filter((name) => !OVERLOAD_COVERED_SKIPS.has(name)).length +
384
+ countDroppedHandleMethods(entry.doc, entry.namespace),
362
385
  optionalAsRequired,
363
386
  };
364
387
  }
@@ -4,10 +4,33 @@ import {
4
4
  type ApiTarget,
5
5
  generateModuleDeclaration,
6
6
  generateVersionIndex,
7
+ KIND_MODULE_MANIFEST,
8
+ LUA_STDLIB_REFERENCES,
7
9
  type ResolveTargetOptions,
8
10
  resolveTargetModules,
9
11
  } from "./regen";
10
12
 
13
+ export { KIND_MODULE_MANIFEST } from "./regen";
14
+
15
+ export interface RenderMaterializedKindIndexOptions {
16
+ readonly kind: string;
17
+ readonly universalModules: readonly string[];
18
+ readonly restrictedModule: string | null;
19
+ }
20
+
21
+ // Render one per-kind subpath for the materialized surface, mirroring
22
+ // `generateKindIndex` but re-exporting the factory from the installed
23
+ // `@defold-typescript/types/lifecycle` subpath (the materialized surface has no
24
+ // relative `src/lifecycle` to reach). Pure: returns a string, no FS.
25
+ export function renderMaterializedKindIndex(opts: RenderMaterializedKindIndexOptions): string {
26
+ const entry = KIND_MODULE_MANIFEST.find((e) => e.kind === opts.kind);
27
+ if (!entry) throw new Error(`unknown script kind: ${opts.kind}`);
28
+ const universal = [...new Set(["engine-globals", ...opts.universalModules])].sort();
29
+ const lines = universal.map((mod) => `import "../${mod}";`);
30
+ if (opts.restrictedModule) lines.push(`import "../${opts.restrictedModule}";`);
31
+ return `${LUA_STDLIB_REFERENCES}${lines.join("\n")}\n\nexport { ${entry.factory} } from "@defold-typescript/types/lifecycle";\nexport type { ScriptProperties, ScriptProperty } from "@defold-typescript/types/lifecycle";\n`;
32
+ }
33
+
11
34
  export interface MaterializeVersionedSurfaceOptions {
12
35
  readonly destDir: string;
13
36
  readonly resolveOpts?: ResolveTargetOptions;
package/scripts/regen.ts CHANGED
@@ -230,7 +230,7 @@ export const RESTRICTED_NAMESPACES: Readonly<Record<string, string>> = {
230
230
  // The Lua standard library rides every per-kind subpath the same as the full
231
231
  // entrypoint. Triple-slash directives must precede the first statement, so they
232
232
  // lead the generated kind index.
233
- const LUA_STDLIB_REFERENCES =
233
+ export const LUA_STDLIB_REFERENCES =
234
234
  '/// <reference types="lua-types/5.1" />\n/// <reference types="lua-types/special/jit-only" />\n';
235
235
 
236
236
  const UNIVERSAL_EXTRA_IMPORTS: readonly string[] = [
package/src/core-types.ts CHANGED
@@ -123,15 +123,20 @@ export const DEFOLD_TYPE_MAP: Readonly<Record<string, string>> = {
123
123
  constant: 'Opaque<"constant">',
124
124
  constant_buffer: 'Opaque<"constant_buffer">',
125
125
  buffer: 'Opaque<"buffer">',
126
- bufferstream: 'Opaque<"bufferstream">',
126
+ bufferstream: 'Opaque<"bufferstream"> & { [index: number]: number }',
127
127
  userdata: 'Opaque<"userdata">',
128
128
  resource: 'Opaque<"resource">',
129
129
  b2World: 'Opaque<"b2World">',
130
130
  b2Body: 'Opaque<"b2Body">',
131
131
  b2BodyType:
132
132
  '(number & { readonly __brand: "b2d.body.B2_DYNAMIC_BODY" }) | (number & { readonly __brand: "b2d.body.B2_KINEMATIC_BODY" }) | (number & { readonly __brand: "b2d.body.B2_STATIC_BODY" })',
133
- client: 'Opaque<"client">',
134
- master: 'Opaque<"master">',
135
- unconnected: 'Opaque<"unconnected">',
133
+ // socket handle types resolve to the method-bearing `interface <receiver>`
134
+ // emitted inside `namespace socket`, not opaque brands — the documented
135
+ // colon methods (`client:send`, …) make each handle structurally distinct.
136
+ client: "client",
137
+ connected: "connected",
138
+ master: "master",
139
+ server: "server",
140
+ unconnected: "unconnected",
136
141
  any: "unknown",
137
142
  } as const;
package/src/emit-dts.ts CHANGED
@@ -180,6 +180,14 @@ export const MAPPING_TABLE_SLOTS: ReadonlyMap<string, { key: string; value: stri
180
180
  ["gui.get_layouts", { key: "hash", value: "vector3" }],
181
181
  ]);
182
182
 
183
+ // FQN-keyed return-type overrides for functions whose doc `returnvalues` are
184
+ // empty yet the engine returns a value. `gui.get`'s doc lists no return, but it
185
+ // yields the property value (a vmath.vector4 or a number); `unknown` is the
186
+ // honest, ts-defold-matching shape. Per-FQN, not a blanket "empty returnvalues
187
+ // -> unknown" rule: `gui.set` also has empty returnvalues and `void` is correct
188
+ // there.
189
+ export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([["gui.get", "unknown"]]);
190
+
183
191
  // Element names whose `table` slot is a prose-only `array/list/table of <T>` shape
184
192
  // the field-list parser cannot read, but whose element type a human curated from
185
193
  // the doc. The value is a single element token (`T[]`) or a token list when the
@@ -219,7 +227,7 @@ export type TableSlotCuration =
219
227
  | { kind: "object"; fields: readonly TableField[] }
220
228
  | { kind: "array-object"; fields: readonly TableField[] };
221
229
 
222
- const SOCKET_HANDLE_TOKENS = ["client", "master", "unconnected"] as const;
230
+ export const SOCKET_HANDLE_TOKENS = ["client", "master", "unconnected"] as const;
223
231
 
224
232
  export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new Map([
225
233
  ["collectionfactory.create:return:ids", { kind: "mapping", key: "hash", value: "hash" }],
@@ -866,6 +874,42 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
866
874
  : a.name.localeCompare(b.name),
867
875
  );
868
876
 
877
+ // One-level nested functions (`socket.dns.toip`) fail the flat-identifier test
878
+ // in `prepareFunction` above (the stripped `dns.toip` has a dot), so they are
879
+ // absent from `functions`. Re-collect them grouped by their single leading
880
+ // segment, re-stripping against `<namespace>.<segment>.` so the emitted
881
+ // identifier is the final segment. Only exactly-one-dot, both-sides-identifier
882
+ // locals qualify; deeper nesting and non-identifier segments stay dropped.
883
+ const nestedFunctionLocal = /^[A-Za-z_$][\w$]*\.[A-Za-z_$][\w$]*$/;
884
+ const nestedGroups = new Map<string, PreparedFunction[]>();
885
+ for (const fn of module.functions) {
886
+ const local = stripPrefix(fn.name, prefix);
887
+ if (!nestedFunctionLocal.test(local)) continue;
888
+ const segment = local.slice(0, local.indexOf("."));
889
+ const prepared = prepareFunction(fn, `${module.namespace}.${segment}.`);
890
+ if (prepared === null) continue;
891
+ const group = nestedGroups.get(segment) ?? [];
892
+ group.push(prepared);
893
+ nestedGroups.set(segment, group);
894
+ }
895
+ for (const group of nestedGroups.values()) {
896
+ group.sort((a, b) =>
897
+ a.name === b.name
898
+ ? a.original.parameters.length - b.original.parameters.length
899
+ : a.name.localeCompare(b.name),
900
+ );
901
+ }
902
+ const nestedSegments = [...nestedGroups.keys()].sort((a, b) => a.localeCompare(b));
903
+
904
+ // Colon methods (`client:send`) are FUNCTION elements named `<receiver>:<method>`
905
+ // and are NOT namespace-prefixed, so they fail the flat-identifier test in
906
+ // prepareFunction (the colon) and are absent from `functions`. A returned handle
907
+ // is defined entirely by these methods; emit each receiver as a method-bearing
908
+ // interface, the same non-flat recovery the dns nested-namespace pass does one
909
+ // container shape shallower.
910
+ const handleGroups = collectHandleMethodGroups(module);
911
+ const handleReceivers = [...handleGroups.keys()].sort((a, b) => a.localeCompare(b));
912
+
869
913
  const resolver = buildTableDocResolver(
870
914
  module.functions.map((fn) => ({
871
915
  name: fn.name,
@@ -891,8 +935,23 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
891
935
  lines.push(`declare namespace ${module.namespace} {`);
892
936
 
893
937
  for (const t of typedefs) {
938
+ // A typedef that also has colon methods is emitted as a method-bearing
939
+ // interface below, not an opaque brand alias.
940
+ if (handleGroups.has(t.name)) continue;
894
941
  lines.push(`${INDENT}${decl}type ${t.name} = Opaque<"${t.name}">;`);
895
942
  }
943
+ const handleIndent = `${INDENT}${INDENT}`;
944
+ for (const receiver of handleReceivers) {
945
+ const group = handleGroups.get(receiver) ?? [];
946
+ lines.push(`${INDENT}${decl}interface ${receiver} {`);
947
+ for (const fn of group) {
948
+ for (const docLine of functionDocLines(fn.original, translations, handleIndent)) {
949
+ lines.push(docLine);
950
+ }
951
+ lines.push(`${handleIndent}${emitMethod(fn, mapType, resolver)}`);
952
+ }
953
+ lines.push(`${INDENT}}`);
954
+ }
896
955
  for (const c of constants) {
897
956
  for (const docLine of summaryDocLines(c.original.brief, c.original.description, INDENT)) {
898
957
  lines.push(docLine);
@@ -926,6 +985,19 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
926
985
  lines.push(`${INDENT}export { ${alias.internal} as ${alias.public} };`);
927
986
  }
928
987
 
988
+ const nestedIndent = `${INDENT}${INDENT}`;
989
+ for (const segment of nestedSegments) {
990
+ const group = nestedGroups.get(segment) ?? [];
991
+ lines.push(`${INDENT}${decl}namespace ${segment} {`);
992
+ for (const fn of group) {
993
+ for (const docLine of functionDocLines(fn.original, translations, nestedIndent)) {
994
+ lines.push(docLine);
995
+ }
996
+ lines.push(`${nestedIndent}${decl}${emitFunction(fn, fn.name, mapType, resolver)}`);
997
+ }
998
+ lines.push(`${INDENT}}`);
999
+ }
1000
+
929
1001
  if (module.properties.length > 0) {
930
1002
  const members = [...module.properties].sort((a, b) => a.name.localeCompare(b.name));
931
1003
  lines.push(`${INDENT}${decl}interface properties {`);
@@ -964,6 +1036,37 @@ function prepareFunction(fn: ApiFunction, prefix: string): PreparedFunction | nu
964
1036
  return { name: stripped, original: fn };
965
1037
  }
966
1038
 
1039
+ // A `<receiver>:<method>` handle method, both segments bare identifiers. Deeper
1040
+ // shapes and non-identifier segments do not match and stay dropped.
1041
+ export const HANDLE_METHOD_LOCAL = /^[A-Za-z_$][\w$]*:[A-Za-z_$][\w$]*$/;
1042
+
1043
+ // Group a module's colon-method FUNCTION elements (`client:send`) by their
1044
+ // receiver segment, each method prepared so its emitted name is the final
1045
+ // segment. Receivers (and the methods within each) are returned unsorted; the
1046
+ // emitter sorts for a stable surface. A non-identifier segment yields no group.
1047
+ export function collectHandleMethodGroups(module: ApiModule): Map<string, PreparedFunction[]> {
1048
+ const prefix = `${module.namespace}.`;
1049
+ const groups = new Map<string, PreparedFunction[]>();
1050
+ for (const fn of module.functions) {
1051
+ const local = stripPrefix(fn.name, prefix);
1052
+ if (!HANDLE_METHOD_LOCAL.test(local)) continue;
1053
+ const receiver = local.slice(0, local.indexOf(":"));
1054
+ const prepared = prepareFunction(fn, fn.name.slice(0, fn.name.lastIndexOf(":") + 1));
1055
+ if (prepared === null) continue;
1056
+ const group = groups.get(receiver) ?? [];
1057
+ group.push(prepared);
1058
+ groups.set(receiver, group);
1059
+ }
1060
+ for (const group of groups.values()) {
1061
+ group.sort((a, b) =>
1062
+ a.name === b.name
1063
+ ? a.original.parameters.length - b.original.parameters.length
1064
+ : a.name.localeCompare(b.name),
1065
+ );
1066
+ }
1067
+ return groups;
1068
+ }
1069
+
967
1070
  function prepareConstant(c: ApiConstant, prefix: string): PreparedConstant | null {
968
1071
  const stripped = stripPrefix(c.name, prefix);
969
1072
  if (!TS_IDENTIFIER.test(stripped)) return null;
@@ -1007,7 +1110,7 @@ function emitVariable(
1007
1110
  return `const ${name}: ${ts};`;
1008
1111
  }
1009
1112
 
1010
- function emitFunction(
1113
+ function memberSignature(
1011
1114
  prepared: PreparedFunction,
1012
1115
  name: string,
1013
1116
  mapType: (t: string) => string,
@@ -1020,7 +1123,27 @@ function emitFunction(
1020
1123
  .map((p, i) => emitParameter(p, i, i >= cutoff, mapType, resolver, elementName))
1021
1124
  .join(", ");
1022
1125
  const ret = emitReturn(prepared.original.returnValues, mapType, resolver, elementName);
1023
- return `function ${name}(${params}): ${ret.type};${ret.trailing}`;
1126
+ return `${name}(${params}): ${ret.type};${ret.trailing}`;
1127
+ }
1128
+
1129
+ function emitFunction(
1130
+ prepared: PreparedFunction,
1131
+ name: string,
1132
+ mapType: (t: string) => string,
1133
+ resolver: TableDocResolver,
1134
+ ): string {
1135
+ return `function ${memberSignature(prepared, name, mapType, resolver)}`;
1136
+ }
1137
+
1138
+ // A colon-method member of a handle interface: identical signature machinery to a
1139
+ // free function, but without the `function` keyword. `@noSelfInFile` means the
1140
+ // member carries no `self` parameter.
1141
+ function emitMethod(
1142
+ prepared: PreparedFunction,
1143
+ mapType: (t: string) => string,
1144
+ resolver: TableDocResolver,
1145
+ ): string {
1146
+ return memberSignature(prepared, prepared.name, mapType, resolver);
1024
1147
  }
1025
1148
 
1026
1149
  // Build the indented JSDoc lines for a function from its ref-doc prose. The
@@ -1029,7 +1152,11 @@ function emitFunction(
1029
1152
  // fallback applies to non-identifier names, matching `emitParameter`) so the tag
1030
1153
  // resolves on hover; a single documented return becomes `@returns`. Returns `[]`
1031
1154
  // for a fully-undocumented function, leaving its emission byte-identical.
1032
- function functionDocLines(fn: ApiFunction, translations: TranslationStore): string[] {
1155
+ function functionDocLines(
1156
+ fn: ApiFunction,
1157
+ translations: TranslationStore,
1158
+ indent: string = INDENT,
1159
+ ): string[] {
1033
1160
  const params = fn.parameters.map((p, index) => ({
1034
1161
  name: safeParamName(p.name, index),
1035
1162
  doc: htmlToDocText(p.doc),
@@ -1047,7 +1174,7 @@ function functionDocLines(fn: ApiFunction, translations: TranslationStore): stri
1047
1174
  ...(onlyReturn ? { returns: htmlToDocText(onlyReturn.doc) } : {}),
1048
1175
  ...exampleParts,
1049
1176
  };
1050
- return indentDocLines(parts, INDENT);
1177
+ return indentDocLines(parts, indent);
1051
1178
  }
1052
1179
 
1053
1180
  // Summary-only doc lines for a member that carries no params or returns
@@ -1116,6 +1243,8 @@ function emitReturn(
1116
1243
  resolver: TableDocResolver,
1117
1244
  elementName: string,
1118
1245
  ): { type: string; trailing: string } {
1246
+ const override = RETURN_TYPE_OVERRIDES.get(elementName);
1247
+ if (override !== undefined) return { type: override, trailing: "" };
1119
1248
  if (returnValues.length === 0) return { type: "void", trailing: "" };
1120
1249
  if (returnValues.length > 1) {
1121
1250
  // Defold multi-returns are positional and always present; each slot maps