@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
@@ -15,7 +15,12 @@
15
15
  * `name` is scoped into a child `MapContext` (an identity rename) so a bare `T`
16
16
  * resolves to `T` instead of lowering to `unknown`, constraints and `extends`
17
17
  * targets map through the existing rename map, and an `extends` clause is emitted
18
- * only for parents that resolve to a declared interface.
18
+ * only for parents that resolve to a declared interface or an imported external type.
19
+ *
20
+ * A token owned by another target is declared through `externalTypes`: the emitter
21
+ * writes a real `import { <name> } from '<module>';` into the `declare module` block
22
+ * and resolves the token to that binding, so a cross-library reference carries its
23
+ * real type instead of lowering to `unknown`.
19
24
  */
20
25
 
21
26
  import {
@@ -26,6 +31,7 @@ import {
26
31
  varargElementType,
27
32
  } from "@defold-typescript/types";
28
33
  import {
34
+ LUALS_VARARG_TOKEN,
29
35
  type MapContext,
30
36
  mapLualsCallSignature,
31
37
  mapLualsType,
@@ -42,9 +48,20 @@ import type {
42
48
  LibraryParam,
43
49
  } from "./parse-luals";
44
50
 
51
+ /**
52
+ * A type token owned by another target: the module id that declares it and the name
53
+ * it exports. Resolving one emits a real `import` into the `declare module` block
54
+ * rather than lowering the token to `unknown`.
55
+ */
56
+ export interface ExternalTypeRef {
57
+ module: string;
58
+ name: string;
59
+ }
60
+
45
61
  export interface EmitLibraryOptions {
46
62
  moduleId: string;
47
63
  typeRenames?: Record<string, string>;
64
+ externalTypes?: Record<string, ExternalTypeRef> | undefined;
48
65
  }
49
66
 
50
67
  const INDENT = "\t";
@@ -85,6 +102,42 @@ function safeParamName(name: string, index: number): string {
85
102
  return TS_RESERVED_NAMES.has(name) ? `${name}_` : name;
86
103
  }
87
104
 
105
+ /**
106
+ * The token -> local-alias rename map an `externalTypes` config contributes, aliasing
107
+ * exactly as a declared name would (dotted `event.foo` -> `event_foo`). Shared by the
108
+ * emitter, the fidelity report, and the api-doc lowering so all three resolve an
109
+ * external reference to the same identifier the import binds.
110
+ */
111
+ export function externalTypeRenames(
112
+ externalTypes?: Record<string, ExternalTypeRef>,
113
+ ): Record<string, string> {
114
+ const renames: Record<string, string> = {};
115
+ for (const token of Object.keys(externalTypes ?? {})) renames[token] = sanitizeTypeName(token);
116
+ return renames;
117
+ }
118
+
119
+ /**
120
+ * `import { ... } from '<module>';` lines for the external tokens, one per source
121
+ * module with its names sorted and the modules sorted by id, so re-rendering is
122
+ * byte-identical. A token whose local alias differs from the export name binds
123
+ * through an `as` clause.
124
+ */
125
+ function renderExternalImports(externalTypes?: Record<string, ExternalTypeRef>): string[] {
126
+ const byModule = new Map<string, Set<string>>();
127
+ for (const [token, ref] of Object.entries(externalTypes ?? {})) {
128
+ const alias = sanitizeTypeName(token);
129
+ const binding = alias === ref.name ? ref.name : `${ref.name} as ${alias}`;
130
+ const names = byModule.get(ref.module) ?? new Set<string>();
131
+ names.add(binding);
132
+ byModule.set(ref.module, names);
133
+ }
134
+ return [...byModule.entries()]
135
+ .sort(([a], [b]) => (a < b ? -1 : 1))
136
+ .map(
137
+ ([module, names]) => `${INDENT}import { ${[...names].sort().join(", ")} } from '${module}';`,
138
+ );
139
+ }
140
+
88
141
  export function mapTypes(types: readonly string[], ctx: MapContext): string {
89
142
  if (types.length === 0) return "unknown";
90
143
  return types.map((token) => mapLualsType(token, ctx).ts).join(" | ");
@@ -107,24 +160,29 @@ export function renderGenericParams(generics: readonly LibraryGeneric[], ctx: Ma
107
160
  function renderHookReturn(returnTokens: readonly string[], ctx: MapContext): string {
108
161
  if (returnTokens.length === 0) return "void";
109
162
  if (returnTokens.length === 1) return mapTypes([returnTokens[0] as string], ctx);
110
- return luaMultiReturn(returnTokens.map((token) => mapTypes([token], ctx)));
163
+ return luaMultiReturn(
164
+ returnTokens.map((token) => mapTypes([token], ctx)),
165
+ returnTokens.at(-1) === LUALS_VARARG_TOKEN,
166
+ );
111
167
  }
112
168
 
113
169
  /**
114
170
  * An ` extends X, Y` clause built from `iface.extends` split on commas, keeping only
115
- * parents that name a declared interface (so no `extends unknown` is ever emitted),
116
- * mapped through the rename map; `""` when none survive.
171
+ * parents that name a declared interface or an imported external type (so no
172
+ * `extends unknown` is ever emitted), mapped through the rename map; `""` when none
173
+ * survive.
117
174
  */
118
175
  function renderExtends(
119
176
  iface: LibraryInterface,
120
177
  ctx: MapContext,
121
178
  interfaceNames: ReadonlySet<string>,
179
+ externalTokens: ReadonlySet<string>,
122
180
  ): string {
123
181
  if (!iface.extends) return "";
124
182
  const parents = iface.extends
125
183
  .split(",")
126
184
  .map((name) => name.trim())
127
- .filter((name) => interfaceNames.has(name))
185
+ .filter((name) => interfaceNames.has(name) || externalTokens.has(name))
128
186
  .map((name) => mapTypes([name], ctx));
129
187
  return parents.length > 0 ? ` extends ${parents.join(", ")}` : "";
130
188
  }
@@ -171,16 +229,22 @@ function renderParams(params: readonly LibraryParam[], ctx: MapContext): string
171
229
  function renderReturn(returns: readonly LibraryParam[], ctx: MapContext): string {
172
230
  if (returns.length === 0) return "void";
173
231
  if (returns.length === 1) return mapTypes((returns[0] as LibraryParam).types, ctx);
174
- return luaMultiReturn(returns.map((ret) => mapTypes(ret.types, ctx)));
232
+ const last = returns[returns.length - 1] as LibraryParam;
233
+ const restTail = last.types.length === 1 && last.types[0] === LUALS_VARARG_TOKEN;
234
+ return luaMultiReturn(
235
+ returns.map((ret) => mapTypes(ret.types, ctx)),
236
+ restTail,
237
+ );
175
238
  }
176
239
 
177
- function pushDoc(lines: string[], summary: string, indent: string): void {
178
- for (const line of renderDocComment({ summary })) lines.push(`${indent}${line}`);
240
+ function pushDoc(lines: string[], summary: string, indent: string, deprecated?: string): void {
241
+ const parts = deprecated !== undefined ? { summary, deprecated } : { summary };
242
+ for (const line of renderDocComment(parts)) lines.push(`${indent}${line}`);
179
243
  }
180
244
 
181
245
  function renderAlias(alias: LibraryAlias, ctx: MapContext): string[] {
182
246
  const lines: string[] = [];
183
- pushDoc(lines, alias.doc, INDENT);
247
+ pushDoc(lines, alias.doc, INDENT, alias.deprecated);
184
248
  lines.push(`${INDENT}type ${sanitizeTypeName(alias.name)} = ${mapTypes(alias.types, ctx)};`);
185
249
  return lines;
186
250
  }
@@ -189,12 +253,13 @@ function renderInterface(
189
253
  iface: LibraryInterface,
190
254
  ctx: MapContext,
191
255
  interfaceNames: ReadonlySet<string>,
256
+ externalTokens: ReadonlySet<string>,
192
257
  ): string[] {
193
258
  const lines: string[] = [];
194
- pushDoc(lines, iface.brief, INDENT);
259
+ pushDoc(lines, iface.brief, INDENT, iface.deprecated);
195
260
  const ifaceCtx = scopeGenerics(ctx, iface.generics);
196
261
  const params = renderGenericParams(iface.generics, ifaceCtx);
197
- const extendsClause = renderExtends(iface, ifaceCtx, interfaceNames);
262
+ const extendsClause = renderExtends(iface, ifaceCtx, interfaceNames, externalTokens);
198
263
  lines.push(`${INDENT}interface ${sanitizeTypeName(iface.name)}${params}${extendsClause} {`);
199
264
  const body = INDENT + INDENT;
200
265
  for (const field of iface.fields) {
@@ -216,7 +281,7 @@ function renderInterface(
216
281
  }
217
282
  for (const method of iface.methods) {
218
283
  if (!isPublicMethod(method)) continue;
219
- pushDoc(lines, method.brief, body);
284
+ pushDoc(lines, method.brief, body, method.deprecated);
220
285
  const methodCtx = scopeGenerics(ifaceCtx, method.generics);
221
286
  const methodParams = renderGenericParams(method.generics, methodCtx);
222
287
  lines.push(
@@ -258,7 +323,7 @@ function renderModuleConstants(iface: LibraryInterface, ctx: MapContext): string
258
323
 
259
324
  function renderModuleFunction(fn: LibraryMethod, ctx: MapContext): string[] {
260
325
  const lines: string[] = [];
261
- pushDoc(lines, fn.brief, INDENT);
326
+ pushDoc(lines, fn.brief, INDENT, fn.deprecated);
262
327
  const fnCtx = scopeGenerics(ctx, fn.generics);
263
328
  const genericParams = renderGenericParams(fn.generics, fnCtx);
264
329
  const params = renderParams(fn.params, fnCtx);
@@ -290,6 +355,7 @@ function renderModuleFunction(fn: LibraryMethod, ctx: MapContext): string[] {
290
355
  export function buildModelContext(
291
356
  model: LibraryModel,
292
357
  typeRenames?: Record<string, string>,
358
+ externalTypes?: Record<string, ExternalTypeRef>,
293
359
  ): MapContext {
294
360
  const declaredNames = [
295
361
  ...model.interfaces.map((iface) => iface.name),
@@ -297,9 +363,18 @@ export function buildModelContext(
297
363
  ];
298
364
  const nameRenames: Record<string, string> = {};
299
365
  for (const name of declaredNames) nameRenames[name] = sanitizeTypeName(name);
366
+ const externalRenames = externalTypeRenames(externalTypes);
367
+ for (const [token, alias] of Object.entries(externalRenames)) {
368
+ const collision = declaredNames.find((name) => sanitizeTypeName(name) === alias);
369
+ if (collision !== undefined) {
370
+ throw new Error(
371
+ `buildModelContext: external type "${token}" resolves to local name "${alias}", which the model already declares as "${collision}".`,
372
+ );
373
+ }
374
+ }
300
375
  return {
301
- knownNames: new Set(declaredNames),
302
- typeRenames: { ...(typeRenames ?? {}), ...nameRenames },
376
+ knownNames: new Set([...declaredNames, ...Object.values(externalRenames)]),
377
+ typeRenames: { ...(typeRenames ?? {}), ...externalRenames, ...nameRenames },
303
378
  };
304
379
  }
305
380
 
@@ -310,10 +385,12 @@ export function buildModelContext(
310
385
  * `unknown` inside `mapLualsType` exactly as the fidelity report records them.
311
386
  */
312
387
  export function emitLibraryDeclarations(model: LibraryModel, opts: EmitLibraryOptions): string {
313
- const ctx = buildModelContext(model, opts.typeRenames);
388
+ const ctx = buildModelContext(model, opts.typeRenames, opts.externalTypes);
314
389
 
315
390
  const interfaceNames = new Set(model.interfaces.map((iface) => iface.name));
391
+ const externalTokens = new Set(Object.keys(opts.externalTypes ?? {}));
316
392
  const out: string[] = ["/** @noResolution */", `declare module '${opts.moduleId}' {`];
393
+ out.push(...renderExternalImports(opts.externalTypes));
317
394
  for (const alias of model.aliases) out.push(...renderAlias(alias, ctx));
318
395
  for (const iface of model.interfaces) {
319
396
  // The module object's fields become module-level `export const`s (below), so it is
@@ -322,7 +399,7 @@ export function emitLibraryDeclarations(model: LibraryModel, opts: EmitLibraryOp
322
399
  out.push(...renderModuleConstants(iface, ctx));
323
400
  continue;
324
401
  }
325
- out.push(...renderInterface(iface, ctx, interfaceNames));
402
+ out.push(...renderInterface(iface, ctx, interfaceNames, externalTokens));
326
403
  }
327
404
  for (const fn of model.moduleFunctions) {
328
405
  if (!isPublicMethod(fn)) continue;
@@ -3,15 +3,19 @@ import ts from "typescript";
3
3
  const printer = ts.createPrinter({ removeComments: true });
4
4
 
5
5
  /**
6
- * A type node's text as a single comment-free line. The printer re-emits the
7
- * AST (so a `//`/`/*` inside a string-literal type is never mistaken for a
6
+ * Any node's text as a single comment-free line. The printer re-emits the AST
7
+ * (so a `//`/`/*` inside a string-literal type is never mistaken for a
8
8
  * comment), then interior whitespace is collapsed so multi-line object literals
9
9
  * and wrapped unions no longer leak newlines or member JSDoc into `/api`.
10
10
  */
11
- function typeText(node: ts.TypeNode, sf: ts.SourceFile): string {
11
+ function oneLineText(node: ts.Node, sf: ts.SourceFile): string {
12
12
  return printer.printNode(ts.EmitHint.Unspecified, node, sf).replace(/\s+/g, " ").trim();
13
13
  }
14
14
 
15
+ function typeText(node: ts.TypeNode, sf: ts.SourceFile): string {
16
+ return oneLineText(node, sf);
17
+ }
18
+
15
19
  interface Field {
16
20
  name: string;
17
21
  doc: string;
@@ -80,52 +84,88 @@ export function extractApiDoc(source: string, moduleName: string): unknown {
80
84
  // vendors both `export function` and bare `function` for the same intent
81
85
  // (`rendy.rendy`, `in.onscreen`). With an `export =`, the bare declarations
82
86
  // are internal plumbing behind the re-exported value, so they stay unemitted
83
- // and the value's interface drives the surface instead (`squid`, `starly`).
87
+ // and the value's interface drives the surface instead (`squid`).
84
88
  const emitBare = !statements.some(ts.isExportAssignment);
85
89
 
86
90
  const elements: Record<string, unknown>[] = [];
87
91
  const emittedNames = new Set<string>();
88
92
  const referencedTypeNodes: ts.TypeNode[] = [];
93
+ // Type nodes reached from file-scope declarations, kept apart from the module
94
+ // block's so an ambient global can never pull a module-block shape onto `/api`
95
+ // that the module surface alone did not reach.
96
+ const globalTypeNodes: ts.TypeNode[] = [];
97
+ // Each alias's declaration paired with the bare element it already pushed, so
98
+ // a reachable object-literal alias is filled in by merging into that element
99
+ // rather than by appending a second one — element order stays as emitted.
100
+ const aliasElements: { node: ts.TypeAliasDeclaration; element: Record<string, unknown> }[] = [];
89
101
 
90
102
  // Members nested in an `export namespace` (`bridge.bridge`) keep their
91
103
  // namespace path so same-named members across namespaces (e.g. `is_supported`)
92
104
  // stay distinct instead of colliding in `emittedNames`.
93
- const collect = (nodes: readonly ts.Statement[], prefix: string): void => {
105
+ //
106
+ // `global` switches the walk to the ambient lane: the file's own statements
107
+ // rather than the module block's. There the gate is the `declare` modifier
108
+ // (there is no `export =` to suppress bare declarations, so `emitBare` does not
109
+ // apply), each element carries the `global` marker, and a type alias is not
110
+ // pushed eagerly — file scope carries far more incidental type machinery than a
111
+ // module block, so a shape publishes only once reachability proves a published
112
+ // signature names it.
113
+ const collect = (nodes: readonly ts.Statement[], prefix: string, global: boolean): void => {
94
114
  const qualify = (name: string): string => (prefix ? `${prefix}.${name}` : name);
115
+ const mark = global ? { global: true } : {};
116
+ const typeNodes = global ? globalTypeNodes : referencedTypeNodes;
117
+ // Inside an ambient namespace every member is ambient by containment, so the
118
+ // nested lane emits regardless of an `export` keyword.
119
+ const gate = (stmt: ts.HasModifiers): boolean =>
120
+ global ? prefix !== "" || isAmbient(stmt) || isExported(stmt) : isExported(stmt) || emitBare;
95
121
  for (const stmt of nodes) {
96
- if (ts.isFunctionDeclaration(stmt) && stmt.name && (isExported(stmt) || emitBare)) {
97
- collectFunctionReferenceTypes(stmt, referencedTypeNodes);
122
+ if (ts.isFunctionDeclaration(stmt) && stmt.name && gate(stmt)) {
123
+ collectFunctionReferenceTypes(stmt, typeNodes);
98
124
  const name = qualify(stmt.name.text);
99
- elements.push(functionElement(stmt, name, sf));
125
+ elements.push({ ...functionElement(stmt, name, sf), ...mark });
100
126
  emittedNames.add(name);
101
- } else if (ts.isVariableStatement(stmt) && (isExported(stmt) || emitBare)) {
127
+ } else if (ts.isVariableStatement(stmt) && gate(stmt)) {
128
+ // The `VariableStatement` carries the JSDoc, not the declaration, so both the
129
+ // summary and the deprecation tag are read once from the statement and shared
130
+ // by every declarator under it.
131
+ const summary = jsDocSummary(stmt);
102
132
  for (const decl of stmt.declarationList.declarations) {
103
- if (decl.type) referencedTypeNodes.push(decl.type);
133
+ if (decl.type) typeNodes.push(decl.type);
104
134
  const fields = objectFields(decl.type, sf);
105
135
  const name = qualify(decl.name.getText(sf));
106
136
  elements.push({
107
137
  type: "VARIABLE",
108
138
  name,
139
+ // Unconditional, matching `functionElement`: an absent key and an empty one
140
+ // would be two shapes the docs-site had to tell apart for no gain.
141
+ brief: briefOf(summary),
142
+ description: summary,
109
143
  types: decl.type ? [typeText(decl.type, sf)] : [],
110
144
  ...(fields ? { fields } : {}),
145
+ ...deprecatedKey(stmt),
146
+ ...mark,
111
147
  });
112
148
  emittedNames.add(name);
113
149
  }
114
150
  } else if (ts.isTypeAliasDeclaration(stmt)) {
151
+ if (global) continue;
115
152
  const name = qualify(stmt.name.text);
116
- elements.push({ type: "TYPEDEF", name });
153
+ const element: Record<string, unknown> = { type: "TYPEDEF", name };
154
+ elements.push(element);
117
155
  emittedNames.add(name);
156
+ aliasElements.push({ node: stmt, element });
118
157
  } else if (
119
158
  ts.isModuleDeclaration(stmt) &&
159
+ !ts.isStringLiteral(stmt.name) &&
120
160
  stmt.body &&
121
161
  ts.isModuleBlock(stmt.body) &&
122
- (isExported(stmt) || emitBare)
162
+ gate(stmt)
123
163
  ) {
124
- collect(stmt.body.statements, qualify(stmt.name.getText(sf)));
164
+ collect(stmt.body.statements, qualify(stmt.name.getText(sf)), global);
125
165
  }
126
166
  }
127
167
  };
128
- collect(statements, "");
168
+ collect(statements, "", false);
129
169
 
130
170
  const moduleValueInterfaces = exportedValueInterfaces(moduleBlock);
131
171
  for (const iface of moduleValueInterfaces) {
@@ -145,6 +185,7 @@ export function extractApiDoc(source: string, moduleName: string): unknown {
145
185
  name,
146
186
  types: member.type ? [typeText(member.type, sf)] : [],
147
187
  ...(fields ? { fields } : {}),
188
+ ...deprecatedKey(member),
148
189
  });
149
190
  emittedNames.add(name);
150
191
  }
@@ -152,16 +193,59 @@ export function extractApiDoc(source: string, moduleName: string): unknown {
152
193
  }
153
194
 
154
195
  const moduleValueInterfaceNames = new Set(moduleValueInterfaces.map((iface) => iface.name.text));
155
- for (const iface of referencedInterfaces(
156
- moduleBlock,
157
- referencedTypeNodes,
158
- moduleValueInterfaceNames,
159
- )) {
160
- if (emittedNames.has(iface.name.text)) continue;
196
+ const declared = moduleBlock ? moduleTypeDeclarations(moduleBlock) : undefined;
197
+ const reachable = referencedTypeNames(declared, referencedTypeNodes);
198
+
199
+ for (const { node, element } of aliasElements) {
200
+ const name = node.name.text;
201
+ // Reachability is resolved against the module block, so identity keeps a
202
+ // same-named alias nested in a namespace from borrowing the outer one's.
203
+ if (declared?.aliases.get(name) !== node) continue;
204
+ if (!reachable.has(name) || !ts.isTypeLiteralNode(node.type)) continue;
205
+ const { functions, properties } = shapeMembers(node.type.members, sf);
206
+ if (functions.length > 0) element.functions = functions;
207
+ if (properties.length > 0) element.properties = properties;
208
+ }
209
+
210
+ for (const name of reachable) {
211
+ if (moduleValueInterfaceNames.has(name) || emittedNames.has(name)) continue;
212
+ const iface = declared?.interfaces.get(name);
213
+ if (!iface) continue;
161
214
  const typedef = typedefElement(iface, sf);
162
215
  if (!typedef) continue;
163
216
  elements.push(typedef);
164
- emittedNames.add(iface.name.text);
217
+ emittedNames.add(name);
218
+ }
219
+
220
+ // The ambient lane runs last so every module-block element keeps the index it
221
+ // has today. Its shapes resolve against file-scope declarations only, by node
222
+ // identity, so a file-scope shape never borrows a same-named module-block one.
223
+ collect(sf.statements, "", true);
224
+ const globalDeclared = typeDeclarationsIn(sf.statements);
225
+ for (const name of referencedTypeNames(globalDeclared, globalTypeNodes)) {
226
+ if (emittedNames.has(name)) continue;
227
+ const alias = globalDeclared.aliases.get(name);
228
+ if (alias) {
229
+ if (!ts.isTypeLiteralNode(alias.type)) continue;
230
+ const { functions, properties } = shapeMembers(alias.type.members, sf);
231
+ if (functions.length === 0 && properties.length === 0) continue;
232
+ elements.push({
233
+ type: "TYPEDEF",
234
+ name,
235
+ ...(functions.length > 0 ? { functions } : {}),
236
+ ...(properties.length > 0 ? { properties } : {}),
237
+ ...deprecatedKey(alias),
238
+ global: true,
239
+ });
240
+ emittedNames.add(name);
241
+ continue;
242
+ }
243
+ const iface = globalDeclared.interfaces.get(name);
244
+ if (!iface) continue;
245
+ const typedef = typedefElement(iface, sf);
246
+ if (!typedef) continue;
247
+ elements.push({ ...typedef, global: true });
248
+ emittedNames.add(name);
165
249
  }
166
250
 
167
251
  return {
@@ -170,6 +254,21 @@ export function extractApiDoc(source: string, moduleName: string): unknown {
170
254
  };
171
255
  }
172
256
 
257
+ /**
258
+ * A declaration's type parameter list as the pre-rendered string the sibling
259
+ * `lower-api-doc` lane emits (`<T>`, `<T = unknown>`, `<A, B extends A>`), or
260
+ * `""` when there is none — absence of the key, not an empty string, is how the
261
+ * model encodes "takes no type arguments".
262
+ */
263
+ function genericClause(
264
+ decl: ts.FunctionDeclaration | ts.MethodSignature,
265
+ sf: ts.SourceFile,
266
+ ): string {
267
+ const params = decl.typeParameters;
268
+ if (!params || params.length === 0) return "";
269
+ return `<${params.map((p) => oneLineText(p, sf)).join(", ")}>`;
270
+ }
271
+
173
272
  function functionElement(
174
273
  decl: ts.FunctionDeclaration | ts.MethodSignature,
175
274
  name: string,
@@ -204,14 +303,17 @@ function functionElement(
204
303
  ];
205
304
 
206
305
  const example = exampleText(decl);
306
+ const generics = genericClause(decl, sf);
207
307
  return {
208
308
  type: "FUNCTION",
209
309
  name,
210
310
  brief: briefOf(summary),
211
311
  description: summary,
312
+ ...(generics === "" ? {} : { generics }),
212
313
  parameters,
213
314
  returnvalues,
214
315
  ...(example === "" ? {} : { examples: example }),
316
+ ...deprecatedKey(decl),
215
317
  };
216
318
  }
217
319
 
@@ -225,13 +327,22 @@ function collectFunctionReferenceTypes(
225
327
  if (decl.type) out.push(decl.type);
226
328
  }
227
329
 
228
- function typedefElement(
229
- iface: ts.InterfaceDeclaration,
230
- sf: ts.SourceFile,
231
- ): Record<string, unknown> | undefined {
330
+ interface ShapeMembers {
331
+ functions: Record<string, unknown>[];
332
+ properties: Record<string, unknown>[];
333
+ }
334
+
335
+ /**
336
+ * The one member reader shared by both shape carriers — an `interface`
337
+ * declaration and a `type X = { … }` alias — so the two lanes cannot drift.
338
+ * `is_optional` is written only on an optional member, matching how
339
+ * `deprecatedKey` encodes absence and keeping a regen diff proportional to what
340
+ * actually changed.
341
+ */
342
+ function shapeMembers(members: readonly ts.TypeElement[], sf: ts.SourceFile): ShapeMembers {
232
343
  const functions: Record<string, unknown>[] = [];
233
344
  const properties: Record<string, unknown>[] = [];
234
- for (const member of iface.members) {
345
+ for (const member of members) {
235
346
  if (!member.name) continue;
236
347
  const name = memberName(member.name, sf);
237
348
  if (ts.isMethodSignature(member)) {
@@ -244,50 +355,80 @@ function typedefElement(
244
355
  brief: briefOf(summary),
245
356
  description: summary,
246
357
  types: member.type ? [typeText(member.type, sf)] : [],
358
+ ...(member.questionToken ? { is_optional: "True" } : {}),
247
359
  ...(fields ? { fields } : {}),
360
+ ...deprecatedKey(member),
248
361
  });
249
362
  }
250
363
  }
364
+ return { functions, properties };
365
+ }
366
+
367
+ function typedefElement(
368
+ iface: ts.InterfaceDeclaration,
369
+ sf: ts.SourceFile,
370
+ ): Record<string, unknown> | undefined {
371
+ const { functions, properties } = shapeMembers(iface.members, sf);
251
372
  if (functions.length === 0 && properties.length === 0) return undefined;
252
373
  return {
253
374
  type: "TYPEDEF",
254
375
  name: iface.name.text,
255
376
  ...(functions.length > 0 ? { functions } : {}),
256
377
  ...(properties.length > 0 ? { properties } : {}),
378
+ ...deprecatedKey(iface),
257
379
  };
258
380
  }
259
381
 
260
- function referencedInterfaces(
261
- moduleBlock: ts.ModuleBlock | undefined,
262
- typeNodes: ts.TypeNode[],
263
- excludedNames: ReadonlySet<string>,
264
- ): ts.InterfaceDeclaration[] {
265
- if (!moduleBlock) return [];
382
+ interface TypeDeclarations {
383
+ aliases: Map<string, ts.TypeAliasDeclaration>;
384
+ interfaces: Map<string, ts.InterfaceDeclaration>;
385
+ }
386
+
387
+ function typeDeclarationsIn(statements: readonly ts.Statement[]): TypeDeclarations {
266
388
  const aliases = new Map<string, ts.TypeAliasDeclaration>();
267
389
  const interfaces = new Map<string, ts.InterfaceDeclaration>();
268
- for (const stmt of moduleBlock.statements) {
390
+ for (const stmt of statements) {
269
391
  if (ts.isTypeAliasDeclaration(stmt)) aliases.set(stmt.name.text, stmt);
270
392
  if (ts.isInterfaceDeclaration(stmt)) interfaces.set(stmt.name.text, stmt);
271
393
  }
394
+ return { aliases, interfaces };
395
+ }
396
+
397
+ function moduleTypeDeclarations(moduleBlock: ts.ModuleBlock): TypeDeclarations {
398
+ return typeDeclarationsIn(moduleBlock.statements);
399
+ }
400
+
401
+ /**
402
+ * Every alias and interface name declared in `declared` that is reachable from
403
+ * an emitted member's type, in first-reached order. Both shape lanes gate on
404
+ * this single traversal — a reachable interface becomes its own TYPEDEF element,
405
+ * a reachable alias over an object literal has that element's members filled in
406
+ * — so a shape no published member names stays off `/api` either way. The
407
+ * module-block and file-scope lanes each pass their own declarations, so a name
408
+ * declared in both resolves to the lane that named it.
409
+ */
410
+ function referencedTypeNames(
411
+ declared: TypeDeclarations | undefined,
412
+ typeNodes: ts.TypeNode[],
413
+ ): Set<string> {
414
+ const names = new Set<string>();
415
+ if (!declared) return names;
416
+ const { aliases, interfaces } = declared;
272
417
 
273
- const found = new Map<string, ts.InterfaceDeclaration>();
274
- const seenAliases = new Set<string>();
275
418
  const visit = (node: ts.Node): void => {
276
419
  if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
277
420
  const name = node.typeName.text;
278
- const iface = interfaces.get(name);
279
- if (iface && !excludedNames.has(name)) found.set(name, iface);
280
421
  const alias = aliases.get(name);
281
- if (alias && !seenAliases.has(name)) {
282
- seenAliases.add(name);
283
- visit(alias.type);
422
+ if ((alias || interfaces.has(name)) && !names.has(name)) {
423
+ names.add(name);
424
+ if (alias) visit(alias.type);
284
425
  }
285
426
  }
286
427
  ts.forEachChild(node, visit);
287
428
  };
288
429
 
289
430
  for (const node of typeNodes) visit(node);
290
- return [...found.values()];
431
+ return names;
291
432
  }
292
433
 
293
434
  function exportedValueInterfaces(
@@ -350,9 +491,17 @@ function memberName(name: ts.PropertyName, sf: ts.SourceFile): string {
350
491
  return name.getText(sf);
351
492
  }
352
493
 
494
+ // string-named only: a file-scope `declare namespace` is a `ModuleDeclaration`
495
+ // too, so an identifier-named block appearing before the real `declare
496
+ // module '<name>'` would otherwise be mistaken for the module surface.
353
497
  function findModuleBlock(sf: ts.SourceFile): ts.ModuleBlock | undefined {
354
498
  for (const stmt of sf.statements) {
355
- if (ts.isModuleDeclaration(stmt) && stmt.body && ts.isModuleBlock(stmt.body)) {
499
+ if (
500
+ ts.isModuleDeclaration(stmt) &&
501
+ ts.isStringLiteral(stmt.name) &&
502
+ stmt.body &&
503
+ ts.isModuleBlock(stmt.body)
504
+ ) {
356
505
  return stmt.body;
357
506
  }
358
507
  }
@@ -363,6 +512,10 @@ function isExported(node: ts.HasModifiers): boolean {
363
512
  return (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0;
364
513
  }
365
514
 
515
+ function isAmbient(node: ts.HasModifiers): boolean {
516
+ return (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Ambient) !== 0;
517
+ }
518
+
366
519
  /** The closest non-empty JSDoc summary text attached to a node, tags stripped. */
367
520
  function jsDocSummary(node: ts.Node): string {
368
521
  const comments = ts
@@ -412,6 +565,25 @@ function exampleText(decl: ts.FunctionDeclaration | ts.MethodSignature): string
412
565
  return "";
413
566
  }
414
567
 
568
+ // `undefined` when the node carries no `@deprecated`, the trimmed tag text when
569
+ // it does — `""` for a bare tag. The caller spreads the key only on a defined
570
+ // result, so absence of the key is the sole encoding of "not deprecated" and a
571
+ // bare tag stays distinguishable from an untagged symbol.
572
+ function deprecatedText(node: ts.Node): string | undefined {
573
+ for (const tag of ts.getJSDocTags(node)) {
574
+ if (tag.tagName.text === "deprecated") {
575
+ return (ts.getTextOfJSDocComment(tag.comment) ?? "").trim();
576
+ }
577
+ }
578
+ return undefined;
579
+ }
580
+
581
+ /** The `{ deprecated }` key to spread onto an element, empty when untagged. */
582
+ function deprecatedKey(node: ts.Node): Record<string, string> {
583
+ const text = deprecatedText(node);
584
+ return text === undefined ? {} : { deprecated: text };
585
+ }
586
+
415
587
  /** Trim a JSDoc `@param`/`@returns` comment and drop a leading `-` delimiter. */
416
588
  function cleanDoc(comment: string | undefined): string {
417
589
  return (comment ?? "").trim().replace(/^-\s*/, "");