@defold-typescript/library-types 0.23.0 → 0.25.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.
Files changed (130) hide show
  1. package/NOTICE +2 -3
  2. package/api-doc/boom.json +3872 -0
  3. package/api-doc/bridge.json +10 -5
  4. package/api-doc/bzAnim.json +338 -0
  5. package/api-doc/checkpoint.json +127 -0
  6. package/api-doc/decore.json +1 -1
  7. package/api-doc/defcon.json +150 -0
  8. package/api-doc/{defmath.defmath.json → defmath.json} +6 -46
  9. package/api-doc/defsave.json +518 -0
  10. package/api-doc/deftest.json +745 -0
  11. package/api-doc/{dicebag.dicebag.json → dicebag.json} +19 -1
  12. package/api-doc/druid.json +90 -92
  13. package/api-doc/event.json +16 -16
  14. package/api-doc/gooey.json +2050 -0
  15. package/api-doc/in.accelerometer.json +75 -1
  16. package/api-doc/in.button.json +17 -10
  17. package/api-doc/in.cursor.json +26 -0
  18. package/api-doc/in.gesture.json +401 -6
  19. package/api-doc/in.keyboard.json +2 -0
  20. package/api-doc/in.mapper.json +12 -8
  21. package/api-doc/in.onscreen.json +61 -5
  22. package/api-doc/in.state.json +61 -13
  23. package/api-doc/in.textbox.json +23 -12
  24. package/api-doc/in.triggers.json +336 -0
  25. package/api-doc/lang.json +5 -5
  26. package/api-doc/log.json +1 -1
  27. package/api-doc/metrics.fps.json +61 -2
  28. package/api-doc/metrics.mem.json +61 -2
  29. package/api-doc/monarch.monarch.json +249 -7
  30. package/api-doc/monarch.transitions.easings.json +19 -26
  31. package/api-doc/monarch.transitions.gui.json +782 -79
  32. package/api-doc/nakama.engine.defold.json +31 -18
  33. package/api-doc/{nakama.nakama.json → nakama.json} +4752 -2554
  34. package/api-doc/nakama.session.json +315 -0
  35. package/api-doc/nakama.socket.json +3356 -0
  36. package/api-doc/nakama.util.log.json +32 -4
  37. package/api-doc/narrator.json +1 -1
  38. package/api-doc/openapi/nakama.nakama.json +7680 -0
  39. package/api-doc/orthographic.camera.json +96 -98
  40. package/api-doc/orthographic.json +602 -0
  41. package/api-doc/panthera.json +1183 -0
  42. package/api-doc/{persist.persist.json → persist.json} +26 -1
  43. package/api-doc/{platypus.platypus.json → platypus.json} +68 -14
  44. package/api-doc/{rendy.rendy.json → rendy.json} +46 -99
  45. package/api-doc/richtext.color.json +77 -0
  46. package/api-doc/richtext.richtext.json +375 -5
  47. package/api-doc/saver.saver.json +2 -1
  48. package/api-doc/saver.storage.json +2 -1
  49. package/api-doc/shutter.json +551 -0
  50. package/api-doc/{yagames.yagames.json → yagames.json} +784 -189
  51. package/api-doc/{zzfx.api.json → zzfx.json} +9 -9
  52. package/authored-targets.json +433 -0
  53. package/generated/{boom.boom.d.ts → boom.d.ts} +14 -0
  54. package/generated/bridge.d.ts +5 -5
  55. package/generated/{bzAnim.bzLibrary.d.ts → bzAnim.d.ts} +6 -0
  56. package/generated/checkpoint.d.ts +62 -0
  57. package/generated/decore.d.ts +1 -1
  58. package/generated/defcon.d.ts +43 -0
  59. package/generated/{defmath.defmath.d.ts → defmath.d.ts} +3 -2
  60. package/generated/defsave.d.ts +144 -0
  61. package/generated/{dicebag.dicebag.d.ts → dicebag.d.ts} +25 -0
  62. package/generated/druid.d.ts +81 -79
  63. package/generated/event.d.ts +15 -15
  64. package/generated/{gooey.gooey.d.ts → gooey.d.ts} +61 -6
  65. package/generated/in.onscreen.d.ts +5 -0
  66. package/generated/in.textbox.d.ts +4 -0
  67. package/generated/lang.d.ts +5 -5
  68. package/generated/log.d.ts +1 -1
  69. package/generated/metrics.fps.d.ts +19 -1
  70. package/generated/metrics.mem.d.ts +19 -1
  71. package/generated/monarch.monarch.d.ts +44 -1
  72. package/generated/monarch.transitions.easings.d.ts +0 -1
  73. package/generated/monarch.transitions.gui.d.ts +96 -12
  74. package/generated/nakama.d.ts +1158 -0
  75. package/generated/nakama.engine.defold.d.ts +3 -1
  76. package/generated/nakama.session.d.ts +95 -0
  77. package/generated/nakama.socket.d.ts +706 -0
  78. package/generated/nakama.util.log.d.ts +6 -0
  79. package/generated/narrator.d.ts +1 -1
  80. package/generated/openapi/nakama.nakama.d.ts +884 -0
  81. package/generated/orthographic.camera.d.ts +24 -38
  82. package/generated/orthographic.d.ts +155 -0
  83. package/generated/panthera.d.ts +170 -0
  84. package/generated/{persist.persist.d.ts → persist.d.ts} +3 -0
  85. package/generated/{platypus.platypus.d.ts → platypus.d.ts} +9 -0
  86. package/generated/proto.d.ts +4 -4
  87. package/generated/{rendy.rendy.d.ts → rendy.d.ts} +33 -80
  88. package/generated/richtext.color.d.ts +15 -0
  89. package/generated/saver.saver.d.ts +2 -0
  90. package/generated/saver.storage.d.ts +2 -0
  91. package/generated/shutter.d.ts +183 -0
  92. package/generated/{yagames.yagames.d.ts → yagames.d.ts} +151 -29
  93. package/generated/{zzfx.api.d.ts → zzfx.d.ts} +7 -0
  94. package/library-classification.json +0 -156
  95. package/library-targets.json +1 -218
  96. package/luals-targets.json +56 -3
  97. package/openapi-targets.json +17 -0
  98. package/package.json +7 -57
  99. package/scripts/__snapshots__/parse-luals.test.ts.snap +164 -10
  100. package/scripts/apply-luals-overrides.ts +39 -7
  101. package/scripts/authored-parity.ts +718 -0
  102. package/scripts/emit-library-dts.ts +94 -17
  103. package/scripts/extract-api-doc.ts +214 -42
  104. package/scripts/fidelity-floor.ts +178 -0
  105. package/scripts/import-upstream-docs.ts +125 -0
  106. package/scripts/lower-api-doc.ts +21 -3
  107. package/scripts/luals-fidelity.ts +16 -11
  108. package/scripts/map-luals-types.ts +52 -21
  109. package/scripts/parse-lua-surface.ts +187 -0
  110. package/scripts/parse-luals.ts +50 -4
  111. package/scripts/parse-markdown-api.ts +247 -0
  112. package/scripts/parse-openapi-api.ts +313 -0
  113. package/scripts/sync-authored-types.ts +249 -0
  114. package/scripts/sync-library-types.ts +99 -3
  115. package/scripts/sync-luals-types.ts +14 -1
  116. package/scripts/sync-markdown-types.ts +761 -0
  117. package/scripts/sync-openapi-types.ts +308 -0
  118. package/scripts/sync-script-api-types.ts +16 -17
  119. package/api-doc/boom.boom.json +0 -26
  120. package/api-doc/bzAnim.bzLibrary.json +0 -156
  121. package/api-doc/defcon.console.json +0 -109
  122. package/api-doc/defsave.defsave.json +0 -175
  123. package/api-doc/deftest.deftest.json +0 -71
  124. package/api-doc/gooey.gooey.json +0 -963
  125. package/api-doc/starly.starly.json +0 -488
  126. package/generated/defcon.console.d.ts +0 -24
  127. package/generated/defsave.defsave.d.ts +0 -31
  128. package/generated/nakama.nakama.d.ts +0 -594
  129. package/generated/starly.starly.d.ts +0 -148
  130. /package/generated/{deftest.deftest.d.ts → deftest.d.ts} +0 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * A one-sided coverage ratchet over the committed `fidelity/*.json` reports.
3
+ *
4
+ * Each emitting lane already round-trips its committed report against a freshly
5
+ * built one, which proves the report matches what the pipeline builds *today* —
6
+ * but a regeneration that lowers coverage rewrites the report too, so the drop
7
+ * lands silently. `fidelity-floor.json` pins each report's coverage from the
8
+ * outside: `regen` never touches it, so the only way a floor moves is the
9
+ * explicit, monotone `--raise` below — which also makes it the one hand-editable
10
+ * input here, so it is parsed under a validating contract (`parseFloors`).
11
+ *
12
+ * Reports are keyed by package-root-relative POSIX path, not by namespace:
13
+ * `fidelity/openapi/nakama.nakama.json` reports `namespace: "nakama"`, and two
14
+ * lanes emitting the same namespace would collide on a namespace key.
15
+ *
16
+ * The walk is universal over `fidelity/` with one carve-out: `fidelity/authored/`
17
+ * holds *surface* parity, not *type-token* coverage. Those reports have no
18
+ * `totalTypeTokens` to be a fraction of, and their coverage means something else —
19
+ * upstream members declared at the right arity, from `authored-parity.ts`. They get
20
+ * their own ratchet in `authored-parity-floor.json`, so folding them in here would
21
+ * only mean two incompatible denominators under one floor manifest. That manifest
22
+ * also ratchets *two* axes per key (`callableCoverage` and `fieldCoverage`, never
23
+ * averaged), which a flat one-ratio-per-key manifest cannot express — a second,
24
+ * independent reason the two do not merge. Its entries are parsed by
25
+ * `parseAuthoredFloors`, not by `parseFloors` below.
26
+ */
27
+
28
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
29
+ import { join, resolve } from "node:path";
30
+
31
+ export const FIDELITY_DIR = "fidelity";
32
+ /** Directory name under `fidelity/` the token-coverage walk skips — see the
33
+ * module note; ratcheted by `authored-parity-floor.json` instead. */
34
+ export const AUTHORED_PARITY_DIRNAME = "authored";
35
+ export const FLOOR_MANIFEST_FILE = "fidelity-floor.json";
36
+ export const FLOOR_RAISE_COMMAND = "bun run --cwd packages/library-types fidelity:floor";
37
+
38
+ export interface FidelityFloorReport {
39
+ coverage: number;
40
+ totalMembers: number;
41
+ totalTypeTokens: number;
42
+ }
43
+
44
+ function numberField(raw: Record<string, unknown>, field: string, path: string): number {
45
+ const value = raw[field];
46
+ if (typeof value !== "number" || !Number.isFinite(value)) {
47
+ throw new Error(`${path}: expected a finite numeric "${field}", got ${JSON.stringify(value)}`);
48
+ }
49
+ return value;
50
+ }
51
+
52
+ function walkJsonFiles(dir: string, prefix: string, out: string[]): void {
53
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
54
+ const rel = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
55
+ if (entry.isDirectory()) {
56
+ if (prefix === "" && entry.name === AUTHORED_PARITY_DIRNAME) continue;
57
+ walkJsonFiles(join(dir, entry.name), rel, out);
58
+ } else if (entry.name.endsWith(".json")) out.push(rel);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Every committed fidelity report under `fidelity/`, at any depth, keyed by its
64
+ * package-root-relative POSIX path and sorted by that key. Reads only — the walk
65
+ * is the enumeration the gate trusts, so a lane that emits into a nested
66
+ * directory (`fidelity/openapi/`) is picked up without registration.
67
+ */
68
+ export function collectFidelityReports(root: string): Record<string, FidelityFloorReport> {
69
+ const dir = join(root, FIDELITY_DIR);
70
+ if (!existsSync(dir)) return {};
71
+ const relatives: string[] = [];
72
+ walkJsonFiles(dir, "", relatives);
73
+ const reports: Record<string, FidelityFloorReport> = {};
74
+ for (const rel of relatives.sort()) {
75
+ const key = `${FIDELITY_DIR}/${rel}`;
76
+ const raw = JSON.parse(readFileSync(join(dir, rel), "utf8")) as Record<string, unknown>;
77
+ reports[key] = {
78
+ coverage: numberField(raw, "coverage", key),
79
+ totalMembers: numberField(raw, "totalMembers", key),
80
+ totalTypeTokens: numberField(raw, "totalTypeTokens", key),
81
+ };
82
+ }
83
+ return reports;
84
+ }
85
+
86
+ // `JSON.stringify` renders NaN and Infinity as `null` and returns `undefined` for
87
+ // `undefined`, both of which would misreport the value the manifest actually holds.
88
+ function describeFloor(value: unknown): string {
89
+ if (typeof value === "number") return String(value);
90
+ return JSON.stringify(value) ?? String(value);
91
+ }
92
+
93
+ /**
94
+ * The floor manifest's parse-time contract: a plain object of finite numbers in
95
+ * `[0, 1]`. Every consuming assertion compares coercively, so an unvalidated
96
+ * `null` or `false` floor would pass `floor !== undefined` and fail every
97
+ * `coverage < floor` comparison — switching that report's ratchet off silently.
98
+ * Key order is the writer's contract and is preserved here, not sorted.
99
+ */
100
+ export function parseFloors(raw: unknown, path: string): Record<string, number> {
101
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
102
+ throw new Error(`${path}: expected a JSON object of floors, got ${describeFloor(raw)}`);
103
+ }
104
+ const floors: Record<string, number> = {};
105
+ for (const [key, value] of Object.entries(raw)) {
106
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
107
+ throw new Error(
108
+ `${path}: floor "${key}" must be a finite number in [0, 1], got ${describeFloor(value)}`,
109
+ );
110
+ }
111
+ floors[key] = value;
112
+ }
113
+ return floors;
114
+ }
115
+
116
+ /** The committed floor manifest, or an empty manifest when it does not exist yet. */
117
+ export function readFloors(root: string): Record<string, number> {
118
+ const path = join(root, FLOOR_MANIFEST_FILE);
119
+ if (!existsSync(path)) return {};
120
+ return parseFloors(JSON.parse(readFileSync(path, "utf8")), FLOOR_MANIFEST_FILE);
121
+ }
122
+
123
+ /**
124
+ * The next floor manifest: each report's floor rises to its current coverage and
125
+ * never falls, a report with no floor gains one, and a floor whose report is gone
126
+ * is dropped. Monotone by construction — no code path here writes a smaller
127
+ * number, so a regression can only ever be reported by the gate, never absorbed.
128
+ */
129
+ export function raiseFloors(
130
+ floors: Record<string, number>,
131
+ reports: Record<string, FidelityFloorReport>,
132
+ ): Record<string, number> {
133
+ const next: Record<string, number> = {};
134
+ for (const key of Object.keys(reports).sort()) {
135
+ const existing = floors[key];
136
+ const { coverage } = reports[key] as FidelityFloorReport;
137
+ next[key] = existing === undefined ? coverage : Math.max(existing, coverage);
138
+ }
139
+ return next;
140
+ }
141
+
142
+ // `fidelity-floor.json` sits at the package root, so unlike the reports under
143
+ // `fidelity/` it is not biome-excluded and must match Biome's formatting.
144
+ function biomeFormatJson(raw: string): string {
145
+ const out = Bun.spawnSync(
146
+ ["bunx", "biome", "format", `--stdin-file-path=${FLOOR_MANIFEST_FILE}`],
147
+ {
148
+ stdin: Buffer.from(raw),
149
+ },
150
+ );
151
+ if (out.exitCode !== 0) {
152
+ throw new Error(`biome format failed: ${out.stderr.toString()}`);
153
+ }
154
+ return out.stdout.toString();
155
+ }
156
+
157
+ if (import.meta.main) {
158
+ const root = resolve(import.meta.dir, "..");
159
+ const reports = collectFidelityReports(root);
160
+ const current = readFloors(root);
161
+ const next = raiseFloors(current, reports);
162
+
163
+ if (process.argv.includes("--raise")) {
164
+ for (const key of Object.keys(next)) {
165
+ const before = current[key];
166
+ if (before === undefined) console.log(`${key}: (new) -> ${next[key]}`);
167
+ else if (before !== next[key]) console.log(`${key}: ${before} -> ${next[key]}`);
168
+ }
169
+ for (const key of Object.keys(current)) {
170
+ if (next[key] === undefined) console.log(`${key}: dropped (no such report)`);
171
+ }
172
+ const path = join(root, FLOOR_MANIFEST_FILE);
173
+ writeFileSync(path, biomeFormatJson(JSON.stringify(next)));
174
+ console.log(`wrote ${path}`);
175
+ } else {
176
+ console.log(JSON.stringify(next, null, 2));
177
+ }
178
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Carrying upstream's own LuaDoc into the authored lane's api-doc, for members the
3
+ * fork documents nowhere.
4
+ *
5
+ * `parse-lua-surface.ts` already reads each upstream member's `---` block and
6
+ * `authored-parity.ts` already counts how many of them the fork drops, so the text
7
+ * exists on one side of the pipeline and was thrown away before the docs-site saw
8
+ * it. This module is the merge, and it runs at api-doc lowering rather than in the
9
+ * vendored `.d.ts`: writing upstream prose into `fixtures/authored/*.d.ts` would
10
+ * still pass the forked-vs-generated identity diff while destroying what that diff
11
+ * exists to prove — that the emitted surface *is* the vendored fork — and imported
12
+ * prose would be indistinguishable from fork prose forever after.
13
+ *
14
+ * Three rules carry the whole merge:
15
+ *
16
+ * - **Fork prose always wins.** An element the fork gave either a `brief` or a
17
+ * `description` is returned untouched and gains no `docSource`, so authoring the
18
+ * fork's own doc-comment is how a member opts out. There is no exclusion list:
19
+ * the only thing one would add is "show no prose at all for a member upstream
20
+ * documented", which is worse than writing the correct brief.
21
+ * - **Tags never come with the summary.** Every block in this corpus is a summary
22
+ * followed by `@param`/`@return` lines, and a tag's continuation lines carry no
23
+ * marker of their own, so truncating at the *first* `@` line is what keeps
24
+ * LuaDoc-derived types and parameter names out of the api-doc.
25
+ * - **A brief never restates its own symbol.** A leading line that is exactly the
26
+ * element's name is upstream's heading, not prose, and is dropped before the brief is
27
+ * taken. That is the importer's knowledge, not `summarizeLuaDoc`'s, which stays a pure
28
+ * block-to-prose function.
29
+ */
30
+
31
+ import type { LuaMember } from "./parse-lua-surface";
32
+
33
+ /** One `api-doc/<namespace>.json` element, open at the edges: this module reads the
34
+ * four keys the merge turns on and passes every other key through in place. */
35
+ export interface ApiDocElement {
36
+ type: string;
37
+ name: string;
38
+ global?: boolean;
39
+ brief?: string;
40
+ description?: string;
41
+ docSource?: string;
42
+ [key: string]: unknown;
43
+ }
44
+
45
+ /**
46
+ * The prose half of a LuaDoc block: its lines up to the first whose trimmed text
47
+ * starts with `@`, rejoined and trimmed. Empty for a block that is only tags.
48
+ *
49
+ * Interior newlines and blank lines survive. Markdown collapses a soft break at
50
+ * render time, so no unwrapping is done and the imported description stays
51
+ * upstream's own text.
52
+ */
53
+ export function summarizeLuaDoc(doc: string): string {
54
+ const summary: string[] = [];
55
+ for (const line of doc.split("\n")) {
56
+ if (line.trim().startsWith("@")) break;
57
+ summary.push(line);
58
+ }
59
+ return summary.join("\n").trim();
60
+ }
61
+
62
+ /** The summary without a leading line that is only the element's own name. nakama writes
63
+ * `--- <member_name>` above the real summary, and `briefOf` takes the first line, so
64
+ * leaving it in ships a brief that restates the symbol it sits under. The comparison is
65
+ * exact: a first line that merely mentions the name is prose and survives. */
66
+ function withoutNameLine(summary: string, name: string): string {
67
+ const [first, ...rest] = summary.split("\n");
68
+ return (first as string).trim() === name ? rest.join("\n").trim() : summary;
69
+ }
70
+
71
+ /** The element with upstream's summary in place, `docSource` inserted immediately
72
+ * after `description` by copying the element's own entries in order rather than
73
+ * assigning onto it — the key order is what the committed api-doc golden diffs on. */
74
+ function withImportedDoc(element: ApiDocElement, summary: string): ApiDocElement {
75
+ const brief = summary.split("\n")[0]?.trim() ?? "";
76
+ const imported: ApiDocElement = { type: element.type, name: element.name };
77
+ let placedBrief = false;
78
+ let placedDescription = false;
79
+ for (const [key, value] of Object.entries(element)) {
80
+ if (key === "type" || key === "name") continue;
81
+ if (key === "brief") {
82
+ imported.brief = brief;
83
+ placedBrief = true;
84
+ } else if (key === "description") {
85
+ imported.description = summary;
86
+ imported.docSource = "upstream";
87
+ placedDescription = true;
88
+ } else imported[key] = value;
89
+ }
90
+ if (!placedDescription) {
91
+ imported.description = summary;
92
+ imported.docSource = "upstream";
93
+ }
94
+ if (!placedBrief) imported.brief = brief;
95
+ return imported;
96
+ }
97
+
98
+ /**
99
+ * The api-doc element list with upstream prose merged in, as a new list.
100
+ *
101
+ * An element is imported when it is a `FUNCTION` or a `VARIABLE` — the two kinds
102
+ * `authored-parity.ts` compares, a `TYPEDEF` being a type rather than a runtime
103
+ * member — is not `global: true`, has an empty `brief` *and* an empty
104
+ * `description`, matches an upstream member by exact name, and that member's
105
+ * summary survives tag stripping. Every other element is returned unchanged.
106
+ *
107
+ * The name match is exact on purpose: a namespace-qualified api-doc name
108
+ * (`bridge.bridge`) never matches a bare upstream name, which is the same non-match
109
+ * the parity pass makes.
110
+ */
111
+ export function importUpstreamDocs(
112
+ elements: readonly ApiDocElement[],
113
+ members: ReadonlyMap<string, LuaMember>,
114
+ ): ApiDocElement[] {
115
+ return elements.map((element) => {
116
+ if (element.type !== "FUNCTION" && element.type !== "VARIABLE") return element;
117
+ if (element.global === true) return element;
118
+ if ((element.brief ?? "") !== "" || (element.description ?? "") !== "") return element;
119
+ const member = members.get(element.name);
120
+ if (member === undefined) return element;
121
+ const summary = withoutNameLine(summarizeLuaDoc(member.doc), element.name);
122
+ if (summary === "") return element;
123
+ return withImportedDoc(element, summary);
124
+ });
125
+ }
@@ -15,6 +15,7 @@
15
15
 
16
16
  import {
17
17
  buildModelContext,
18
+ type ExternalTypeRef,
18
19
  isPublicField,
19
20
  isPublicMethod,
20
21
  mapTypes,
@@ -67,9 +68,13 @@ function functionElement(method: LibraryMethod, ctx: MapContext): Record<string,
67
68
  parameterElement(param, optionalFlags[index] ?? false, fnCtx),
68
69
  ),
69
70
  returnvalues: method.returns.map((ret) => returnElement(ret, fnCtx)),
71
+ ...(method.deprecated !== undefined ? { deprecated: method.deprecated } : {}),
70
72
  };
71
73
  }
72
74
 
75
+ // No `deprecated` spread here or on `variableElement`: both take a `LibraryField`,
76
+ // and a one-line `---@field` has no slot for a block tag in this dialect, so a
77
+ // property can never carry one.
73
78
  function propertyElement(field: LibraryField, ctx: MapContext): Record<string, unknown> {
74
79
  return {
75
80
  name: field.name,
@@ -93,9 +98,17 @@ function variableElement(field: LibraryField, ctx: MapContext): Record<string, u
93
98
 
94
99
  export function lowerLibraryModel(
95
100
  model: LibraryModel,
96
- { namespace, typeRenames }: { namespace: string; typeRenames?: Record<string, string> },
101
+ {
102
+ namespace,
103
+ typeRenames,
104
+ externalTypes,
105
+ }: {
106
+ namespace: string;
107
+ typeRenames?: Record<string, string>;
108
+ externalTypes?: Record<string, ExternalTypeRef> | undefined;
109
+ },
97
110
  ): unknown {
98
- const ctx = buildModelContext(model, typeRenames);
111
+ const ctx = buildModelContext(model, typeRenames, externalTypes);
99
112
  const elements: Record<string, unknown>[] = [];
100
113
 
101
114
  for (const fn of model.moduleFunctions) {
@@ -125,11 +138,16 @@ export function lowerLibraryModel(
125
138
  name: sanitizeTypeName(iface.name),
126
139
  ...(functions.length > 0 ? { functions } : {}),
127
140
  ...(properties.length > 0 ? { properties } : {}),
141
+ ...(iface.deprecated !== undefined ? { deprecated: iface.deprecated } : {}),
128
142
  });
129
143
  }
130
144
 
131
145
  for (const alias of model.aliases) {
132
- elements.push({ type: "TYPEDEF", name: sanitizeTypeName(alias.name) });
146
+ elements.push({
147
+ type: "TYPEDEF",
148
+ name: sanitizeTypeName(alias.name),
149
+ ...(alias.deprecated !== undefined ? { deprecated: alias.deprecated } : {}),
150
+ });
133
151
  }
134
152
 
135
153
  // The module's own `@class` carries the library's summary; use it as the page
@@ -10,7 +10,12 @@
10
10
  * sorted-unique token list so the gap is visible instead of silent.
11
11
  */
12
12
 
13
- import { isPublicField, isPublicMethod } from "./emit-library-dts";
13
+ import {
14
+ buildModelContext,
15
+ type ExternalTypeRef,
16
+ isPublicField,
17
+ isPublicMethod,
18
+ } from "./emit-library-dts";
14
19
  import { type MapContext, mapLualsType, scopeGenerics } from "./map-luals-types";
15
20
  import type { LibraryGeneric, LibraryModel } from "./parse-luals";
16
21
 
@@ -29,22 +34,22 @@ function round3(value: number): number {
29
34
  }
30
35
 
31
36
  /**
32
- * Build the fidelity report for one namespace. `knownNames` is drawn from the
33
- * model's own interface and alias names so a reference to a sibling library type
34
- * resolves rather than falling to `unknown`. Every field, every param and return
35
- * of every method and module function, and every alias expression is mapped;
36
- * `undocumentedMembers` counts fields/methods/moduleFunctions whose doc or brief
37
- * is empty. Deterministic; no I/O.
37
+ * Build the fidelity report for one namespace. Tokens map through the same
38
+ * `buildModelContext` the emitter and the api-doc lowering use, so all three agree
39
+ * on what resolves the model's own interface and alias names, plus the
40
+ * `externalTypes` tokens reached through a cross-module import and all three
41
+ * reject an external alias that collides with a declared name. Every field, every
42
+ * param and return of every method and module function, and every alias expression
43
+ * is mapped; `undocumentedMembers` counts fields/methods/moduleFunctions whose doc
44
+ * or brief is empty. Deterministic; no I/O.
38
45
  */
39
46
  export function buildFidelityReport(
40
47
  namespace: string,
41
48
  model: LibraryModel,
42
49
  typeRenames: Record<string, string>,
50
+ externalTypes?: Record<string, ExternalTypeRef>,
43
51
  ): FidelityReport {
44
- const knownNames = new Set<string>();
45
- for (const iface of model.interfaces) knownNames.add(iface.name);
46
- for (const alias of model.aliases) knownNames.add(alias.name);
47
- const ctx: MapContext = { knownNames, typeRenames };
52
+ const ctx = buildModelContext(model, typeRenames, externalTypes);
48
53
 
49
54
  let totalMembers = 0;
50
55
  let totalTypeTokens = 0;
@@ -13,6 +13,7 @@
13
13
  * verbatim and the emitter sanitizes it later.
14
14
  */
15
15
 
16
+ import { luaMultiReturn } from "@defold-typescript/types";
16
17
  import { CORE_TYPE_RENAMES } from "./sync-library-types";
17
18
 
18
19
  export interface MapContext {
@@ -52,6 +53,24 @@ const SCALARS: Readonly<Record<string, string>> = {
52
53
  any: "unknown",
53
54
  };
54
55
 
56
+ /**
57
+ * LuaLS's bare `function` — any callable, signature unspecified. Params are `any[]`
58
+ * because under `strictFunctionTypes` an `unknown[]` rest would reject every concrete
59
+ * callback a consumer passes; the return stays `unknown` because return position is
60
+ * covariant, so `any` there would only leak unchecked values into call sites.
61
+ */
62
+ const CALLABLE_UNSPECIFIED = "(...args: any[]) => unknown";
63
+
64
+ /**
65
+ * LuaLS's two placeholders: the throwaway param name `_`, and a bare `...` in vararg
66
+ * or return position. Neither declares a type at all, so lowering it to `unknown`
67
+ * loses nothing an author wrote — the same reasoning that already exempts `any` from
68
+ * the fallback count. The boundary is deliberate: an untyped `self` or `ctx` *is* an
69
+ * upstream omission and stays recorded.
70
+ */
71
+ const LUALS_THROWAWAY_PARAM = "_";
72
+ export const LUALS_VARARG_TOKEN = "...";
73
+
55
74
  /**
56
75
  * Split `s` on every top-level occurrence of the single-character `sep`, honoring
57
76
  * bracket depth and double-quoted string literals so a separator nested inside
@@ -146,21 +165,18 @@ function functionParts(
146
165
  const paramList = params
147
166
  .map((raw) => raw.trim())
148
167
  .map((part) => {
149
- if (part.startsWith("...")) {
168
+ if (part.startsWith(LUALS_VARARG_TOKEN)) {
150
169
  const after = part.slice(3).trim();
151
- let element: string;
152
- if (after.startsWith(":")) {
153
- element = mapToken(after.slice(1).trim(), ctx, unknowns);
154
- } else {
155
- element = "unknown";
156
- unknowns.push("...");
157
- }
170
+ const element = after.startsWith(":")
171
+ ? mapToken(after.slice(1).trim(), ctx, unknowns)
172
+ : "unknown";
158
173
  return `...args: ${needsArrayParens(element) ? `(${element})[]` : `${element}[]`}`;
159
174
  }
160
175
  const colon = splitTopLevel(part, ":");
161
176
  if (colon.length < 2) {
162
- // Untyped param (`self`, `_`, `ctx`): a recorded gap, not a silent `any`.
163
- unknowns.push(part);
177
+ // Untyped param (`self`, `ctx`): a recorded gap, not a silent `any`. A bare `_`
178
+ // is LuaLS's deliberate throwaway and records nothing.
179
+ if (part !== LUALS_THROWAWAY_PARAM) unknowns.push(part);
164
180
  return `${part}: unknown`;
165
181
  }
166
182
  const name = colon[0]?.trim() ?? "";
@@ -177,8 +193,11 @@ function functionParts(
177
193
  if (retTokens.length === 1) {
178
194
  ret = mapToken(retTokens[0] as string, ctx, unknowns);
179
195
  } else if (retTokens.length > 1) {
180
- const inner = retTokens.map((r) => mapToken(r, ctx, unknowns)).join(", ");
181
- ret = `LuaMultiReturn<[${inner}]>`;
196
+ const restTail = retTokens.at(-1) === LUALS_VARARG_TOKEN;
197
+ ret = luaMultiReturn(
198
+ retTokens.map((r) => mapToken(r, ctx, unknowns)),
199
+ restTail,
200
+ );
182
201
  }
183
202
  }
184
203
  return { paramList, ret };
@@ -215,15 +234,11 @@ function mapToken(raw: string, ctx: MapContext, unknowns: string[]): string {
215
234
 
216
235
  if (token === "") return "unknown";
217
236
 
218
- // Optional suffix.
219
- if (token.length > 1 && token.endsWith("?")) {
220
- const base = mapToken(token.slice(0, -1), ctx, unknowns);
221
- const members = splitTopLevel(base, "|").map((m) => m.trim());
222
- return members.includes("undefined") ? base : `${base} | undefined`;
223
- }
224
-
225
- // A `fun(...)` whose return follows the `)` keeps its return-type `|` inside the
226
- // function; splitting the union first would cut `fun(): a|b` into `(fun) | b`.
237
+ // A `fun(...)` whose return follows the `)` keeps its return-type `|` and `?` inside
238
+ // the function; splitting the union first would cut `fun(): a|b` into `(fun) | b`, and
239
+ // peeling the optional suffix first would turn `fun(): number?` — a function with an
240
+ // optional *return* into an optional function. The whole-function optional is spelled
241
+ // with explicit parentheses, `(fun(): number)?`, which does not match here.
227
242
  // `fun()|nil` (a `|` right after the `)`) falls through to the union split.
228
243
  if (/^fun\s*\(/.test(token)) {
229
244
  const close = matchBracket(token, token.indexOf("("));
@@ -233,6 +248,13 @@ function mapToken(raw: string, ctx: MapContext, unknowns: string[]): string {
233
248
  }
234
249
  }
235
250
 
251
+ // Optional suffix.
252
+ if (token.length > 1 && token.endsWith("?")) {
253
+ const base = mapToken(token.slice(0, -1), ctx, unknowns);
254
+ const members = splitTopLevel(base, "|").map((m) => m.trim());
255
+ return members.includes("undefined") ? base : `${wrapForUnion(base)} | undefined`;
256
+ }
257
+
236
258
  // Top-level union.
237
259
  const unionParts = splitTopLevel(token, "|");
238
260
  if (unionParts.length > 1) {
@@ -263,6 +285,15 @@ function mapToken(raw: string, ctx: MapContext, unknowns: string[]): string {
263
285
  // String literal — passthrough.
264
286
  if (token.startsWith('"') && token.endsWith('"')) return token;
265
287
 
288
+ // Signature-less callable. Placed after every composite branch so `function[]`,
289
+ // `function|nil`, and `fun(cb: function)` route through those first.
290
+ if (token === "function") return CALLABLE_UNSPECIFIED;
291
+
292
+ // Placeholder vararg in return position — not a `SCALARS` entry, which maps real Lua
293
+ // type names. Sits beside the callable branch so `...[]` and `...|nil` reach their
294
+ // structural handlers first.
295
+ if (token === LUALS_VARARG_TOKEN) return "unknown";
296
+
266
297
  // Scalars.
267
298
  const scalar = SCALARS[token];
268
299
  if (scalar !== undefined) return scalar;