@defold-typescript/library-types 0.23.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.
Files changed (120) hide show
  1. package/api-doc/boom.json +3870 -0
  2. package/api-doc/bridge.json +10 -5
  3. package/api-doc/bzAnim.json +338 -0
  4. package/api-doc/decore.json +1 -1
  5. package/api-doc/defcon.json +150 -0
  6. package/api-doc/{defmath.defmath.json → defmath.json} +6 -46
  7. package/api-doc/defsave.json +518 -0
  8. package/api-doc/deftest.json +741 -0
  9. package/api-doc/{dicebag.dicebag.json → dicebag.json} +19 -1
  10. package/api-doc/druid.json +90 -92
  11. package/api-doc/event.json +16 -16
  12. package/api-doc/gooey.json +2050 -0
  13. package/api-doc/in.accelerometer.json +75 -1
  14. package/api-doc/in.button.json +17 -10
  15. package/api-doc/in.cursor.json +26 -0
  16. package/api-doc/in.gesture.json +401 -6
  17. package/api-doc/in.keyboard.json +2 -0
  18. package/api-doc/in.mapper.json +12 -8
  19. package/api-doc/in.onscreen.json +61 -5
  20. package/api-doc/in.state.json +61 -13
  21. package/api-doc/in.textbox.json +23 -12
  22. package/api-doc/in.triggers.json +336 -0
  23. package/api-doc/lang.json +5 -5
  24. package/api-doc/log.json +1 -1
  25. package/api-doc/metrics.fps.json +61 -2
  26. package/api-doc/metrics.mem.json +61 -2
  27. package/api-doc/monarch.monarch.json +249 -7
  28. package/api-doc/monarch.transitions.easings.json +19 -26
  29. package/api-doc/monarch.transitions.gui.json +782 -79
  30. package/api-doc/nakama.engine.defold.json +31 -18
  31. package/api-doc/{nakama.nakama.json → nakama.json} +4752 -2554
  32. package/api-doc/nakama.util.log.json +32 -4
  33. package/api-doc/narrator.json +1 -1
  34. package/api-doc/openapi/nakama.nakama.json +7680 -0
  35. package/api-doc/orthographic.camera.json +96 -98
  36. package/api-doc/orthographic.json +602 -0
  37. package/api-doc/panthera.json +1183 -0
  38. package/api-doc/{persist.persist.json → persist.json} +26 -1
  39. package/api-doc/{platypus.platypus.json → platypus.json} +68 -14
  40. package/api-doc/{rendy.rendy.json → rendy.json} +46 -99
  41. package/api-doc/richtext.color.json +77 -0
  42. package/api-doc/richtext.richtext.json +375 -5
  43. package/api-doc/saver.saver.json +2 -1
  44. package/api-doc/saver.storage.json +2 -1
  45. package/api-doc/{starly.starly.json → starly.json} +1 -1
  46. package/api-doc/{yagames.yagames.json → yagames.json} +784 -189
  47. package/api-doc/{zzfx.api.json → zzfx.json} +9 -9
  48. package/authored-targets.json +403 -0
  49. package/generated/{boom.boom.d.ts → boom.d.ts} +14 -0
  50. package/generated/bridge.d.ts +5 -5
  51. package/generated/{bzAnim.bzLibrary.d.ts → bzAnim.d.ts} +6 -0
  52. package/generated/decore.d.ts +1 -1
  53. package/generated/defcon.d.ts +43 -0
  54. package/generated/{defmath.defmath.d.ts → defmath.d.ts} +3 -2
  55. package/generated/defsave.d.ts +144 -0
  56. package/generated/{dicebag.dicebag.d.ts → dicebag.d.ts} +25 -0
  57. package/generated/druid.d.ts +81 -79
  58. package/generated/event.d.ts +15 -15
  59. package/generated/{gooey.gooey.d.ts → gooey.d.ts} +61 -6
  60. package/generated/in.onscreen.d.ts +5 -0
  61. package/generated/in.textbox.d.ts +4 -0
  62. package/generated/lang.d.ts +5 -5
  63. package/generated/log.d.ts +1 -1
  64. package/generated/metrics.fps.d.ts +19 -1
  65. package/generated/metrics.mem.d.ts +19 -1
  66. package/generated/monarch.monarch.d.ts +44 -1
  67. package/generated/monarch.transitions.easings.d.ts +0 -1
  68. package/generated/monarch.transitions.gui.d.ts +96 -12
  69. package/generated/nakama.d.ts +1158 -0
  70. package/generated/nakama.engine.defold.d.ts +3 -1
  71. package/generated/nakama.util.log.d.ts +6 -0
  72. package/generated/narrator.d.ts +1 -1
  73. package/generated/openapi/nakama.nakama.d.ts +884 -0
  74. package/generated/orthographic.camera.d.ts +24 -38
  75. package/generated/orthographic.d.ts +155 -0
  76. package/generated/panthera.d.ts +170 -0
  77. package/generated/{persist.persist.d.ts → persist.d.ts} +3 -0
  78. package/generated/{platypus.platypus.d.ts → platypus.d.ts} +9 -0
  79. package/generated/{rendy.rendy.d.ts → rendy.d.ts} +33 -80
  80. package/generated/richtext.color.d.ts +15 -0
  81. package/generated/saver.saver.d.ts +2 -0
  82. package/generated/saver.storage.d.ts +2 -0
  83. package/generated/{yagames.yagames.d.ts → yagames.d.ts} +151 -29
  84. package/generated/{zzfx.api.d.ts → zzfx.d.ts} +7 -0
  85. package/library-classification.json +0 -156
  86. package/library-targets.json +1 -218
  87. package/luals-targets.json +56 -3
  88. package/openapi-targets.json +17 -0
  89. package/package.json +7 -57
  90. package/scripts/__snapshots__/parse-luals.test.ts.snap +164 -10
  91. package/scripts/apply-luals-overrides.ts +39 -7
  92. package/scripts/authored-parity.ts +718 -0
  93. package/scripts/emit-library-dts.ts +94 -17
  94. package/scripts/extract-api-doc.ts +189 -38
  95. package/scripts/fidelity-floor.ts +178 -0
  96. package/scripts/import-upstream-docs.ts +125 -0
  97. package/scripts/lower-api-doc.ts +21 -3
  98. package/scripts/luals-fidelity.ts +16 -11
  99. package/scripts/map-luals-types.ts +52 -21
  100. package/scripts/parse-lua-surface.ts +187 -0
  101. package/scripts/parse-luals.ts +50 -4
  102. package/scripts/parse-markdown-api.ts +247 -0
  103. package/scripts/parse-openapi-api.ts +313 -0
  104. package/scripts/sync-authored-types.ts +249 -0
  105. package/scripts/sync-library-types.ts +99 -3
  106. package/scripts/sync-luals-types.ts +14 -1
  107. package/scripts/sync-markdown-types.ts +761 -0
  108. package/scripts/sync-openapi-types.ts +308 -0
  109. package/scripts/sync-script-api-types.ts +16 -17
  110. package/api-doc/boom.boom.json +0 -26
  111. package/api-doc/bzAnim.bzLibrary.json +0 -156
  112. package/api-doc/defcon.console.json +0 -109
  113. package/api-doc/defsave.defsave.json +0 -175
  114. package/api-doc/deftest.deftest.json +0 -71
  115. package/api-doc/gooey.gooey.json +0 -963
  116. package/generated/defcon.console.d.ts +0 -24
  117. package/generated/defsave.defsave.d.ts +0 -31
  118. package/generated/nakama.nakama.d.ts +0 -594
  119. /package/generated/{deftest.deftest.d.ts → deftest.d.ts} +0 -0
  120. /package/generated/{starly.starly.d.ts → starly.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;
@@ -86,46 +86,82 @@ export function extractApiDoc(source: string, moduleName: string): unknown {
86
86
  const elements: Record<string, unknown>[] = [];
87
87
  const emittedNames = new Set<string>();
88
88
  const referencedTypeNodes: ts.TypeNode[] = [];
89
+ // Type nodes reached from file-scope declarations, kept apart from the module
90
+ // block's so an ambient global can never pull a module-block shape onto `/api`
91
+ // that the module surface alone did not reach.
92
+ const globalTypeNodes: ts.TypeNode[] = [];
93
+ // Each alias's declaration paired with the bare element it already pushed, so
94
+ // a reachable object-literal alias is filled in by merging into that element
95
+ // rather than by appending a second one — element order stays as emitted.
96
+ const aliasElements: { node: ts.TypeAliasDeclaration; element: Record<string, unknown> }[] = [];
89
97
 
90
98
  // Members nested in an `export namespace` (`bridge.bridge`) keep their
91
99
  // namespace path so same-named members across namespaces (e.g. `is_supported`)
92
100
  // stay distinct instead of colliding in `emittedNames`.
93
- const collect = (nodes: readonly ts.Statement[], prefix: string): void => {
101
+ //
102
+ // `global` switches the walk to the ambient lane: the file's own statements
103
+ // rather than the module block's. There the gate is the `declare` modifier
104
+ // (there is no `export =` to suppress bare declarations, so `emitBare` does not
105
+ // apply), each element carries the `global` marker, and a type alias is not
106
+ // pushed eagerly — file scope carries far more incidental type machinery than a
107
+ // module block, so a shape publishes only once reachability proves a published
108
+ // signature names it.
109
+ const collect = (nodes: readonly ts.Statement[], prefix: string, global: boolean): void => {
94
110
  const qualify = (name: string): string => (prefix ? `${prefix}.${name}` : name);
111
+ const mark = global ? { global: true } : {};
112
+ const typeNodes = global ? globalTypeNodes : referencedTypeNodes;
113
+ // Inside an ambient namespace every member is ambient by containment, so the
114
+ // nested lane emits regardless of an `export` keyword.
115
+ const gate = (stmt: ts.HasModifiers): boolean =>
116
+ global ? prefix !== "" || isAmbient(stmt) || isExported(stmt) : isExported(stmt) || emitBare;
95
117
  for (const stmt of nodes) {
96
- if (ts.isFunctionDeclaration(stmt) && stmt.name && (isExported(stmt) || emitBare)) {
97
- collectFunctionReferenceTypes(stmt, referencedTypeNodes);
118
+ if (ts.isFunctionDeclaration(stmt) && stmt.name && gate(stmt)) {
119
+ collectFunctionReferenceTypes(stmt, typeNodes);
98
120
  const name = qualify(stmt.name.text);
99
- elements.push(functionElement(stmt, name, sf));
121
+ elements.push({ ...functionElement(stmt, name, sf), ...mark });
100
122
  emittedNames.add(name);
101
- } else if (ts.isVariableStatement(stmt) && (isExported(stmt) || emitBare)) {
123
+ } else if (ts.isVariableStatement(stmt) && gate(stmt)) {
124
+ // The `VariableStatement` carries the JSDoc, not the declaration, so both the
125
+ // summary and the deprecation tag are read once from the statement and shared
126
+ // by every declarator under it.
127
+ const summary = jsDocSummary(stmt);
102
128
  for (const decl of stmt.declarationList.declarations) {
103
- if (decl.type) referencedTypeNodes.push(decl.type);
129
+ if (decl.type) typeNodes.push(decl.type);
104
130
  const fields = objectFields(decl.type, sf);
105
131
  const name = qualify(decl.name.getText(sf));
106
132
  elements.push({
107
133
  type: "VARIABLE",
108
134
  name,
135
+ // Unconditional, matching `functionElement`: an absent key and an empty one
136
+ // would be two shapes the docs-site had to tell apart for no gain.
137
+ brief: briefOf(summary),
138
+ description: summary,
109
139
  types: decl.type ? [typeText(decl.type, sf)] : [],
110
140
  ...(fields ? { fields } : {}),
141
+ ...deprecatedKey(stmt),
142
+ ...mark,
111
143
  });
112
144
  emittedNames.add(name);
113
145
  }
114
146
  } else if (ts.isTypeAliasDeclaration(stmt)) {
147
+ if (global) continue;
115
148
  const name = qualify(stmt.name.text);
116
- elements.push({ type: "TYPEDEF", name });
149
+ const element: Record<string, unknown> = { type: "TYPEDEF", name };
150
+ elements.push(element);
117
151
  emittedNames.add(name);
152
+ aliasElements.push({ node: stmt, element });
118
153
  } else if (
119
154
  ts.isModuleDeclaration(stmt) &&
155
+ !ts.isStringLiteral(stmt.name) &&
120
156
  stmt.body &&
121
157
  ts.isModuleBlock(stmt.body) &&
122
- (isExported(stmt) || emitBare)
158
+ gate(stmt)
123
159
  ) {
124
- collect(stmt.body.statements, qualify(stmt.name.getText(sf)));
160
+ collect(stmt.body.statements, qualify(stmt.name.getText(sf)), global);
125
161
  }
126
162
  }
127
163
  };
128
- collect(statements, "");
164
+ collect(statements, "", false);
129
165
 
130
166
  const moduleValueInterfaces = exportedValueInterfaces(moduleBlock);
131
167
  for (const iface of moduleValueInterfaces) {
@@ -145,6 +181,7 @@ export function extractApiDoc(source: string, moduleName: string): unknown {
145
181
  name,
146
182
  types: member.type ? [typeText(member.type, sf)] : [],
147
183
  ...(fields ? { fields } : {}),
184
+ ...deprecatedKey(member),
148
185
  });
149
186
  emittedNames.add(name);
150
187
  }
@@ -152,16 +189,59 @@ export function extractApiDoc(source: string, moduleName: string): unknown {
152
189
  }
153
190
 
154
191
  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;
192
+ const declared = moduleBlock ? moduleTypeDeclarations(moduleBlock) : undefined;
193
+ const reachable = referencedTypeNames(declared, referencedTypeNodes);
194
+
195
+ for (const { node, element } of aliasElements) {
196
+ const name = node.name.text;
197
+ // Reachability is resolved against the module block, so identity keeps a
198
+ // same-named alias nested in a namespace from borrowing the outer one's.
199
+ if (declared?.aliases.get(name) !== node) continue;
200
+ if (!reachable.has(name) || !ts.isTypeLiteralNode(node.type)) continue;
201
+ const { functions, properties } = shapeMembers(node.type.members, sf);
202
+ if (functions.length > 0) element.functions = functions;
203
+ if (properties.length > 0) element.properties = properties;
204
+ }
205
+
206
+ for (const name of reachable) {
207
+ if (moduleValueInterfaceNames.has(name) || emittedNames.has(name)) continue;
208
+ const iface = declared?.interfaces.get(name);
209
+ if (!iface) continue;
161
210
  const typedef = typedefElement(iface, sf);
162
211
  if (!typedef) continue;
163
212
  elements.push(typedef);
164
- emittedNames.add(iface.name.text);
213
+ emittedNames.add(name);
214
+ }
215
+
216
+ // The ambient lane runs last so every module-block element keeps the index it
217
+ // has today. Its shapes resolve against file-scope declarations only, by node
218
+ // identity, so a file-scope shape never borrows a same-named module-block one.
219
+ collect(sf.statements, "", true);
220
+ const globalDeclared = typeDeclarationsIn(sf.statements);
221
+ for (const name of referencedTypeNames(globalDeclared, globalTypeNodes)) {
222
+ if (emittedNames.has(name)) continue;
223
+ const alias = globalDeclared.aliases.get(name);
224
+ if (alias) {
225
+ if (!ts.isTypeLiteralNode(alias.type)) continue;
226
+ const { functions, properties } = shapeMembers(alias.type.members, sf);
227
+ if (functions.length === 0 && properties.length === 0) continue;
228
+ elements.push({
229
+ type: "TYPEDEF",
230
+ name,
231
+ ...(functions.length > 0 ? { functions } : {}),
232
+ ...(properties.length > 0 ? { properties } : {}),
233
+ ...deprecatedKey(alias),
234
+ global: true,
235
+ });
236
+ emittedNames.add(name);
237
+ continue;
238
+ }
239
+ const iface = globalDeclared.interfaces.get(name);
240
+ if (!iface) continue;
241
+ const typedef = typedefElement(iface, sf);
242
+ if (!typedef) continue;
243
+ elements.push({ ...typedef, global: true });
244
+ emittedNames.add(name);
165
245
  }
166
246
 
167
247
  return {
@@ -212,6 +292,7 @@ function functionElement(
212
292
  parameters,
213
293
  returnvalues,
214
294
  ...(example === "" ? {} : { examples: example }),
295
+ ...deprecatedKey(decl),
215
296
  };
216
297
  }
217
298
 
@@ -225,13 +306,22 @@ function collectFunctionReferenceTypes(
225
306
  if (decl.type) out.push(decl.type);
226
307
  }
227
308
 
228
- function typedefElement(
229
- iface: ts.InterfaceDeclaration,
230
- sf: ts.SourceFile,
231
- ): Record<string, unknown> | undefined {
309
+ interface ShapeMembers {
310
+ functions: Record<string, unknown>[];
311
+ properties: Record<string, unknown>[];
312
+ }
313
+
314
+ /**
315
+ * The one member reader shared by both shape carriers — an `interface`
316
+ * declaration and a `type X = { … }` alias — so the two lanes cannot drift.
317
+ * `is_optional` is written only on an optional member, matching how
318
+ * `deprecatedKey` encodes absence and keeping a regen diff proportional to what
319
+ * actually changed.
320
+ */
321
+ function shapeMembers(members: readonly ts.TypeElement[], sf: ts.SourceFile): ShapeMembers {
232
322
  const functions: Record<string, unknown>[] = [];
233
323
  const properties: Record<string, unknown>[] = [];
234
- for (const member of iface.members) {
324
+ for (const member of members) {
235
325
  if (!member.name) continue;
236
326
  const name = memberName(member.name, sf);
237
327
  if (ts.isMethodSignature(member)) {
@@ -244,50 +334,80 @@ function typedefElement(
244
334
  brief: briefOf(summary),
245
335
  description: summary,
246
336
  types: member.type ? [typeText(member.type, sf)] : [],
337
+ ...(member.questionToken ? { is_optional: "True" } : {}),
247
338
  ...(fields ? { fields } : {}),
339
+ ...deprecatedKey(member),
248
340
  });
249
341
  }
250
342
  }
343
+ return { functions, properties };
344
+ }
345
+
346
+ function typedefElement(
347
+ iface: ts.InterfaceDeclaration,
348
+ sf: ts.SourceFile,
349
+ ): Record<string, unknown> | undefined {
350
+ const { functions, properties } = shapeMembers(iface.members, sf);
251
351
  if (functions.length === 0 && properties.length === 0) return undefined;
252
352
  return {
253
353
  type: "TYPEDEF",
254
354
  name: iface.name.text,
255
355
  ...(functions.length > 0 ? { functions } : {}),
256
356
  ...(properties.length > 0 ? { properties } : {}),
357
+ ...deprecatedKey(iface),
257
358
  };
258
359
  }
259
360
 
260
- function referencedInterfaces(
261
- moduleBlock: ts.ModuleBlock | undefined,
262
- typeNodes: ts.TypeNode[],
263
- excludedNames: ReadonlySet<string>,
264
- ): ts.InterfaceDeclaration[] {
265
- if (!moduleBlock) return [];
361
+ interface TypeDeclarations {
362
+ aliases: Map<string, ts.TypeAliasDeclaration>;
363
+ interfaces: Map<string, ts.InterfaceDeclaration>;
364
+ }
365
+
366
+ function typeDeclarationsIn(statements: readonly ts.Statement[]): TypeDeclarations {
266
367
  const aliases = new Map<string, ts.TypeAliasDeclaration>();
267
368
  const interfaces = new Map<string, ts.InterfaceDeclaration>();
268
- for (const stmt of moduleBlock.statements) {
369
+ for (const stmt of statements) {
269
370
  if (ts.isTypeAliasDeclaration(stmt)) aliases.set(stmt.name.text, stmt);
270
371
  if (ts.isInterfaceDeclaration(stmt)) interfaces.set(stmt.name.text, stmt);
271
372
  }
373
+ return { aliases, interfaces };
374
+ }
375
+
376
+ function moduleTypeDeclarations(moduleBlock: ts.ModuleBlock): TypeDeclarations {
377
+ return typeDeclarationsIn(moduleBlock.statements);
378
+ }
379
+
380
+ /**
381
+ * Every alias and interface name declared in `declared` that is reachable from
382
+ * an emitted member's type, in first-reached order. Both shape lanes gate on
383
+ * this single traversal — a reachable interface becomes its own TYPEDEF element,
384
+ * a reachable alias over an object literal has that element's members filled in
385
+ * — so a shape no published member names stays off `/api` either way. The
386
+ * module-block and file-scope lanes each pass their own declarations, so a name
387
+ * declared in both resolves to the lane that named it.
388
+ */
389
+ function referencedTypeNames(
390
+ declared: TypeDeclarations | undefined,
391
+ typeNodes: ts.TypeNode[],
392
+ ): Set<string> {
393
+ const names = new Set<string>();
394
+ if (!declared) return names;
395
+ const { aliases, interfaces } = declared;
272
396
 
273
- const found = new Map<string, ts.InterfaceDeclaration>();
274
- const seenAliases = new Set<string>();
275
397
  const visit = (node: ts.Node): void => {
276
398
  if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
277
399
  const name = node.typeName.text;
278
- const iface = interfaces.get(name);
279
- if (iface && !excludedNames.has(name)) found.set(name, iface);
280
400
  const alias = aliases.get(name);
281
- if (alias && !seenAliases.has(name)) {
282
- seenAliases.add(name);
283
- visit(alias.type);
401
+ if ((alias || interfaces.has(name)) && !names.has(name)) {
402
+ names.add(name);
403
+ if (alias) visit(alias.type);
284
404
  }
285
405
  }
286
406
  ts.forEachChild(node, visit);
287
407
  };
288
408
 
289
409
  for (const node of typeNodes) visit(node);
290
- return [...found.values()];
410
+ return names;
291
411
  }
292
412
 
293
413
  function exportedValueInterfaces(
@@ -350,9 +470,17 @@ function memberName(name: ts.PropertyName, sf: ts.SourceFile): string {
350
470
  return name.getText(sf);
351
471
  }
352
472
 
473
+ // string-named only: a file-scope `declare namespace` is a `ModuleDeclaration`
474
+ // too, so an identifier-named block appearing before the real `declare
475
+ // module '<name>'` would otherwise be mistaken for the module surface.
353
476
  function findModuleBlock(sf: ts.SourceFile): ts.ModuleBlock | undefined {
354
477
  for (const stmt of sf.statements) {
355
- if (ts.isModuleDeclaration(stmt) && stmt.body && ts.isModuleBlock(stmt.body)) {
478
+ if (
479
+ ts.isModuleDeclaration(stmt) &&
480
+ ts.isStringLiteral(stmt.name) &&
481
+ stmt.body &&
482
+ ts.isModuleBlock(stmt.body)
483
+ ) {
356
484
  return stmt.body;
357
485
  }
358
486
  }
@@ -363,6 +491,10 @@ function isExported(node: ts.HasModifiers): boolean {
363
491
  return (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0;
364
492
  }
365
493
 
494
+ function isAmbient(node: ts.HasModifiers): boolean {
495
+ return (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Ambient) !== 0;
496
+ }
497
+
366
498
  /** The closest non-empty JSDoc summary text attached to a node, tags stripped. */
367
499
  function jsDocSummary(node: ts.Node): string {
368
500
  const comments = ts
@@ -412,6 +544,25 @@ function exampleText(decl: ts.FunctionDeclaration | ts.MethodSignature): string
412
544
  return "";
413
545
  }
414
546
 
547
+ // `undefined` when the node carries no `@deprecated`, the trimmed tag text when
548
+ // it does — `""` for a bare tag. The caller spreads the key only on a defined
549
+ // result, so absence of the key is the sole encoding of "not deprecated" and a
550
+ // bare tag stays distinguishable from an untagged symbol.
551
+ function deprecatedText(node: ts.Node): string | undefined {
552
+ for (const tag of ts.getJSDocTags(node)) {
553
+ if (tag.tagName.text === "deprecated") {
554
+ return (ts.getTextOfJSDocComment(tag.comment) ?? "").trim();
555
+ }
556
+ }
557
+ return undefined;
558
+ }
559
+
560
+ /** The `{ deprecated }` key to spread onto an element, empty when untagged. */
561
+ function deprecatedKey(node: ts.Node): Record<string, string> {
562
+ const text = deprecatedText(node);
563
+ return text === undefined ? {} : { deprecated: text };
564
+ }
565
+
415
566
  /** Trim a JSDoc `@param`/`@returns` comment and drop a leading `-` delimiter. */
416
567
  function cleanDoc(comment: string | undefined): string {
417
568
  return (comment ?? "").trim().replace(/^-\s*/, "");