@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.
- package/NOTICE +2 -3
- package/api-doc/boom.json +3872 -0
- package/api-doc/bridge.json +10 -5
- package/api-doc/bzAnim.json +338 -0
- package/api-doc/checkpoint.json +127 -0
- package/api-doc/decore.json +1 -1
- package/api-doc/defcon.json +150 -0
- package/api-doc/{defmath.defmath.json → defmath.json} +6 -46
- package/api-doc/defsave.json +518 -0
- package/api-doc/deftest.json +745 -0
- package/api-doc/{dicebag.dicebag.json → dicebag.json} +19 -1
- package/api-doc/druid.json +90 -92
- package/api-doc/event.json +16 -16
- package/api-doc/gooey.json +2050 -0
- package/api-doc/in.accelerometer.json +75 -1
- package/api-doc/in.button.json +17 -10
- package/api-doc/in.cursor.json +26 -0
- package/api-doc/in.gesture.json +401 -6
- package/api-doc/in.keyboard.json +2 -0
- package/api-doc/in.mapper.json +12 -8
- package/api-doc/in.onscreen.json +61 -5
- package/api-doc/in.state.json +61 -13
- package/api-doc/in.textbox.json +23 -12
- package/api-doc/in.triggers.json +336 -0
- package/api-doc/lang.json +5 -5
- package/api-doc/log.json +1 -1
- package/api-doc/metrics.fps.json +61 -2
- package/api-doc/metrics.mem.json +61 -2
- package/api-doc/monarch.monarch.json +249 -7
- package/api-doc/monarch.transitions.easings.json +19 -26
- package/api-doc/monarch.transitions.gui.json +782 -79
- package/api-doc/nakama.engine.defold.json +31 -18
- package/api-doc/{nakama.nakama.json → nakama.json} +4752 -2554
- package/api-doc/nakama.session.json +315 -0
- package/api-doc/nakama.socket.json +3356 -0
- package/api-doc/nakama.util.log.json +32 -4
- package/api-doc/narrator.json +1 -1
- package/api-doc/openapi/nakama.nakama.json +7680 -0
- package/api-doc/orthographic.camera.json +96 -98
- package/api-doc/orthographic.json +602 -0
- package/api-doc/panthera.json +1183 -0
- package/api-doc/{persist.persist.json → persist.json} +26 -1
- package/api-doc/{platypus.platypus.json → platypus.json} +68 -14
- package/api-doc/{rendy.rendy.json → rendy.json} +46 -99
- package/api-doc/richtext.color.json +77 -0
- package/api-doc/richtext.richtext.json +375 -5
- package/api-doc/saver.saver.json +2 -1
- package/api-doc/saver.storage.json +2 -1
- package/api-doc/shutter.json +551 -0
- package/api-doc/{yagames.yagames.json → yagames.json} +784 -189
- package/api-doc/{zzfx.api.json → zzfx.json} +9 -9
- package/authored-targets.json +433 -0
- package/generated/{boom.boom.d.ts → boom.d.ts} +14 -0
- package/generated/bridge.d.ts +5 -5
- package/generated/{bzAnim.bzLibrary.d.ts → bzAnim.d.ts} +6 -0
- package/generated/checkpoint.d.ts +62 -0
- package/generated/decore.d.ts +1 -1
- package/generated/defcon.d.ts +43 -0
- package/generated/{defmath.defmath.d.ts → defmath.d.ts} +3 -2
- package/generated/defsave.d.ts +144 -0
- package/generated/{dicebag.dicebag.d.ts → dicebag.d.ts} +25 -0
- package/generated/druid.d.ts +81 -79
- package/generated/event.d.ts +15 -15
- package/generated/{gooey.gooey.d.ts → gooey.d.ts} +61 -6
- package/generated/in.onscreen.d.ts +5 -0
- package/generated/in.textbox.d.ts +4 -0
- package/generated/lang.d.ts +5 -5
- package/generated/log.d.ts +1 -1
- package/generated/metrics.fps.d.ts +19 -1
- package/generated/metrics.mem.d.ts +19 -1
- package/generated/monarch.monarch.d.ts +44 -1
- package/generated/monarch.transitions.easings.d.ts +0 -1
- package/generated/monarch.transitions.gui.d.ts +96 -12
- package/generated/nakama.d.ts +1158 -0
- package/generated/nakama.engine.defold.d.ts +3 -1
- package/generated/nakama.session.d.ts +95 -0
- package/generated/nakama.socket.d.ts +706 -0
- package/generated/nakama.util.log.d.ts +6 -0
- package/generated/narrator.d.ts +1 -1
- package/generated/openapi/nakama.nakama.d.ts +884 -0
- package/generated/orthographic.camera.d.ts +24 -38
- package/generated/orthographic.d.ts +155 -0
- package/generated/panthera.d.ts +170 -0
- package/generated/{persist.persist.d.ts → persist.d.ts} +3 -0
- package/generated/{platypus.platypus.d.ts → platypus.d.ts} +9 -0
- package/generated/proto.d.ts +4 -4
- package/generated/{rendy.rendy.d.ts → rendy.d.ts} +33 -80
- package/generated/richtext.color.d.ts +15 -0
- package/generated/saver.saver.d.ts +2 -0
- package/generated/saver.storage.d.ts +2 -0
- package/generated/shutter.d.ts +183 -0
- package/generated/{yagames.yagames.d.ts → yagames.d.ts} +151 -29
- package/generated/{zzfx.api.d.ts → zzfx.d.ts} +7 -0
- package/library-classification.json +0 -156
- package/library-targets.json +1 -218
- package/luals-targets.json +56 -3
- package/openapi-targets.json +17 -0
- package/package.json +7 -57
- package/scripts/__snapshots__/parse-luals.test.ts.snap +164 -10
- package/scripts/apply-luals-overrides.ts +39 -7
- package/scripts/authored-parity.ts +718 -0
- package/scripts/emit-library-dts.ts +94 -17
- package/scripts/extract-api-doc.ts +214 -42
- package/scripts/fidelity-floor.ts +178 -0
- package/scripts/import-upstream-docs.ts +125 -0
- package/scripts/lower-api-doc.ts +21 -3
- package/scripts/luals-fidelity.ts +16 -11
- package/scripts/map-luals-types.ts +52 -21
- package/scripts/parse-lua-surface.ts +187 -0
- package/scripts/parse-luals.ts +50 -4
- package/scripts/parse-markdown-api.ts +247 -0
- package/scripts/parse-openapi-api.ts +313 -0
- package/scripts/sync-authored-types.ts +249 -0
- package/scripts/sync-library-types.ts +99 -3
- package/scripts/sync-luals-types.ts +14 -1
- package/scripts/sync-markdown-types.ts +761 -0
- package/scripts/sync-openapi-types.ts +308 -0
- package/scripts/sync-script-api-types.ts +16 -17
- package/api-doc/boom.boom.json +0 -26
- package/api-doc/bzAnim.bzLibrary.json +0 -156
- package/api-doc/defcon.console.json +0 -109
- package/api-doc/defsave.defsave.json +0 -175
- package/api-doc/deftest.deftest.json +0 -71
- package/api-doc/gooey.gooey.json +0 -963
- package/api-doc/starly.starly.json +0 -488
- package/generated/defcon.console.d.ts +0 -24
- package/generated/defsave.defsave.d.ts +0 -31
- package/generated/nakama.nakama.d.ts +0 -594
- package/generated/starly.starly.d.ts +0 -148
- /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(
|
|
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
|
|
116
|
-
* mapped through the rename map; `""` when none
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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 && (
|
|
97
|
-
collectFunctionReferenceTypes(stmt,
|
|
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) && (
|
|
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)
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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(
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
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 && !
|
|
282
|
-
|
|
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
|
|
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 (
|
|
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*/, "");
|