@defold-typescript/library-types 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api-doc/boom.json +3870 -0
- package/api-doc/{bridge.bridge.json → bridge.json} +754 -844
- package/api-doc/bzAnim.json +338 -0
- package/api-doc/decore.json +23 -23
- 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 +741 -0
- package/api-doc/{dicebag.dicebag.json → dicebag.json} +19 -1
- package/api-doc/druid.json +249 -1846
- package/api-doc/event.json +1074 -0
- package/api-doc/gooey.json +2050 -0
- package/api-doc/immutable.json +65 -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 +528 -0
- package/api-doc/{event.event.json → log.json} +96 -101
- 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.util.log.json +32 -4
- package/api-doc/narrator.json +647 -0
- 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/proto.json +995 -0
- 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 +714 -263
- package/api-doc/saver.storage.json +283 -52
- package/api-doc/squid.json +846 -0
- package/api-doc/{starly.starly.json → starly.json} +1 -1
- package/api-doc/tweener.json +277 -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 +403 -0
- package/generated/{boom.boom.d.ts → boom.d.ts} +14 -0
- package/generated/bridge.d.ts +468 -0
- package/generated/{bzAnim.bzLibrary.d.ts → bzAnim.d.ts} +6 -0
- package/generated/decore.d.ts +37 -37
- 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 +198 -502
- package/generated/event.d.ts +318 -0
- package/generated/{gooey.gooey.d.ts → gooey.d.ts} +61 -6
- package/generated/immutable.d.ts +13 -0
- package/generated/in.onscreen.d.ts +5 -0
- package/generated/in.textbox.d.ts +4 -0
- package/generated/lang.d.ts +101 -0
- package/generated/log.d.ts +36 -0
- 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.util.log.d.ts +6 -0
- package/generated/narrator.d.ts +121 -0
- 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 +146 -0
- 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 +289 -42
- package/generated/saver.storage.d.ts +79 -14
- package/generated/squid.d.ts +127 -0
- package/generated/tweener.d.ts +42 -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 -227
- package/library-targets.json +1 -284
- package/luals-targets.json +168 -1
- package/openapi-targets.json +17 -0
- package/package.json +9 -88
- package/script-api-targets.json +15 -0
- package/scripts/__snapshots__/parse-luals.test.ts.snap +504 -85
- package/scripts/apply-luals-overrides.ts +95 -0
- package/scripts/authored-parity.ts +718 -0
- package/scripts/emit-library-dts.ts +177 -19
- package/scripts/extract-api-doc.ts +189 -38
- package/scripts/fidelity-floor.ts +178 -0
- package/scripts/import-upstream-docs.ts +125 -0
- package/scripts/lower-api-doc.ts +69 -19
- package/scripts/luals-fidelity.ts +22 -10
- package/scripts/map-luals-types.ts +86 -22
- package/scripts/parse-lua-surface.ts +187 -0
- package/scripts/parse-luals.ts +473 -22
- 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 +29 -2
- package/scripts/sync-markdown-types.ts +761 -0
- package/scripts/sync-openapi-types.ts +308 -0
- package/scripts/sync-script-api-types.ts +367 -0
- 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/immutable.immutable.json +0 -63
- package/api-doc/lang.lang.json +0 -411
- package/api-doc/log.log.json +0 -50
- package/api-doc/narrator.narrator.json +0 -150
- package/api-doc/proto.proto.json +0 -355
- package/api-doc/squid.squid.json +0 -660
- package/api-doc/tweener.tweener.json +0 -419
- package/generated/bridge.bridge.d.ts +0 -533
- package/generated/defcon.console.d.ts +0 -24
- package/generated/defsave.defsave.d.ts +0 -31
- package/generated/event.event.d.ts +0 -54
- package/generated/immutable.immutable.d.ts +0 -13
- package/generated/lang.lang.d.ts +0 -33
- package/generated/log.log.d.ts +0 -40
- package/generated/nakama.nakama.d.ts +0 -594
- package/generated/narrator.narrator.d.ts +0 -66
- package/generated/proto.proto.d.ts +0 -36
- package/generated/squid.squid.d.ts +0 -106
- package/generated/tweener.tweener.d.ts +0 -151
- /package/generated/{deftest.deftest.d.ts → deftest.d.ts} +0 -0
- /package/generated/{starly.starly.d.ts → starly.d.ts} +0 -0
|
@@ -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
|
-
|
|
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 && (
|
|
97
|
-
collectFunctionReferenceTypes(stmt,
|
|
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) && (
|
|
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)
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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(
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
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 && !
|
|
282
|
-
|
|
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
|
|
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 (
|
|
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*/, "");
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A one-sided coverage ratchet over the committed `fidelity/*.json` reports.
|
|
3
|
+
*
|
|
4
|
+
* Each emitting lane already round-trips its committed report against a freshly
|
|
5
|
+
* built one, which proves the report matches what the pipeline builds *today* —
|
|
6
|
+
* but a regeneration that lowers coverage rewrites the report too, so the drop
|
|
7
|
+
* lands silently. `fidelity-floor.json` pins each report's coverage from the
|
|
8
|
+
* outside: `regen` never touches it, so the only way a floor moves is the
|
|
9
|
+
* explicit, monotone `--raise` below — which also makes it the one hand-editable
|
|
10
|
+
* input here, so it is parsed under a validating contract (`parseFloors`).
|
|
11
|
+
*
|
|
12
|
+
* Reports are keyed by package-root-relative POSIX path, not by namespace:
|
|
13
|
+
* `fidelity/openapi/nakama.nakama.json` reports `namespace: "nakama"`, and two
|
|
14
|
+
* lanes emitting the same namespace would collide on a namespace key.
|
|
15
|
+
*
|
|
16
|
+
* The walk is universal over `fidelity/` with one carve-out: `fidelity/authored/`
|
|
17
|
+
* holds *surface* parity, not *type-token* coverage. Those reports have no
|
|
18
|
+
* `totalTypeTokens` to be a fraction of, and their coverage means something else —
|
|
19
|
+
* upstream members declared at the right arity, from `authored-parity.ts`. They get
|
|
20
|
+
* their own ratchet in `authored-parity-floor.json`, so folding them in here would
|
|
21
|
+
* only mean two incompatible denominators under one floor manifest. That manifest
|
|
22
|
+
* also ratchets *two* axes per key (`callableCoverage` and `fieldCoverage`, never
|
|
23
|
+
* averaged), which a flat one-ratio-per-key manifest cannot express — a second,
|
|
24
|
+
* independent reason the two do not merge. Its entries are parsed by
|
|
25
|
+
* `parseAuthoredFloors`, not by `parseFloors` below.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { join, resolve } from "node:path";
|
|
30
|
+
|
|
31
|
+
export const FIDELITY_DIR = "fidelity";
|
|
32
|
+
/** Directory name under `fidelity/` the token-coverage walk skips — see the
|
|
33
|
+
* module note; ratcheted by `authored-parity-floor.json` instead. */
|
|
34
|
+
export const AUTHORED_PARITY_DIRNAME = "authored";
|
|
35
|
+
export const FLOOR_MANIFEST_FILE = "fidelity-floor.json";
|
|
36
|
+
export const FLOOR_RAISE_COMMAND = "bun run --cwd packages/library-types fidelity:floor";
|
|
37
|
+
|
|
38
|
+
export interface FidelityFloorReport {
|
|
39
|
+
coverage: number;
|
|
40
|
+
totalMembers: number;
|
|
41
|
+
totalTypeTokens: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function numberField(raw: Record<string, unknown>, field: string, path: string): number {
|
|
45
|
+
const value = raw[field];
|
|
46
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
47
|
+
throw new Error(`${path}: expected a finite numeric "${field}", got ${JSON.stringify(value)}`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function walkJsonFiles(dir: string, prefix: string, out: string[]): void {
|
|
53
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
54
|
+
const rel = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
if (prefix === "" && entry.name === AUTHORED_PARITY_DIRNAME) continue;
|
|
57
|
+
walkJsonFiles(join(dir, entry.name), rel, out);
|
|
58
|
+
} else if (entry.name.endsWith(".json")) out.push(rel);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Every committed fidelity report under `fidelity/`, at any depth, keyed by its
|
|
64
|
+
* package-root-relative POSIX path and sorted by that key. Reads only — the walk
|
|
65
|
+
* is the enumeration the gate trusts, so a lane that emits into a nested
|
|
66
|
+
* directory (`fidelity/openapi/`) is picked up without registration.
|
|
67
|
+
*/
|
|
68
|
+
export function collectFidelityReports(root: string): Record<string, FidelityFloorReport> {
|
|
69
|
+
const dir = join(root, FIDELITY_DIR);
|
|
70
|
+
if (!existsSync(dir)) return {};
|
|
71
|
+
const relatives: string[] = [];
|
|
72
|
+
walkJsonFiles(dir, "", relatives);
|
|
73
|
+
const reports: Record<string, FidelityFloorReport> = {};
|
|
74
|
+
for (const rel of relatives.sort()) {
|
|
75
|
+
const key = `${FIDELITY_DIR}/${rel}`;
|
|
76
|
+
const raw = JSON.parse(readFileSync(join(dir, rel), "utf8")) as Record<string, unknown>;
|
|
77
|
+
reports[key] = {
|
|
78
|
+
coverage: numberField(raw, "coverage", key),
|
|
79
|
+
totalMembers: numberField(raw, "totalMembers", key),
|
|
80
|
+
totalTypeTokens: numberField(raw, "totalTypeTokens", key),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return reports;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// `JSON.stringify` renders NaN and Infinity as `null` and returns `undefined` for
|
|
87
|
+
// `undefined`, both of which would misreport the value the manifest actually holds.
|
|
88
|
+
function describeFloor(value: unknown): string {
|
|
89
|
+
if (typeof value === "number") return String(value);
|
|
90
|
+
return JSON.stringify(value) ?? String(value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The floor manifest's parse-time contract: a plain object of finite numbers in
|
|
95
|
+
* `[0, 1]`. Every consuming assertion compares coercively, so an unvalidated
|
|
96
|
+
* `null` or `false` floor would pass `floor !== undefined` and fail every
|
|
97
|
+
* `coverage < floor` comparison — switching that report's ratchet off silently.
|
|
98
|
+
* Key order is the writer's contract and is preserved here, not sorted.
|
|
99
|
+
*/
|
|
100
|
+
export function parseFloors(raw: unknown, path: string): Record<string, number> {
|
|
101
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
102
|
+
throw new Error(`${path}: expected a JSON object of floors, got ${describeFloor(raw)}`);
|
|
103
|
+
}
|
|
104
|
+
const floors: Record<string, number> = {};
|
|
105
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
106
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`${path}: floor "${key}" must be a finite number in [0, 1], got ${describeFloor(value)}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
floors[key] = value;
|
|
112
|
+
}
|
|
113
|
+
return floors;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The committed floor manifest, or an empty manifest when it does not exist yet. */
|
|
117
|
+
export function readFloors(root: string): Record<string, number> {
|
|
118
|
+
const path = join(root, FLOOR_MANIFEST_FILE);
|
|
119
|
+
if (!existsSync(path)) return {};
|
|
120
|
+
return parseFloors(JSON.parse(readFileSync(path, "utf8")), FLOOR_MANIFEST_FILE);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The next floor manifest: each report's floor rises to its current coverage and
|
|
125
|
+
* never falls, a report with no floor gains one, and a floor whose report is gone
|
|
126
|
+
* is dropped. Monotone by construction — no code path here writes a smaller
|
|
127
|
+
* number, so a regression can only ever be reported by the gate, never absorbed.
|
|
128
|
+
*/
|
|
129
|
+
export function raiseFloors(
|
|
130
|
+
floors: Record<string, number>,
|
|
131
|
+
reports: Record<string, FidelityFloorReport>,
|
|
132
|
+
): Record<string, number> {
|
|
133
|
+
const next: Record<string, number> = {};
|
|
134
|
+
for (const key of Object.keys(reports).sort()) {
|
|
135
|
+
const existing = floors[key];
|
|
136
|
+
const { coverage } = reports[key] as FidelityFloorReport;
|
|
137
|
+
next[key] = existing === undefined ? coverage : Math.max(existing, coverage);
|
|
138
|
+
}
|
|
139
|
+
return next;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// `fidelity-floor.json` sits at the package root, so unlike the reports under
|
|
143
|
+
// `fidelity/` it is not biome-excluded and must match Biome's formatting.
|
|
144
|
+
function biomeFormatJson(raw: string): string {
|
|
145
|
+
const out = Bun.spawnSync(
|
|
146
|
+
["bunx", "biome", "format", `--stdin-file-path=${FLOOR_MANIFEST_FILE}`],
|
|
147
|
+
{
|
|
148
|
+
stdin: Buffer.from(raw),
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
if (out.exitCode !== 0) {
|
|
152
|
+
throw new Error(`biome format failed: ${out.stderr.toString()}`);
|
|
153
|
+
}
|
|
154
|
+
return out.stdout.toString();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (import.meta.main) {
|
|
158
|
+
const root = resolve(import.meta.dir, "..");
|
|
159
|
+
const reports = collectFidelityReports(root);
|
|
160
|
+
const current = readFloors(root);
|
|
161
|
+
const next = raiseFloors(current, reports);
|
|
162
|
+
|
|
163
|
+
if (process.argv.includes("--raise")) {
|
|
164
|
+
for (const key of Object.keys(next)) {
|
|
165
|
+
const before = current[key];
|
|
166
|
+
if (before === undefined) console.log(`${key}: (new) -> ${next[key]}`);
|
|
167
|
+
else if (before !== next[key]) console.log(`${key}: ${before} -> ${next[key]}`);
|
|
168
|
+
}
|
|
169
|
+
for (const key of Object.keys(current)) {
|
|
170
|
+
if (next[key] === undefined) console.log(`${key}: dropped (no such report)`);
|
|
171
|
+
}
|
|
172
|
+
const path = join(root, FLOOR_MANIFEST_FILE);
|
|
173
|
+
writeFileSync(path, biomeFormatJson(JSON.stringify(next)));
|
|
174
|
+
console.log(`wrote ${path}`);
|
|
175
|
+
} else {
|
|
176
|
+
console.log(JSON.stringify(next, null, 2));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carrying upstream's own LuaDoc into the authored lane's api-doc, for members the
|
|
3
|
+
* fork documents nowhere.
|
|
4
|
+
*
|
|
5
|
+
* `parse-lua-surface.ts` already reads each upstream member's `---` block and
|
|
6
|
+
* `authored-parity.ts` already counts how many of them the fork drops, so the text
|
|
7
|
+
* exists on one side of the pipeline and was thrown away before the docs-site saw
|
|
8
|
+
* it. This module is the merge, and it runs at api-doc lowering rather than in the
|
|
9
|
+
* vendored `.d.ts`: writing upstream prose into `fixtures/authored/*.d.ts` would
|
|
10
|
+
* still pass the forked-vs-generated identity diff while destroying what that diff
|
|
11
|
+
* exists to prove — that the emitted surface *is* the vendored fork — and imported
|
|
12
|
+
* prose would be indistinguishable from fork prose forever after.
|
|
13
|
+
*
|
|
14
|
+
* Three rules carry the whole merge:
|
|
15
|
+
*
|
|
16
|
+
* - **Fork prose always wins.** An element the fork gave either a `brief` or a
|
|
17
|
+
* `description` is returned untouched and gains no `docSource`, so authoring the
|
|
18
|
+
* fork's own doc-comment is how a member opts out. There is no exclusion list:
|
|
19
|
+
* the only thing one would add is "show no prose at all for a member upstream
|
|
20
|
+
* documented", which is worse than writing the correct brief.
|
|
21
|
+
* - **Tags never come with the summary.** Every block in this corpus is a summary
|
|
22
|
+
* followed by `@param`/`@return` lines, and a tag's continuation lines carry no
|
|
23
|
+
* marker of their own, so truncating at the *first* `@` line is what keeps
|
|
24
|
+
* LuaDoc-derived types and parameter names out of the api-doc.
|
|
25
|
+
* - **A brief never restates its own symbol.** A leading line that is exactly the
|
|
26
|
+
* element's name is upstream's heading, not prose, and is dropped before the brief is
|
|
27
|
+
* taken. That is the importer's knowledge, not `summarizeLuaDoc`'s, which stays a pure
|
|
28
|
+
* block-to-prose function.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { LuaMember } from "./parse-lua-surface";
|
|
32
|
+
|
|
33
|
+
/** One `api-doc/<namespace>.json` element, open at the edges: this module reads the
|
|
34
|
+
* four keys the merge turns on and passes every other key through in place. */
|
|
35
|
+
export interface ApiDocElement {
|
|
36
|
+
type: string;
|
|
37
|
+
name: string;
|
|
38
|
+
global?: boolean;
|
|
39
|
+
brief?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
docSource?: string;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The prose half of a LuaDoc block: its lines up to the first whose trimmed text
|
|
47
|
+
* starts with `@`, rejoined and trimmed. Empty for a block that is only tags.
|
|
48
|
+
*
|
|
49
|
+
* Interior newlines and blank lines survive. Markdown collapses a soft break at
|
|
50
|
+
* render time, so no unwrapping is done and the imported description stays
|
|
51
|
+
* upstream's own text.
|
|
52
|
+
*/
|
|
53
|
+
export function summarizeLuaDoc(doc: string): string {
|
|
54
|
+
const summary: string[] = [];
|
|
55
|
+
for (const line of doc.split("\n")) {
|
|
56
|
+
if (line.trim().startsWith("@")) break;
|
|
57
|
+
summary.push(line);
|
|
58
|
+
}
|
|
59
|
+
return summary.join("\n").trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The summary without a leading line that is only the element's own name. nakama writes
|
|
63
|
+
* `--- <member_name>` above the real summary, and `briefOf` takes the first line, so
|
|
64
|
+
* leaving it in ships a brief that restates the symbol it sits under. The comparison is
|
|
65
|
+
* exact: a first line that merely mentions the name is prose and survives. */
|
|
66
|
+
function withoutNameLine(summary: string, name: string): string {
|
|
67
|
+
const [first, ...rest] = summary.split("\n");
|
|
68
|
+
return (first as string).trim() === name ? rest.join("\n").trim() : summary;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The element with upstream's summary in place, `docSource` inserted immediately
|
|
72
|
+
* after `description` by copying the element's own entries in order rather than
|
|
73
|
+
* assigning onto it — the key order is what the committed api-doc golden diffs on. */
|
|
74
|
+
function withImportedDoc(element: ApiDocElement, summary: string): ApiDocElement {
|
|
75
|
+
const brief = summary.split("\n")[0]?.trim() ?? "";
|
|
76
|
+
const imported: ApiDocElement = { type: element.type, name: element.name };
|
|
77
|
+
let placedBrief = false;
|
|
78
|
+
let placedDescription = false;
|
|
79
|
+
for (const [key, value] of Object.entries(element)) {
|
|
80
|
+
if (key === "type" || key === "name") continue;
|
|
81
|
+
if (key === "brief") {
|
|
82
|
+
imported.brief = brief;
|
|
83
|
+
placedBrief = true;
|
|
84
|
+
} else if (key === "description") {
|
|
85
|
+
imported.description = summary;
|
|
86
|
+
imported.docSource = "upstream";
|
|
87
|
+
placedDescription = true;
|
|
88
|
+
} else imported[key] = value;
|
|
89
|
+
}
|
|
90
|
+
if (!placedDescription) {
|
|
91
|
+
imported.description = summary;
|
|
92
|
+
imported.docSource = "upstream";
|
|
93
|
+
}
|
|
94
|
+
if (!placedBrief) imported.brief = brief;
|
|
95
|
+
return imported;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The api-doc element list with upstream prose merged in, as a new list.
|
|
100
|
+
*
|
|
101
|
+
* An element is imported when it is a `FUNCTION` or a `VARIABLE` — the two kinds
|
|
102
|
+
* `authored-parity.ts` compares, a `TYPEDEF` being a type rather than a runtime
|
|
103
|
+
* member — is not `global: true`, has an empty `brief` *and* an empty
|
|
104
|
+
* `description`, matches an upstream member by exact name, and that member's
|
|
105
|
+
* summary survives tag stripping. Every other element is returned unchanged.
|
|
106
|
+
*
|
|
107
|
+
* The name match is exact on purpose: a namespace-qualified api-doc name
|
|
108
|
+
* (`bridge.bridge`) never matches a bare upstream name, which is the same non-match
|
|
109
|
+
* the parity pass makes.
|
|
110
|
+
*/
|
|
111
|
+
export function importUpstreamDocs(
|
|
112
|
+
elements: readonly ApiDocElement[],
|
|
113
|
+
members: ReadonlyMap<string, LuaMember>,
|
|
114
|
+
): ApiDocElement[] {
|
|
115
|
+
return elements.map((element) => {
|
|
116
|
+
if (element.type !== "FUNCTION" && element.type !== "VARIABLE") return element;
|
|
117
|
+
if (element.global === true) return element;
|
|
118
|
+
if ((element.brief ?? "") !== "" || (element.description ?? "") !== "") return element;
|
|
119
|
+
const member = members.get(element.name);
|
|
120
|
+
if (member === undefined) return element;
|
|
121
|
+
const summary = withoutNameLine(summarizeLuaDoc(member.doc), element.name);
|
|
122
|
+
if (summary === "") return element;
|
|
123
|
+
return withImportedDoc(element, summary);
|
|
124
|
+
});
|
|
125
|
+
}
|