@defold-typescript/types 0.22.0 → 0.24.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/types",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "TypeScript types for the Defold engine's Lua APIs.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/scripts/regen.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  parseMessagesDoc,
10
10
  } from "../src/emit-messages";
11
11
  import type { TranslationStore } from "../src/example-store";
12
- import { wrapAsAmbientGlobal } from "../src/publish-dts";
12
+ import { wrapAsAmbientGlobal, wrapAsModule } from "../src/publish-dts";
13
13
  import {
14
14
  type DocSourceProvenance,
15
15
  type DownloadRefDoc,
@@ -90,6 +90,7 @@ export interface ModuleManifestEntry {
90
90
  readonly outFile: string;
91
91
  readonly skipFunctions?: readonly string[];
92
92
  readonly importsFrom?: string;
93
+ readonly moduleId?: string;
93
94
  readonly sourceProvenance?: DocSourceProvenance;
94
95
  }
95
96
 
@@ -239,11 +240,10 @@ export function generateModuleDeclaration(
239
240
  options,
240
241
  );
241
242
  const emitted = emitDeclarations(module, { knownConstantFqns, translations });
242
- const contents = wrapAsAmbientGlobal({
243
- namespace: module.namespace,
244
- emitted,
245
- importsFrom: entry.importsFrom ?? "../src/core-types",
246
- });
243
+ const importsFrom = entry.importsFrom ?? "../src/core-types";
244
+ const contents = entry.moduleId
245
+ ? wrapAsModule({ namespace: module.namespace, emitted, importsFrom, moduleId: entry.moduleId })
246
+ : wrapAsAmbientGlobal({ namespace: module.namespace, emitted, importsFrom });
247
247
  return { contents, dropped };
248
248
  }
249
249
 
@@ -22,6 +22,8 @@ export const PACKAGE_SIGNATURES_PATH = resolve(import.meta.dir, "..", "signature
22
22
  export const BASE_SIGNATURES_PATH = resolve(import.meta.dir, "..", "signatures", "base.json");
23
23
  export const SOCKET_SIGNATURES_PATH = resolve(import.meta.dir, "..", "signatures", "socket.json");
24
24
  export const VMATH_SIGNATURES_PATH = resolve(import.meta.dir, "..", "signatures", "vmath.json");
25
+ export const GO_SIGNATURES_PATH = resolve(import.meta.dir, "..", "signatures", "go.json");
26
+ export const MSG_SIGNATURES_PATH = resolve(import.meta.dir, "..", "signatures", "msg.json");
25
27
 
26
28
  export function loadSignatureFile(path: string): SignatureStore {
27
29
  let raw: string;
package/src/api-doc.ts CHANGED
@@ -20,12 +20,16 @@ export interface ApiTypedef {
20
20
  name: string;
21
21
  functions?: ApiFunction[];
22
22
  properties?: ApiVariable[];
23
+ /** See {@link ApiFunction.global}. */
24
+ global?: true;
23
25
  }
24
26
 
25
27
  export interface ApiConstant {
26
28
  name: string;
27
29
  brief: string;
28
30
  description: string;
31
+ /** See {@link ApiFunction.global}. */
32
+ global?: true;
29
33
  }
30
34
 
31
35
  export interface ApiFunction {
@@ -41,6 +45,27 @@ export interface ApiFunction {
41
45
  * library functions; engine ref-docs carry no `generics`, so it stays absent.
42
46
  */
43
47
  generics?: string;
48
+ /**
49
+ * Present exactly when the source carried a `@deprecated` tag; `""` for a bare
50
+ * tag. Absence is the only encoding of "not deprecated", so a bare tag stays
51
+ * distinguishable from an untagged symbol.
52
+ */
53
+ deprecated?: string;
54
+ /**
55
+ * Present exactly when the source declared the symbol as an ambient global —
56
+ * outside the library's `declare module` block — so it is reachable without
57
+ * the module import. Absence is the only encoding of "module member"; the key
58
+ * is never written as `false`.
59
+ */
60
+ global?: true;
61
+ /**
62
+ * Present exactly when this symbol's prose was imported from the upstream
63
+ * source rather than written in the declaration — the authored/forked library
64
+ * lane lowers upstream's own LuaDoc summary for a member its fork documents
65
+ * nowhere. Absence is the only encoding of first-party prose, so every engine
66
+ * and hand-authored symbol reads as before.
67
+ */
68
+ docSource?: "upstream";
44
69
  }
45
70
 
46
71
  export interface ApiParameter {
@@ -67,6 +92,30 @@ export interface ApiVariable {
67
92
  brief: string;
68
93
  description: string;
69
94
  types: string[];
95
+ /**
96
+ * True for an optional member of a typedef shape (`clear?: boolean`). Set only
97
+ * when the element carries `is_optional: "True"`, so a module-level engine
98
+ * ref-doc VARIABLE — which never carries the key — leaves it absent.
99
+ */
100
+ isOptional?: boolean;
101
+ /** See {@link ApiFunction.deprecated}. */
102
+ deprecated?: string;
103
+ /** See {@link ApiFunction.global}. */
104
+ global?: true;
105
+ /** See {@link ApiFunction.docSource}. */
106
+ docSource?: "upstream";
107
+ }
108
+
109
+ /** The `{ global }` key to spread onto a parsed element, empty for a module member. */
110
+ function globalKey(element: Record<string, unknown>): { global?: true } {
111
+ return element.global === true ? { global: true } : {};
112
+ }
113
+
114
+ /** The `{ docSource }` key to spread onto a parsed element, empty for first-party
115
+ * prose. Only the one recognised value yields a key: an unknown provenance would
116
+ * otherwise reach a page with no marker the render layer knows how to draw. */
117
+ function docSourceKey(element: Record<string, unknown>): { docSource?: "upstream" } {
118
+ return element.docSource === "upstream" ? { docSource: "upstream" } : {};
70
119
  }
71
120
 
72
121
  export function parseDefoldApiDoc(input: unknown): ApiModule {
@@ -119,6 +168,7 @@ function parseTypedef(element: Record<string, unknown>): ApiTypedef {
119
168
  name: stringOr(element.name, ""),
120
169
  ...(functions.length > 0 ? { functions } : {}),
121
170
  ...(properties.length > 0 ? { properties } : {}),
171
+ ...globalKey(element),
122
172
  };
123
173
  }
124
174
 
@@ -145,6 +195,7 @@ function parseConstant(element: Record<string, unknown>): ApiConstant {
145
195
  name: stringOr(element.name, ""),
146
196
  brief: stringOr(element.brief, ""),
147
197
  description: stringOr(element.description, ""),
198
+ ...globalKey(element),
148
199
  };
149
200
  }
150
201
 
@@ -157,6 +208,9 @@ function parseFunction(element: Record<string, unknown>): ApiFunction {
157
208
  returnValues: parseParameterList(element.returnvalues),
158
209
  examples: stringOr(element.examples, ""),
159
210
  ...(typeof element.generics === "string" ? { generics: element.generics } : {}),
211
+ ...(typeof element.deprecated === "string" ? { deprecated: element.deprecated } : {}),
212
+ ...globalKey(element),
213
+ ...docSourceKey(element),
160
214
  };
161
215
  }
162
216
 
@@ -166,6 +220,10 @@ function parseVariable(element: Record<string, unknown>): ApiVariable {
166
220
  brief: stringOr(element.brief, ""),
167
221
  description: stringOr(element.description, ""),
168
222
  types: parseStringArray(element.types),
223
+ ...(element.is_optional === "True" ? { isOptional: true } : {}),
224
+ ...(typeof element.deprecated === "string" ? { deprecated: element.deprecated } : {}),
225
+ ...globalKey(element),
226
+ ...docSourceKey(element),
169
227
  };
170
228
  }
171
229
 
package/src/core-types.ts CHANGED
@@ -188,6 +188,9 @@ export const DEFOLD_TYPE_MAP: Readonly<Record<string, string>> = {
188
188
  vector4: "Vector4",
189
189
  quaternion: "Quaternion",
190
190
  matrix4: "Matrix4",
191
+ // Authored-README shorthand for `vmath.matrix4`; absent from every engine
192
+ // ref-doc, which a core-types.test.ts guard keeps true as releases import.
193
+ matrix: "Matrix4",
191
194
  "vmath.vector3": "Vector3",
192
195
  "vmath.vector4": "Vector4",
193
196
  "vmath.matrix4": "Matrix4",
@@ -107,6 +107,10 @@ export function examplesHtmlToMarkdown(html: string): string {
107
107
 
108
108
  export interface DocCommentParts {
109
109
  summary: string;
110
+ // Present exactly when the source carried a deprecation tag; `""` is the bare
111
+ // form and still renders, so this is tested against `undefined` rather than for
112
+ // truthiness the way the other optional parts are.
113
+ deprecated?: string;
110
114
  params?: { name: string; doc: string }[];
111
115
  returns?: string;
112
116
  example?: string;
@@ -122,8 +126,15 @@ export function renderDocComment(parts: DocCommentParts): string[] {
122
126
  const params = (parts.params ?? []).filter((p) => p.doc.trim() !== "");
123
127
  const returns = parts.returns?.trim() ? parts.returns : "";
124
128
  const example = parts.example?.trim() ? parts.example : "";
125
-
126
- if (summaryLines.length === 0 && params.length === 0 && returns === "" && example === "") {
129
+ const deprecated = parts.deprecated;
130
+
131
+ if (
132
+ summaryLines.length === 0 &&
133
+ params.length === 0 &&
134
+ returns === "" &&
135
+ example === "" &&
136
+ deprecated === undefined
137
+ ) {
127
138
  return [];
128
139
  }
129
140
 
@@ -132,11 +143,18 @@ export function renderDocComment(parts: DocCommentParts): string[] {
132
143
  lines.push(line === "" ? " *" : ` * ${line}`);
133
144
  }
134
145
 
135
- const hasTags = params.length > 0 || returns !== "" || example !== "";
146
+ const hasTags = deprecated !== undefined || params.length > 0 || returns !== "" || example !== "";
136
147
  if (summaryLines.length > 0 && hasTags) {
137
148
  lines.push(" *");
138
149
  }
139
150
 
151
+ if (deprecated !== undefined) {
152
+ const [first, ...rest] = deprecated.split("\n");
153
+ lines.push(first === "" ? " * @deprecated" : ` * @deprecated ${first}`);
154
+ for (const line of rest) {
155
+ lines.push(line === "" ? " *" : ` * ${line}`);
156
+ }
157
+ }
140
158
  for (const param of params) {
141
159
  const [first, ...rest] = param.doc.split("\n");
142
160
  lines.push(` * @param ${param.name} - ${first}`);
package/src/emit-dts.ts CHANGED
@@ -1565,12 +1565,25 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
1565
1565
  const nestedIndent = `${INDENT}${INDENT}`;
1566
1566
  for (const segment of nestedSegments) {
1567
1567
  const group = nestedGroups.get(segment) ?? [];
1568
+ // A reserved-name function inside a nested namespace gets the same recovery as
1569
+ // a top-level one: emitted un-exported as `_<name>` and re-exported under the
1570
+ // reserved name. The alias switches this namespace out of implicit-export mode,
1571
+ // so its siblings then need an explicit `export` to stay reachable.
1572
+ const segmentAliases: { internal: string; public: string }[] = [];
1573
+ const segmentDecl = group.some((fn) => TS_RESERVED_NAMES.has(fn.name)) ? "export " : "";
1568
1574
  lines.push(`${INDENT}${decl}namespace ${segment} {`);
1569
1575
  for (const fn of group) {
1576
+ const reserved = TS_RESERVED_NAMES.has(fn.name);
1577
+ const emitName = aliasName(fn.name, segmentAliases);
1570
1578
  for (const docLine of functionDocLines(fn.original, translations, nestedIndent)) {
1571
1579
  lines.push(docLine);
1572
1580
  }
1573
- lines.push(`${nestedIndent}${decl}${emitFunction(fn, fn.name, mapType, resolver)}`);
1581
+ lines.push(
1582
+ `${nestedIndent}${reserved ? "" : segmentDecl}${emitFunction(fn, emitName, mapType, resolver)}`,
1583
+ );
1584
+ }
1585
+ for (const alias of [...segmentAliases].sort((a, b) => a.public.localeCompare(b.public))) {
1586
+ lines.push(`${nestedIndent}export { ${alias.internal} as ${alias.public} };`);
1574
1587
  }
1575
1588
  lines.push(`${INDENT}}`);
1576
1589
  }
package/src/index.ts CHANGED
@@ -80,7 +80,12 @@ export {
80
80
  type ScriptPropertiesOf,
81
81
  type ScriptProperty,
82
82
  } from "./lifecycle";
83
- export { type WrapOptions, wrapAsAmbientGlobal } from "./publish-dts";
83
+ export {
84
+ type ModuleWrapOptions,
85
+ type WrapOptions,
86
+ wrapAsAmbientGlobal,
87
+ wrapAsModule,
88
+ } from "./publish-dts";
84
89
  export {
85
90
  lookupSignature,
86
91
  type SignatureOverride,
@@ -29,7 +29,15 @@ export function varargElementType(mapped: string): string {
29
29
  return needsArrayParens(mapped) ? `(${mapped})[]` : `${mapped}[]`;
30
30
  }
31
31
 
32
- /** Wrap `>1` mapped return tokens in the `LuaMultiReturn<[...]>` tuple form. */
33
- export function luaMultiReturn(mapped: readonly string[]): string {
34
- return `LuaMultiReturn<[${mapped.join(", ")}]>`;
32
+ /**
33
+ * Wrap `>1` mapped return tokens in the `LuaMultiReturn<[...]>` tuple form. With
34
+ * `restTail`, the last element renders as a rest element — the shape a LuaLS
35
+ * multi-return whose final value is a bare vararg (`fun(): T, ...`) calls for.
36
+ */
37
+ export function luaMultiReturn(mapped: readonly string[], restTail = false): string {
38
+ const elements =
39
+ restTail && mapped.length > 0
40
+ ? [...mapped.slice(0, -1), `...${varargElementType(mapped[mapped.length - 1] as string)}`]
41
+ : mapped;
42
+ return `LuaMultiReturn<[${elements.join(", ")}]>`;
35
43
  }
@@ -16,18 +16,46 @@ export interface WrapOptions {
16
16
  importsFrom: string;
17
17
  }
18
18
 
19
- export function wrapAsAmbientGlobal(opts: WrapOptions): string {
20
- const used = collectEngineTypes(opts.emitted);
19
+ export interface ModuleWrapOptions extends WrapOptions {
20
+ moduleId: string;
21
+ }
22
+
23
+ // The engine-type import line plus the emitted body with its leading
24
+ // `declare namespace` rewritten to `topKeyword` and every line indented two
25
+ // spaces — the shared shape both the ambient-global and importable-module wraps
26
+ // place inside their envelope.
27
+ function prepareWrapBody(
28
+ emitted: string,
29
+ importsFrom: string,
30
+ topKeyword: string,
31
+ ): { importLine: string; indented: string } {
32
+ const used = collectEngineTypes(emitted);
21
33
  const importLine =
22
- used.length === 0 ? "" : `import type { ${used.join(", ")} } from "${opts.importsFrom}";\n\n`;
23
- const inner = opts.emitted.replace(/(^|\n)declare\s+namespace\s+/, "$1namespace ").trimEnd();
34
+ used.length === 0 ? "" : `import type { ${used.join(", ")} } from "${importsFrom}";\n\n`;
35
+ const inner = emitted.replace(/(^|\n)declare\s+namespace\s+/, `$1${topKeyword} `).trimEnd();
24
36
  const indented = inner
25
37
  .split("\n")
26
38
  .map((l) => (l.length === 0 ? l : ` ${l}`))
27
39
  .join("\n");
40
+ return { importLine, indented };
41
+ }
42
+
43
+ export function wrapAsAmbientGlobal(opts: WrapOptions): string {
44
+ const { importLine, indented } = prepareWrapBody(opts.emitted, opts.importsFrom, "namespace");
28
45
  return `/** @noSelfInFile */\n${importLine}declare global {\n${indented}\n}\n\nexport {};\n`;
29
46
  }
30
47
 
48
+ export function wrapAsModule(opts: ModuleWrapOptions): string {
49
+ // ambient-globals-only: the module form references engine handles (`Hash`,
50
+ // `Vector3`, ...) as ambient globals and emits no top-level import. A top-level
51
+ // `import type` would make the `.d.ts` a module, demoting `declare module
52
+ // '<id>'` to an augmentation of an unresolvable specifier that a consumer
53
+ // `import` cannot resolve (`TS2307`). `importsFrom` stays in the signature for
54
+ // caller symmetry but is unused here; `wrapAsAmbientGlobal` still imports it.
55
+ const { indented } = prepareWrapBody(opts.emitted, opts.importsFrom, "export namespace");
56
+ return `/** @noSelfInFile */\n/** @noResolution */\ndeclare module '${opts.moduleId}' {\n${indented}\n}\n`;
57
+ }
58
+
31
59
  function collectEngineTypes(emitted: string): EngineType[] {
32
60
  return ENGINE_TYPES.filter((t) => new RegExp(`\\b${t}\\b`).test(emitted));
33
61
  }
package/src/script-api.ts CHANGED
@@ -27,6 +27,17 @@ function stringOr(value: unknown, fallback: string): string {
27
27
  return typeof value === "string" ? value : fallback;
28
28
  }
29
29
 
30
+ // A `.script_api` `type:` may spell a union inline (`string | nil`), where the
31
+ // core ref-doc format carries one token per alternative. Splitting here keeps the
32
+ // downstream emitter and fidelity resolver working in single tokens.
33
+ function splitTypeTokens(type: unknown): string[] {
34
+ if (typeof type !== "string") return [];
35
+ return type
36
+ .split("|")
37
+ .map((token) => token.trim())
38
+ .filter((token) => token.length > 0);
39
+ }
40
+
30
41
  function mapParameters(raw: unknown): RefDocParameter[] {
31
42
  if (!Array.isArray(raw)) return [];
32
43
  const out: RefDocParameter[] = [];
@@ -36,11 +47,10 @@ function mapParameters(raw: unknown): RefDocParameter[] {
36
47
  // The script_api lists the implicit `self` the engine passes; the emitter
37
48
  // stamps @noSelfInFile, so generated signatures must not declare it.
38
49
  if (name === "self") continue;
39
- const type = item.type;
40
50
  out.push({
41
51
  name,
42
52
  doc: stringOr(item.desc, ""),
43
- types: typeof type === "string" ? [type] : [],
53
+ types: splitTypeTokens(item.type),
44
54
  });
45
55
  }
46
56
  return out;
@@ -73,15 +83,31 @@ export function scriptApiToRefDoc(parsed: unknown): RefDoc {
73
83
  }
74
84
  const members = Array.isArray(table.members) ? table.members : [];
75
85
  const elements: RefDocElement[] = [];
86
+ const fnElement = (name: string, member: Record<string, unknown>): RefDocElement => ({
87
+ type: "FUNCTION",
88
+ name,
89
+ description: stringOr(member.desc, ""),
90
+ parameters: mapParameters(member.parameters),
91
+ returnvalues: mapParameters(member.returns),
92
+ });
76
93
  for (const member of members) {
77
- if (!isRecord(member) || member.type !== "function") continue;
78
- elements.push({
79
- type: "FUNCTION",
80
- name: `${namespace}.${stringOr(member.name, "")}`,
81
- description: stringOr(member.desc, ""),
82
- parameters: mapParameters(member.parameters),
83
- returnvalues: mapParameters(member.returns),
84
- });
94
+ if (!isRecord(member)) continue;
95
+ if (member.type === "function") {
96
+ elements.push(fnElement(`${namespace}.${stringOr(member.name, "")}`, member));
97
+ continue;
98
+ }
99
+ if (member.type === "table") {
100
+ const sub = stringOr(member.name, "");
101
+ // A nameless sub-namespace can't form a valid dotted name; skip it.
102
+ if (sub.length === 0) continue;
103
+ const subMembers = Array.isArray(member.members) ? member.members : [];
104
+ for (const subMember of subMembers) {
105
+ // Recurse exactly one level: a `type: table` nested here is 2nd-level,
106
+ // whose functions the emitter's one-dot pass would silently drop.
107
+ if (!isRecord(subMember) || subMember.type !== "function") continue;
108
+ elements.push(fnElement(`${namespace}.${sub}.${stringOr(subMember.name, "")}`, subMember));
109
+ }
110
+ }
85
111
  }
86
112
  const doc: RefDoc = {
87
113
  info: { namespace, brief: stringOr(table.desc, ""), description: stringOr(table.desc, "") },